From 92b2cddc4d0697301550d1315b2b10961bc8e372 Mon Sep 17 00:00:00 2001 From: e-roy Date: Sun, 24 May 2026 17:08:09 -0400 Subject: [PATCH 01/13] update readme --- .claude/commands/create_plan.md | 468 ++++++++++++++++++++++++++ .claude/commands/implement_plan.md | 91 +++++ .claude/commands/research_codebase.md | 227 +++++++++++++ .cursor/rules/file-structure.mdc | 43 ++- CLAUDE.md | 81 +++++ README.md | 39 +++ apps/docs/README.md | 71 ++-- apps/examples/openai/README.md | 26 +- packages/api/README.md | 111 +++--- packages/tools/README.md | 28 +- packages/types/README.md | 17 +- 11 files changed, 1069 insertions(+), 133 deletions(-) create mode 100644 .claude/commands/create_plan.md create mode 100644 .claude/commands/implement_plan.md create mode 100644 .claude/commands/research_codebase.md create mode 100644 CLAUDE.md diff --git a/.claude/commands/create_plan.md b/.claude/commands/create_plan.md new file mode 100644 index 0000000..0c673f5 --- /dev/null +++ b/.claude/commands/create_plan.md @@ -0,0 +1,468 @@ +--- +description: Create detailed implementation plans through interactive research and iteration +model: opus +--- + +# Create Plan + +You are tasked with creating detailed implementation plans through an interactive, iterative process. You should be skeptical, thorough, and work collaboratively with the user to produce high-quality technical specifications. + +## Initial Response + +When this command is invoked: + +1. **Check if parameters were provided**: + - If a file path or ticket reference was provided as a parameter, skip the default message + - Immediately read any provided files FULLY + - Begin the research process + +2. **If no parameters provided**, respond with: + +``` +I'll help you create a detailed implementation plan. Let me start by understanding what we're building. + +Please provide: +1. The task/ticket description (or reference to a ticket file) +2. Any relevant context, constraints, or specific requirements +3. Links to related research or previous implementations + +I'll analyze this information and work with you to create a comprehensive plan. + +Tip: You can also invoke this command with a ticket file directly: `/create_plan thoughts/allison/tickets/eng_1234.md` +For deeper analysis, try: `/create_plan think deeply about thoughts/allison/tickets/eng_1234.md` +``` + +Then wait for the user's input. + +## Process Steps + +### Step 1: Context Gathering & Initial Analysis + +1. **Read all mentioned files immediately and FULLY**: + - Ticket files (e.g., `thoughts/allison/tickets/eng_1234.md`) + - Research documents + - Related implementation plans + - Any JSON/data files mentioned + - **IMPORTANT**: Use the Read tool WITHOUT limit/offset parameters to read entire files + - **CRITICAL**: DO NOT spawn sub-tasks before reading these files yourself in the main context + - **NEVER** read files partially - if a file is mentioned, read it completely + +2. **Spawn initial research tasks to gather context**: + Before asking the user any questions, use specialized agents to research in parallel: + - Use the **codebase-locator** agent to find all files related to the ticket/task + - Use the **codebase-analyzer** agent to understand how the current implementation works + - If relevant, use the **thoughts-locator** agent to find any existing thoughts documents about this feature + - If a Linear ticket is mentioned, use the **linear-ticket-reader** agent to get full details + + These agents will: + - Find relevant source files, configs, and tests + - Identify the specific directories to focus on (e.g., if WUI is mentioned, they'll focus on humanlayer-wui/) + - Trace data flow and key functions + - Return detailed explanations with file:line references + +3. **Read all files identified by research tasks**: + - After research tasks complete, read ALL files they identified as relevant + - Read them FULLY into the main context + - This ensures you have complete understanding before proceeding + +4. **Analyze and verify understanding**: + - Cross-reference the ticket requirements with actual code + - Identify any discrepancies or misunderstandings + - Note assumptions that need verification + - Determine true scope based on codebase reality + +5. **Present informed understanding and focused questions**: + + ``` + Based on the ticket and my research of the codebase, I understand we need to [accurate summary]. + + I've found that: + - [Current implementation detail with file:line reference] + - [Relevant pattern or constraint discovered] + - [Potential complexity or edge case identified] + + Questions that my research couldn't answer: + - [Specific technical question that requires human judgment] + - [Business logic clarification] + - [Design preference that affects implementation] + ``` + + Only ask questions that you genuinely cannot answer through code investigation. + +### Step 2: Research & Discovery + +After getting initial clarifications: + +1. **If the user corrects any misunderstanding**: + - DO NOT just accept the correction + - Spawn new research tasks to verify the correct information + - Read the specific files/directories they mention + - Only proceed once you've verified the facts yourself + +2. **Create a research todo list** using TodoWrite to track exploration tasks + +3. **Spawn parallel sub-tasks for comprehensive research**: + - Create multiple Task agents to research different aspects concurrently + - Use the right agent for each type of research: + + **For deeper investigation:** + - **codebase-locator** - To find more specific files (e.g., "find all files that handle [specific component]") + - **codebase-analyzer** - To understand implementation details (e.g., "analyze how [system] works") + - **codebase-pattern-finder** - To find similar features we can model after + + **For historical context:** + - **thoughts-locator** - To find any research, plans, or decisions about this area + - **thoughts-analyzer** - To extract key insights from the most relevant documents + + **For related tickets:** + - **linear-searcher** - To find similar issues or past implementations + + Each agent knows how to: + - Find the right files and code patterns + - Identify conventions and patterns to follow + - Look for integration points and dependencies + - Return specific file:line references + - Find tests and examples + +4. **Wait for ALL sub-tasks to complete** before proceeding + +5. **Present findings and design options**: + + ``` + Based on my research, here's what I found: + + **Current State:** + - [Key discovery about existing code] + - [Pattern or convention to follow] + + **Design Options:** + 1. [Option A] - [pros/cons] + 2. [Option B] - [pros/cons] + + **Open Questions:** + - [Technical uncertainty] + - [Design decision needed] + + Which approach aligns best with your vision? + ``` + +### Step 3: Plan Structure Development + +Once aligned on approach: + +1. **Create initial plan outline**: + + ``` + Here's my proposed plan structure: + + ## Overview + [1-2 sentence summary] + + ## Implementation Phases: + 1. [Phase name] - [what it accomplishes] + 2. [Phase name] - [what it accomplishes] + 3. [Phase name] - [what it accomplishes] + + Does this phasing make sense? Should I adjust the order or granularity? + ``` + +2. **Get feedback on structure** before writing details + +### Step 4: Detailed Plan Writing + +After structure approval: + +1. **Write the plan** to `thoughts/shared/plans/YYYY-MM-DD-ENG-XXXX-description.md` + - Format: `YYYY-MM-DD-ENG-XXXX-description.md` where: + - YYYY-MM-DD is today's date + - ENG-XXXX is the ticket number (omit if no ticket) + - description is a brief kebab-case description + - Examples: + - With ticket: `2025-01-08-ENG-1478-parent-child-tracking.md` + - Without ticket: `2025-01-08-improve-error-handling.md` +2. **Use this template structure**: + +````markdown +# [Feature/Task Name] Implementation Plan + +## Overview + +[Brief description of what we're implementing and why] + +## Current State Analysis + +[What exists now, what's missing, key constraints discovered] + +## Desired End State + +[A Specification of the desired end state after this plan is complete, and how to verify it] + +### Key Discoveries: + +- [Important finding with file:line reference] +- [Pattern to follow] +- [Constraint to work within] + +## What We're NOT Doing + +[Explicitly list out-of-scope items to prevent scope creep] + +## Implementation Approach + +[High-level strategy and reasoning] + +## Phase 1: [Descriptive Name] + +### Overview + +[What this phase accomplishes] + +### Changes Required: + +#### 1. [Component/File Group] + +**File**: `path/to/file.ext` +**Changes**: [Summary of changes] + +```[language] +// Specific code to add/modify +``` + +### Success Criteria: + +#### Automated Verification: + +- [ ] Migration applies cleanly: `make migrate` +- [ ] Unit tests pass: `make test-component` +- [ ] Type checking passes: `npm run typecheck` +- [ ] Linting passes: `make lint` +- [ ] Integration tests pass: `make test-integration` + +#### Manual Verification: + +- [ ] Feature works as expected when tested via UI +- [ ] Performance is acceptable under load +- [ ] Edge case handling verified manually +- [ ] No regressions in related features + +**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human that the manual testing was successful before proceeding to the next phase. + +--- + +## Phase 2: [Descriptive Name] + +[Similar structure with both automated and manual success criteria...] + +--- + +## Testing Strategy + +### Unit Tests: + +- [What to test] +- [Key edge cases] + +### Integration Tests: + +- [End-to-end scenarios] + +### Manual Testing Steps: + +1. [Specific step to verify feature] +2. [Another verification step] +3. [Edge case to test manually] + +## Performance Considerations + +[Any performance implications or optimizations needed] + +## Migration Notes + +[If applicable, how to handle existing data/systems] + +## References + +- Original ticket: `thoughts/allison/tickets/eng_XXXX.md` +- Related research: `thoughts/shared/research/[relevant].md` +- Similar implementation: `[file:line]` +```` + +### Step 5: Sync and Review + +1. **Sync the thoughts directory**: + - Run `humanlayer thoughts sync` to sync the newly created plan + - This ensures the plan is properly indexed and available + +2. **Present the draft plan location**: + + ``` + I've created the initial implementation plan at: + `thoughts/shared/plans/YYYY-MM-DD-ENG-XXXX-description.md` + + Please review it and let me know: + - Are the phases properly scoped? + - Are the success criteria specific enough? + - Any technical details that need adjustment? + - Missing edge cases or considerations? + ``` + +3. **Iterate based on feedback** - be ready to: + - Add missing phases + - Adjust technical approach + - Clarify success criteria (both automated and manual) + - Add/remove scope items + - After making changes, run `humanlayer thoughts sync` again + +4. **Continue refining** until the user is satisfied + +## Important Guidelines + +1. **Be Skeptical**: + - Question vague requirements + - Identify potential issues early + - Ask "why" and "what about" + - Don't assume - verify with code + +2. **Be Interactive**: + - Don't write the full plan in one shot + - Get buy-in at each major step + - Allow course corrections + - Work collaboratively + +3. **Be Thorough**: + - Read all context files COMPLETELY before planning + - Research actual code patterns using parallel sub-tasks + - Include specific file paths and line numbers + - Write measurable success criteria with clear automated vs manual distinction + - automated steps should use `make` whenever possible - for example `make -C humanlayer-wui check` instead of `cd humanlayer-wui && bun run fmt` + +4. **Be Practical**: + - Focus on incremental, testable changes + - Consider migration and rollback + - Think about edge cases + - Include "what we're NOT doing" + +5. **Track Progress**: + - Use TodoWrite to track planning tasks + - Update todos as you complete research + - Mark planning tasks complete when done + +6. **No Open Questions in Final Plan**: + - If you encounter open questions during planning, STOP + - Research or ask for clarification immediately + - Do NOT write the plan with unresolved questions + - The implementation plan must be complete and actionable + - Every decision must be made before finalizing the plan + +## Success Criteria Guidelines + +**Always separate success criteria into two categories:** + +1. **Automated Verification** (can be run by execution agents): + - Commands that can be run: `make test`, `npm run lint`, etc. + - Specific files that should exist + - Code compilation/type checking + - Automated test suites + +2. **Manual Verification** (requires human testing): + - UI/UX functionality + - Performance under real conditions + - Edge cases that are hard to automate + - User acceptance criteria + +**Format example:** + +```markdown +### Success Criteria: + +#### Automated Verification: + +- [ ] Database migration runs successfully: `make migrate` +- [ ] All unit tests pass: `go test ./...` +- [ ] No linting errors: `golangci-lint run` +- [ ] API endpoint returns 200: `curl localhost:8080/api/new-endpoint` + +#### Manual Verification: + +- [ ] New feature appears correctly in the UI +- [ ] Performance is acceptable with 1000+ items +- [ ] Error messages are user-friendly +- [ ] Feature works correctly on mobile devices +``` + +## Common Patterns + +### For Database Changes: + +- Start with schema/migration +- Add store methods +- Update business logic +- Expose via API +- Update clients + +### For New Features: + +- Research existing patterns first +- Start with data model +- Build backend logic +- Add API endpoints +- Implement UI last + +### For Refactoring: + +- Document current behavior +- Plan incremental changes +- Maintain backwards compatibility +- Include migration strategy + +## Sub-task Spawning Best Practices + +When spawning research sub-tasks: + +1. **Spawn multiple tasks in parallel** for efficiency +2. **Each task should be focused** on a specific area +3. **Provide detailed instructions** including: + - Exactly what to search for + - Which directories to focus on + - What information to extract + - Expected output format +4. **Be EXTREMELY specific about directories**: + - If the ticket mentions "WUI", specify `humanlayer-wui/` directory + - If it mentions "daemon", specify `hld/` directory + - Never use generic terms like "UI" when you mean "WUI" + - Include the full path context in your prompts +5. **Specify read-only tools** to use +6. **Request specific file:line references** in responses +7. **Wait for all tasks to complete** before synthesizing +8. **Verify sub-task results**: + - If a sub-task returns unexpected results, spawn follow-up tasks + - Cross-check findings against the actual codebase + - Don't accept results that seem incorrect + +Example of spawning multiple tasks: + +```python +# Spawn these tasks concurrently: +tasks = [ + Task("Research database schema", db_research_prompt), + Task("Find API patterns", api_research_prompt), + Task("Investigate UI components", ui_research_prompt), + Task("Check test patterns", test_research_prompt) +] +``` + +## Example Interaction Flow + +``` +User: /create_plan +Assistant: I'll help you create a detailed implementation plan... + +User: We need to add parent-child tracking for Claude sub-tasks. See thoughts/allison/tickets/eng_1478.md +Assistant: Let me read that ticket file completely first... + +[Reads file fully] + +Based on the ticket, I understand we need to track parent-child relationships for Claude sub-task events in the hld daemon. Before I start planning, I have some questions... + +[Interactive process continues...] +``` diff --git a/.claude/commands/implement_plan.md b/.claude/commands/implement_plan.md new file mode 100644 index 0000000..6f89479 --- /dev/null +++ b/.claude/commands/implement_plan.md @@ -0,0 +1,91 @@ +--- +description: Implement technical plans from thoughts/shared/plans with verification +--- + +# Implement Plan + +You are tasked with implementing an approved technical plan from `thoughts/shared/plans/`. These plans contain phases with specific changes and success criteria. + +## Getting Started + +When given a plan path: + +- Read the plan completely and check for any existing checkmarks (- [x]) +- Read the original ticket and all files mentioned in the plan +- **Read files fully** - never use limit/offset parameters, you need complete context +- Think deeply about how the pieces fit together +- Create a todo list to track your progress +- Start implementing if you understand what needs to be done + +If no plan path provided, ask for one. + +## Implementation Philosophy + +Plans are carefully designed, but reality can be messy. Your job is to: + +- Follow the plan's intent while adapting to what you find +- Implement each phase fully before moving to the next +- Verify your work makes sense in the broader codebase context +- Update checkboxes in the plan as you complete sections + +When things don't match the plan exactly, think about why and communicate clearly. The plan is your guide, but your judgment matters too. + +If you encounter a mismatch: + +- STOP and think deeply about why the plan can't be followed +- Present the issue clearly: + + ``` + Issue in Phase [N]: + Expected: [what the plan says] + Found: [actual situation] + Why this matters: [explanation] + + How should I proceed? + ``` + +## Verification Approach + +After implementing a phase: + +- Run the success criteria checks (usually `make check test` covers everything) +- Fix any issues before proceeding +- Update your progress in both the plan and your todos +- Check off completed items in the plan file itself using Edit +- **Pause for human verification**: After completing all automated verification for a phase, pause and inform the human that the phase is ready for manual testing. Use this format: + + ``` + Phase [N] Complete - Ready for Manual Verification + + Automated verification passed: + - [List automated checks that passed] + + Please perform the manual verification steps listed in the plan: + - [List manual verification items from the plan] + + Let me know when manual testing is complete so I can proceed to Phase [N+1]. + ``` + +If instructed to execute multiple phases consecutively, skip the pause until the last phase. Otherwise, assume you are just doing one phase. + +do not check off items in the manual testing steps until confirmed by the user. + +## If You Get Stuck + +When something isn't working as expected: + +- First, make sure you've read and understood all the relevant code +- Consider if the codebase has evolved since the plan was written +- Present the mismatch clearly and ask for guidance + +Use sub-tasks sparingly - mainly for targeted debugging or exploring unfamiliar territory. + +## Resuming Work + +If the plan has existing checkmarks: + +- Trust that completed work is done +- Pick up from the first unchecked item +- Verify previous work only if something seems off + +Remember: You're implementing a solution, not just checking boxes. Keep the end goal in mind and maintain forward momentum. diff --git a/.claude/commands/research_codebase.md b/.claude/commands/research_codebase.md new file mode 100644 index 0000000..1065e28 --- /dev/null +++ b/.claude/commands/research_codebase.md @@ -0,0 +1,227 @@ +--- +description: Document codebase as-is with thoughts directory for historical context +model: opus +--- + +# Research Codebase + +You are tasked with conducting comprehensive research across the codebase to answer user questions by spawning parallel sub-agents and synthesizing their findings. + +## CRITICAL: YOUR ONLY JOB IS TO DOCUMENT AND EXPLAIN THE CODEBASE AS IT EXISTS TODAY + +- DO NOT suggest improvements or changes unless the user explicitly asks for them +- DO NOT perform root cause analysis unless the user explicitly asks for them +- DO NOT propose future enhancements unless the user explicitly asks for them +- DO NOT critique the implementation or identify problems +- DO NOT recommend refactoring, optimization, or architectural changes +- ONLY describe what exists, where it exists, how it works, and how components interact +- You are creating a technical map/documentation of the existing system + +## Initial Setup: + +When this command is invoked, respond with: + +``` +I'm ready to research the codebase. Please provide your research question or area of interest, and I'll analyze it thoroughly by exploring relevant components and connections. +``` + +Then wait for the user's research query. + +## Steps to follow after receiving the research query: + +1. **Read any directly mentioned files first:** + - If the user mentions specific files (tickets, docs, JSON), read them FULLY first + - **IMPORTANT**: Use the Read tool WITHOUT limit/offset parameters to read entire files + - **CRITICAL**: Read these files yourself in the main context before spawning any sub-tasks + - This ensures you have full context before decomposing the research + +2. **Analyze and decompose the research question:** + - Break down the user's query into composable research areas + - Take time to ultrathink about the underlying patterns, connections, and architectural implications the user might be seeking + - Identify specific components, patterns, or concepts to investigate + - Create a research plan using TodoWrite to track all subtasks + - Consider which directories, files, or architectural patterns are relevant + +3. **Spawn parallel sub-agent tasks for comprehensive research:** + - Create multiple Task agents to research different aspects concurrently + - We now have specialized agents that know how to do specific research tasks: + + **For codebase research:** + - Use the **codebase-locator** agent to find WHERE files and components live + - Use the **codebase-analyzer** agent to understand HOW specific code works (without critiquing it) + - Use the **codebase-pattern-finder** agent to find examples of existing patterns (without evaluating them) + + **IMPORTANT**: All agents are documentarians, not critics. They will describe what exists without suggesting improvements or identifying issues. + + **For thoughts directory:** + - Use the **thoughts-locator** agent to discover what documents exist about the topic + - Use the **thoughts-analyzer** agent to extract key insights from specific documents (only the most relevant ones) + + **For web research (only if user explicitly asks):** + - Use the **web-search-researcher** agent for external documentation and resources + - IF you use web-research agents, instruct them to return LINKS with their findings, and please INCLUDE those links in your final report + + **For Linear tickets (if relevant):** + - Use the **linear-ticket-reader** agent to get full details of a specific ticket + - Use the **linear-searcher** agent to find related tickets or historical context + + The key is to use these agents intelligently: + - Start with locator agents to find what exists + - Then use analyzer agents on the most promising findings to document how they work + - Run multiple agents in parallel when they're searching for different things + - Each agent knows its job - just tell it what you're looking for + - Don't write detailed prompts about HOW to search - the agents already know + - Remind agents they are documenting, not evaluating or improving + +4. **Wait for all sub-agents to complete and synthesize findings:** + - IMPORTANT: Wait for ALL sub-agent tasks to complete before proceeding + - Compile all sub-agent results (both codebase and thoughts findings) + - Prioritize live codebase findings as primary source of truth + - Use thoughts/ findings as supplementary historical context + - Connect findings across different components + - Include specific file paths and line numbers for reference + - Verify all thoughts/ paths are correct (e.g., thoughts/allison/ not thoughts/shared/ for personal files) + - Highlight patterns, connections, and architectural decisions + - Answer the user's specific questions with concrete evidence + +5. **Gather metadata for the research document:** + - Run the `hack/spec_metadata.sh` script to generate all relevant metadata + - Filename: `thoughts/shared/research/YYYY-MM-DD-ENG-XXXX-description.md` + - Format: `YYYY-MM-DD-ENG-XXXX-description.md` where: + - YYYY-MM-DD is today's date + - ENG-XXXX is the ticket number (omit if no ticket) + - description is a brief kebab-case description of the research topic + - Examples: + - With ticket: `2025-01-08-ENG-1478-parent-child-tracking.md` + - Without ticket: `2025-01-08-authentication-flow.md` + +6. **Generate research document:** + - Use the metadata gathered in step 4 + - Structure the document with YAML frontmatter followed by content: + + ```markdown + --- + date: [Current date and time with timezone in ISO format] + researcher: [Researcher name from thoughts status] + git_commit: [Current commit hash] + branch: [Current branch name] + repository: [Repository name] + topic: "[User's Question/Topic]" + tags: [research, codebase, relevant-component-names] + status: complete + last_updated: [Current date in YYYY-MM-DD format] + last_updated_by: [Researcher name] + --- + + # Research: [User's Question/Topic] + + **Date**: [Current date and time with timezone from step 4] + **Researcher**: [Researcher name from thoughts status] + **Git Commit**: [Current commit hash from step 4] + **Branch**: [Current branch name from step 4] + **Repository**: [Repository name] + + ## Research Question + + [Original user query] + + ## Summary + + [High-level documentation of what was found, answering the user's question by describing what exists] + + ## Detailed Findings + + ### [Component/Area 1] + + - Description of what exists ([file.ext:line](link)) + - How it connects to other components + - Current implementation details (without evaluation) + + ### [Component/Area 2] + + ... + + ## Code References + + - `path/to/file.py:123` - Description of what's there + - `another/file.ts:45-67` - Description of the code block + + ## Architecture Documentation + + [Current patterns, conventions, and design implementations found in the codebase] + + ## Historical Context (from thoughts/) + + [Relevant insights from thoughts/ directory with references] + + - `thoughts/shared/something.md` - Historical decision about X + - `thoughts/local/notes.md` - Past exploration of Y + Note: Paths exclude "searchable/" even if found there + + ## Related Research + + [Links to other research documents in thoughts/shared/research/] + + ## Open Questions + + [Any areas that need further investigation] + ``` + +7. **Add GitHub permalinks (if applicable):** + - Check if on main branch or if commit is pushed: `git branch --show-current` and `git status` + - If on main/master or pushed, generate GitHub permalinks: + - Get repo info: `gh repo view --json owner,name` + - Create permalinks: `https://github.com/{owner}/{repo}/blob/{commit}/{file}#L{line}` + - Replace local file references with permalinks in the document + +8. **Sync and present findings:** + - Run `humanlayer thoughts sync` to sync the thoughts directory + - Present a concise summary of findings to the user + - Include key file references for easy navigation + - Ask if they have follow-up questions or need clarification + +9. **Handle follow-up questions:** + - If the user has follow-up questions, append to the same research document + - Update the frontmatter fields `last_updated` and `last_updated_by` to reflect the update + - Add `last_updated_note: "Added follow-up research for [brief description]"` to frontmatter + - Add a new section: `## Follow-up Research [timestamp]` + - Spawn new sub-agents as needed for additional investigation + - Continue updating the document and syncing + +## Important notes: + +- Always use parallel Task agents to maximize efficiency and minimize context usage +- Always run fresh codebase research - never rely solely on existing research documents +- The thoughts/ directory provides historical context to supplement live findings +- Focus on finding concrete file paths and line numbers for developer reference +- Research documents should be self-contained with all necessary context +- Each sub-agent prompt should be specific and focused on read-only documentation operations +- Document cross-component connections and how systems interact +- Include temporal context (when the research was conducted) +- Link to GitHub when possible for permanent references +- Keep the main agent focused on synthesis, not deep file reading +- Have sub-agents document examples and usage patterns as they exist +- Explore all of thoughts/ directory, not just research subdirectory +- **CRITICAL**: You and all sub-agents are documentarians, not evaluators +- **REMEMBER**: Document what IS, not what SHOULD BE +- **NO RECOMMENDATIONS**: Only describe the current state of the codebase +- **File reading**: Always read mentioned files FULLY (no limit/offset) before spawning sub-tasks +- **Critical ordering**: Follow the numbered steps exactly + - ALWAYS read mentioned files first before spawning sub-tasks (step 1) + - ALWAYS wait for all sub-agents to complete before synthesizing (step 4) + - ALWAYS gather metadata before writing the document (step 5 before step 6) + - NEVER write the research document with placeholder values +- **Path handling**: The thoughts/searchable/ directory contains hard links for searching + - Always document paths by removing ONLY "searchable/" - preserve all other subdirectories + - Examples of correct transformations: + - `thoughts/searchable/allison/old_stuff/notes.md` → `thoughts/allison/old_stuff/notes.md` + - `thoughts/searchable/shared/prs/123.md` → `thoughts/shared/prs/123.md` + - `thoughts/searchable/global/shared/templates.md` → `thoughts/global/shared/templates.md` + - NEVER change allison/ to shared/ or vice versa - preserve the exact directory structure + - This ensures paths are correct for editing and navigation +- **Frontmatter consistency**: + - Always include frontmatter at the beginning of research documents + - Keep frontmatter fields consistent across all research documents + - Update frontmatter when adding follow-up research + - Use snake_case for multi-word field names (e.g., `last_updated`, `git_commit`) + - Tags should be relevant to the research topic and components studied diff --git a/.cursor/rules/file-structure.mdc b/.cursor/rules/file-structure.mdc index 499fc50..f77fd36 100644 --- a/.cursor/rules/file-structure.mdc +++ b/.cursor/rules/file-structure.mdc @@ -1,3 +1,9 @@ +--- +description: File structure of entire project +globs: +alwaysApply: true +--- + # FMP Node Wrapper - Project Structure Rules ## Project Overview @@ -70,18 +76,23 @@ fmp-node-wrapper/ - `market/` - Market-wide data and performance - `economic/` - Economic indicators - `list/` - Stock lists and indices + - `screener/` - Stock screener and filter lookups - `calendar/` - Earnings and economic calendar - `senate-house/` - Congressional trading data - `institutional/` - Form 13F filings - `insider/` - Insider trading data - `sec/` - SEC filings and industry data + - `news/` - Financial news articles - **`apps/examples/`**: Example applications demonstrating FMP API usage - **`vercel-ai/`**: Vercel AI SDK integration example - - Chat interface using FMP tools with OpenAI + - Chat interface using FMP tools with the Vercel AI SDK - Demonstrates AI tool integration patterns - Built with Next.js 15, React 19, and Tailwind CSS - - Uses `fmp-tools` and `fmp-node-api` as workspace dependencies + - Uses `fmp-ai-tools` and `fmp-node-api` as workspace dependencies + - **`openai/`**: OpenAI integration example + - Demonstrates using FMP tools with OpenAI directly + - Uses `fmp-ai-tools` and `fmp-node-api` as workspace dependencies ### Packages @@ -98,28 +109,31 @@ fmp-node-wrapper/ - `market.ts` - Market-wide data and performance - `economic.ts` - Economic indicators - `list.ts` - Stock lists and indices + - `screener.ts` - Stock screener and available exchanges/sectors/industries/countries - `calendar.ts` - Earnings and economic calendar - `senate-house.ts` - Congressional trading data - `institutional.ts` - Form 13F filings - `insider.ts` - Insider trading data - `sec.ts` - SEC filings and industry data + - `news.ts` - Financial news (general, stock, crypto, forex; latest or by symbol) - Shared utilities and constants - Jest tests and manual testing scripts - Built with tsup for multiple output formats -- **`packages/tools/`**: AI tools for FMP API integrations (`fmp-tools`) +- **`packages/tools/`**: AI tools for FMP API integrations (`fmp-ai-tools`) - - AI tool implementations compatible with Vercel AI SDK, Langchain, OpenAI, and more - - Provider-specific implementations: + - AI tool implementations compatible with Vercel AI SDK, OpenAI, and more + - Provider-specific implementations (one file per endpoint category, aggregated in `index.ts`): - `providers/vercel-ai/` - Vercel AI SDK tool providers + - `providers/openai/` - OpenAI tool providers - Tool wrapper utilities and type definitions - Built with tsup for multiple output formats - - Uses `fmp-node-types` and `fmp-node-api` as workspace dependencies + - Depends on `fmp-node-api` as a workspace dependency - **`packages/types/`**: Shared TypeScript types (`fmp-node-types`) - Internal package containing all TypeScript interfaces and types - Organized by endpoint category (quote, stock, financial, etc.) - - Used by both `fmp-node-api` and `fmp-tools` packages + - Used by both `fmp-node-api` and `fmp-ai-tools` packages - Built with tsup for multiple output formats ## Build System @@ -213,12 +227,12 @@ fmp-node-wrapper/ - Internal packages use workspace dependencies: - `"fmp-node-api": "workspace:*"` - - `"fmp-tools": "workspace:*"` + - `"fmp-ai-tools": "workspace:*"` - `"fmp-node-types": "workspace:*"` - External dependencies are managed at the package level - Types are centralized in `fmp-node-types` package - API client is imported from `fmp-node-api` -- AI tools are imported from `fmp-tools` +- AI tools are imported from `fmp-ai-tools` (subpaths `fmp-ai-tools/vercel-ai`, `fmp-ai-tools/openai`) ## API Structure @@ -232,18 +246,21 @@ The API is organized into logical endpoint categories: - **Mutual Fund Endpoints**: Mutual fund data, NAV, and performance - **Market Endpoints**: Market performance, trading hours, sector data - **Economic Endpoints**: Economic indicators, treasury rates, inflation data -- **List Endpoints**: Stock listings, screening, and filtering +- **List Endpoints**: Stock listings, indices, and symbol lists +- **Screener Endpoints**: Stock screener with filters, plus available exchanges/sectors/industries/countries - **Calendar Endpoints**: Earnings calendar, economic calendar, events - **Senate House Endpoints**: Congressional trading data - **Institutional Endpoints**: Form 13F filings and institutional ownership - **Insider Endpoints**: Insider trading data and transactions - **SEC Endpoints**: SEC filings, RSS feeds, industry classification +- **News Endpoints**: Financial news articles — general, stock, crypto, and forex; latest feeds or filtered by symbol ## AI Tools Structure The AI tools package provides integrations for various AI platforms: -- **Vercel AI SDK**: Tool providers for Vercel AI SDK integration -- **Provider-specific implementations**: Organized by AI platform -- **Tool wrappers**: Utilities for creating and managing AI tools +- **Vercel AI SDK**: Tool providers exported from `fmp-ai-tools/vercel-ai` +- **OpenAI**: Tool providers exported from `fmp-ai-tools/openai` +- **Provider-specific implementations**: Organized by AI platform under `src/providers/` +- **Tool wrappers**: Utilities for creating and managing AI tools (`aisdk-tool-wrapper.ts`, `openai-tool-wrapper.ts`) - **Type definitions**: Shared types for tool implementations diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9f8423a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,81 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository layout + +Monorepo managed with **pnpm workspaces** + **Turborepo**. Three published packages plus apps: + +- `packages/types/` (`fmp-node-types`) — shared TypeScript interfaces, one file per endpoint category. No runtime code. Internal-only; not published standalone but bundled into the others. +- `packages/api/` (`fmp-node-api`) — the main API wrapper. Depends on `fmp-node-types`. +- `packages/tools/` (`fmp-ai-tools`) — AI/LLM tool definitions. Depends on `fmp-node-api` and `fmp-node-types`. +- `apps/docs/` — Next.js 15 documentation site that consumes `fmp-node-api`. +- `apps/examples/vercel-ai/` — example Next.js app using `fmp-ai-tools`. + +Dependency chain is `types → api → tools`. Turbo encodes this: most tasks `dependsOn: ["^build"]`, so a change in `types` requires upstream packages to rebuild before their tests/type-checks pass. + +## Commands + +**Always run from the repo root, and use `pnpm` exclusively** (npm/yarn will break the workspace). Turbo fans tasks out to packages. + +```bash +pnpm build # build all packages (tsup, respects dep order) +pnpm test # all tests (depends on ^build) +pnpm type-check # tsc --noEmit across packages +pnpm lint # NOTE: only lints packages/api + apps/docs +pnpm lint:all # turbo lint across every package (incl. tools) +pnpm format # prettier --write +``` + +Per-endpoint Jest runs (filter to `fmp-node-api`), e.g. `pnpm test:quote`, `pnpm test:stock`, `pnpm test:financial`, `pnpm test:sec`, etc. Also `pnpm test:unit` (client + fmp), `pnpm test:integration`, `pnpm test:endpoints`, `pnpm test:tools`. + +Run a single test file directly: +```bash +pnpm --filter fmp-node-api exec jest src/__tests__/endpoints/quote.test.ts +``` + +Manual live-API smoke tests (hit the real FMP API, need `FMP_API_KEY`): +```bash +pnpm test:endpoint # packages/api/scripts/test-endpoint.ts +pnpm test:tool # packages/tools manual tool runner +``` + +### API key for tests + +Integration tests and manual scripts read `FMP_API_KEY` from a root `.env` (`cp .env.example .env`). Turbo passes `FMP_API_KEY` through to test tasks (see `turbo.json` `env`). Constructing `new FMP()` with no key falls back to `process.env.FMP_API_KEY` and throws if absent or malformed (`FMPValidation.isValidApiKey`). + +## Architecture + +### The API client (`packages/api`) + +`FMP` (`src/fmp.ts`) is the public entry point. It validates the key, builds one `FMPClient`, and instantiates every endpoint class with it, exposing them as readonly fields (`fmp.quote`, `fmp.stock`, `fmp.financial`, …). To add an endpoint category, add the class in `src/endpoints/`, wire it in `fmp.ts`, and re-export it from `src/index.ts`. + +`FMPClient` (`src/client.ts`) is the key abstraction. **FMP has three live API surfaces — `v3`, `v4`, and `stable` — so the client holds three separate axios instances.** Every endpoint method passes the version explicitly: `this.client.get('/path', 'v3', params)`. An interceptor injects `apikey` into every request. Picking the wrong version is a common source of 404s. + +Two fetch methods with different response shaping, both returning `APIResponse` (`{ success, data, error, status }`) and never throwing (errors are captured into the response): +- `get` — for list endpoints. Normalizes so `data` is always an array (null/undefined → `[]`). +- `getSingle` — for single-object endpoints. Unwraps a single-element array to the object and normalizes null → `{}`. + +Choose `get` vs `getSingle` based on whether the endpoint conceptually returns one record or many. + +### Types (`packages/types`) + +All interfaces live here and are imported by package name (`import { Quote } from 'fmp-node-types'`), not relative paths — both `api` and `tools` resolve it as a workspace dependency. When adding/changing an endpoint, update the matching type file here first. + +### Path aliases + +Inside `packages/api` and `packages/tools`, source uses the `@/` alias for `src/` (e.g. `import { FMPClient } from '@/client'`), configured in each package's `tsconfig.json` and resolved by tsup/tsx. Jest maps `fmp-node-api` → `packages/api/src` (see root `jest.config.js`) so tools tests run against API source without a build. + +### AI tools (`packages/tools`) + +Tools wrap the `fmp-node-api` client for LLM use. `getFMPClient()` (`src/client.ts`) just does `new FMP()` (env-var key). Tools are organized by provider under `src/providers/` (`vercel-ai/`, `openai/`), one file per endpoint category, aggregated in each provider's `index.ts`. The package exports per-provider subpaths: `fmp-ai-tools/vercel-ai` and `fmp-ai-tools/openai`. `fmpTools` is the combined `ToolSet` for the Vercel AI SDK. Provider files use `createTool` wrappers (`src/utils/aisdk-tool-wrapper.ts`, `openai-tool-wrapper.ts`) with Zod input schemas. + +## Build & publish + +Each package builds with **tsup** to dual CJS+ESM with `.d.ts` (see per-package `tsup.config.ts`); `axios` is marked external in the API build. Versioning/publishing uses **Changesets**: `pnpm changeset` to record a change, `pnpm publish-packages` to build+lint+test+version+publish. + +## Conventions + +- Files: kebab-case. Functions/vars: camelCase. Classes/interfaces/types: PascalCase. Constants: UPPER_SNAKE_CASE. +- Prefer `interface` for object shapes; avoid `any` (use `unknown`). Strict mode is on. +- The published tools package is `fmp-ai-tools` (not `fmp-tools`); types are `fmp-node-types`; the API is `fmp-node-api`. When docs and code disagree, trust the code in `src/`. diff --git a/README.md b/README.md index 0ee2ba4..5044a26 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,41 @@ const earnings = await fmp.calendar.getEarningsCalendar({ const companies = await fmp.company.searchCompany({ query: 'Apple' }); ``` +### Stock Screener + +```typescript +// Screen stocks by multiple criteria +const largeCapTech = await fmp.screener.getScreener({ + marketCapMoreThan: 10000000000, // $10B+ + sector: 'Technology', + isActivelyTrading: true, + limit: 50, +}); + +// Lookup values available for screener filters +const exchanges = await fmp.screener.getAvailableExchanges(); +const sectors = await fmp.screener.getAvailableSectors(); +const industries = await fmp.screener.getAvailableIndustries(); +const countries = await fmp.screener.getAvailableCountries(); +``` + +### News + +```typescript +// Latest financial articles (FMP-authored) +const articles = await fmp.news.getArticles({ page: 1, limit: 20 }); + +// Latest news by asset class +const stockNews = await fmp.news.getStockNews({ from: '2024-01-01', to: '2024-01-31' }); +const cryptoNews = await fmp.news.getCryptoNews({ limit: 50 }); +const forexNews = await fmp.news.getForexNews({ limit: 50 }); + +// News for specific symbols +const aaplNews = await fmp.news.getStockNewsBySymbol({ symbols: ['AAPL', 'MSFT'] }); +const btcNews = await fmp.news.getCryptoNewsBySymbol({ symbols: ['BTCUSD'] }); +const eurNews = await fmp.news.getForexNewsBySymbol({ symbols: ['EURUSD'] }); +``` + ## Response Format All API methods return a standardized response format: @@ -273,11 +308,13 @@ All API responses and parameters are fully typed for complete type safety. - **`fmp.economic`** - Economic indicators - **`fmp.market`** - Market-wide data and performance - **`fmp.list`** - Stock lists and indices +- **`fmp.screener`** - Stock screener with filters and available exchanges/sectors/industries/countries - **`fmp.calendar`** - Earnings and economic calendar - **`fmp.senateHouse`** - Congressional trading data - **`fmp.institutional`** - Form 13F filings and institutional ownership - **`fmp.insider`** - Insider trading data - **`fmp.sec`** - SEC filings and industry classification +- **`fmp.news`** - Financial news articles (general, stock, crypto, forex; by symbol or latest) ## AI Tools Integration @@ -372,6 +409,7 @@ This is a monorepo containing: - **`apps/docs/`**: Next.js documentation site - **`apps/examples/`**: Example applications - **`vercel-ai/`**: Vercel AI SDK integration example + - **`openai/`**: OpenAI integration example ### Development Setup @@ -398,6 +436,7 @@ pnpm api:dev # Just API pnpm tools:dev # Just tools package pnpm types:dev # Just types package pnpm example:dev # Just Vercel AI example +pnpm example:openai:dev # Just OpenAI example pnpm build # Build all packages pnpm clean # Clean build artifacts pnpm clean:install # Clean all node_modules and reinstall diff --git a/apps/docs/README.md b/apps/docs/README.md index 9ce2977..f3750f5 100644 --- a/apps/docs/README.md +++ b/apps/docs/README.md @@ -10,40 +10,44 @@ The documentation is organized using MDX files for easy content management and e src/ ├── app/ │ ├── docs/ # Main documentation pages -│ │ ├── getting-started/ # Getting started guide -│ │ ├── api/ # API reference -│ │ │ ├── stock/ # Stock endpoints (market cap, splits, dividends) +│ │ ├── api/ # API wrapper (fmp-node-api) reference +│ │ │ ├── getting-started/ # Getting started guide +│ │ │ ├── configuration/ # Client configuration │ │ │ ├── quote/ # Quote endpoints (unified quotes for all assets) +│ │ │ ├── stock/ # Stock endpoints (market cap, splits, dividends) │ │ │ ├── financial/ # Financial endpoints │ │ │ ├── etf/ # ETF endpoints +│ │ │ ├── mutual-fund/ # Mutual fund endpoints │ │ │ ├── market/ # Market data endpoints │ │ │ ├── economic/ # Economic indicators -│ │ │ ├── mutual-fund/ # Mutual fund endpoints -│ │ │ ├── list/ # List and screening endpoints +│ │ │ ├── news/ # Financial news endpoints +│ │ │ ├── list/ # List endpoints +│ │ │ ├── screener/ # Stock screener endpoints │ │ │ ├── calendar/ # Calendar and events endpoints │ │ │ ├── company/ # Company information endpoints │ │ │ ├── senate-house/ # Congressional trading data │ │ │ ├── institutional/ # Form 13F filings │ │ │ ├── insider/ # Insider trading data -│ │ │ └── sec/ # SEC filings and industry data -│ │ ├── examples/ # Code examples -│ │ ├── layout.tsx # Documentation layout -│ │ └── page.mdx # Main docs page +│ │ │ ├── sec/ # SEC filings and industry data +│ │ │ ├── helpers/ # Helper utilities +│ │ │ ├── examples/ # Code examples +│ │ │ ├── layout.tsx # API docs nav + layout +│ │ │ └── page.mdx # API reference landing page +│ │ └── tools/ # AI tools (fmp-ai-tools) reference +│ │ ├── categories/ # Tool categories +│ │ ├── best-practices/ # Usage best practices +│ │ ├── error-handling/ # Error handling guide +│ │ ├── vercel-ai/ # Vercel AI SDK provider docs +│ │ ├── openai/ # OpenAI provider docs +│ │ ├── layout.tsx # Tools docs nav + layout +│ │ └── page.mdx # Tools landing page │ ├── layout.tsx # Root layout │ └── page.tsx # Homepage ├── components/ -│ ├── mdx/ # MDX-specific components -│ │ ├── code-block.tsx # Syntax highlighting -│ │ └── api-table.tsx # API endpoint and parameter tables -│ ├── layout/ # Layout components -│ │ ├── header.tsx # Site header -│ │ └── footer.tsx # Site footer -│ ├── theme/ # Theme components -│ │ ├── theme-provider.tsx # Theme context provider -│ │ └── theme-toggle.tsx # Dark/light mode toggle -│ └── ui/ # UI components -│ ├── button.tsx # Button component -│ └── card.tsx # Card component +│ ├── mdx/ # MDX-specific components (code blocks, API tables) +│ ├── layout/ # Layout components (header, footer) +│ ├── theme/ # Theme components (provider, toggle) +│ └── ui/ # Reusable UI components (button, card) ├── hooks/ # Custom React hooks └── lib/ # Utility functions └── utils.ts # Common utilities @@ -104,16 +108,21 @@ Shows parameter documentation (included in the same file as ApiTable): ## Navigation -The documentation uses a grouped sidebar navigation system defined in `src/app/docs/layout.tsx`: +The API reference uses a grouped sidebar navigation system defined in `src/app/docs/api/layout.tsx`: ### Documentation - Getting Started +- Configuration - API Reference +- Examples -### Asset Classes +### Quotes - Quote Endpoints (unified quotes for all assets) + +### Asset Classes + - Stock Endpoints (market cap, splits, dividends) - Financial Endpoints - ETF Endpoints @@ -123,20 +132,24 @@ The documentation uses a grouped sidebar navigation system defined in `src/app/d - Market Endpoints - Economic Endpoints +- News Endpoints ### Information - List Endpoints - Calendar Endpoints - Company Endpoints +- Screener Endpoints - Senate & House Trading -- Institutional Data -- Insider Trading +- Institutional Endpoints +- Insider Endpoints - SEC Filings ### Resources -- Examples +- Helper Utilities + +The AI tools reference under `src/app/docs/tools/` has its own navigation defined in `src/app/docs/tools/layout.tsx`. ## Adding New Documentation @@ -170,7 +183,7 @@ Regular markdown content here. ### 4. Update navigation -Add the new page to the appropriate group in the navigation in `src/app/docs/layout.tsx`. +Add the new page to the appropriate group in the navigation in `src/app/docs/api/layout.tsx` (or `src/app/docs/tools/layout.tsx` for AI tools pages). ## API Endpoint Categories @@ -188,10 +201,12 @@ The documentation covers comprehensive financial data across multiple asset clas - **Market Endpoints**: Market performance, trading hours, sector data, gainers/losers - **Economic Endpoints**: Economic indicators, calendar, treasury rates, inflation data +- **News Endpoints**: Financial news articles — general, stock, crypto, and forex ### Information -- **List Endpoints**: Stock listings, screening, and filtering capabilities +- **List Endpoints**: Stock listings, indices, and symbol lists +- **Screener Endpoints**: Stock screener with filters and available exchanges/sectors/industries/countries - **Calendar Endpoints**: Earnings calendar, economic calendar, and event scheduling - **Company Endpoints**: Company profiles, executive information, and corporate data - **Senate & House Trading**: Congressional trading data and political stock activity diff --git a/apps/examples/openai/README.md b/apps/examples/openai/README.md index 25711d1..77d40e9 100644 --- a/apps/examples/openai/README.md +++ b/apps/examples/openai/README.md @@ -11,11 +11,7 @@ This example demonstrates how to use FMP Tools with the OpenAI Agents SDK to cre ## Available Tools -This example currently includes: - -- **getCompanyProfile** - Get comprehensive company profile information - -Additional tools from the FMP Tools library can be easily added to expand functionality. +This example wires up the full FMP tool set via `fmpTools` from `fmp-ai-tools/openai`, so the agent can answer questions about quotes, financials, company profiles, market data, economic indicators, and more. To narrow the agent's scope, pass a specific subset of tools instead (see [Adding New Tools](#adding-new-tools)). ## Getting Started @@ -92,12 +88,12 @@ src/ ```typescript import { Agent, run } from '@openai/agents'; -import { getCompanyProfile } from 'fmp-ai-tools/openai'; +import { fmpTools } from 'fmp-ai-tools/openai'; const agent = new Agent({ + name: 'Stock Agent', model: 'gpt-4o-mini', - instructions: 'You are a financial assistant...', - tools: [getCompanyProfile], + tools: fmpTools, }); const result = await run(agent, userMessage.content); @@ -137,21 +133,15 @@ You can easily extend this example by: ## Adding New Tools -To add more FMP tools to the agent: +The example uses the full `fmpTools` set. To restrict the agent to a specific subset, import individual tools (or category arrays) and pass them instead: ```typescript -import { - getCompanyProfile, - // Add more tools as they become available -} from 'fmp-ai-tools/openai'; +import { getCompanyProfile, getStockQuote, financialTools } from 'fmp-ai-tools/openai'; const agent = new Agent({ + name: 'Stock Agent', model: 'gpt-4o-mini', - instructions: '...', - tools: [ - getCompanyProfile, - // Add more tools here - ], + tools: [getCompanyProfile, getStockQuote, ...financialTools], }); ``` diff --git a/packages/api/README.md b/packages/api/README.md index 66a7aa7..c879336 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -143,11 +143,13 @@ const fmp = new FMP({ - **`fmp.economic`** - Economic indicators - **`fmp.market`** - Market-wide data and performance - **`fmp.list`** - Stock lists and indices +- **`fmp.screener`** - Stock screener with filters and available exchanges/sectors/industries/countries - **`fmp.calendar`** - Earnings and economic calendar - **`fmp.senateHouse`** - Congressional trading data - **`fmp.institutional`** - Form 13F filings and institutional ownership - **`fmp.insider`** - Insider trading data - **`fmp.sec`** - SEC filings and industry classification +- **`fmp.news`** - Financial news articles (general, stock, crypto, forex; by symbol or latest) ## Usage Examples @@ -303,6 +305,39 @@ const companies = await fmp.company.searchCompany({ query: 'Apple' }); const companyProfile = await fmp.company.getCompanyProfile('AAPL'); ``` +### Stock Screener + +```typescript +// Filter stocks by multiple criteria +const results = await fmp.screener.getScreener({ + marketCapMoreThan: 10000000000, + sector: 'Technology', + isActivelyTrading: true, + limit: 50, +}); + +// Lookups for available filter values +const exchanges = await fmp.screener.getAvailableExchanges(); +const sectors = await fmp.screener.getAvailableSectors(); +const industries = await fmp.screener.getAvailableIndustries(); +const countries = await fmp.screener.getAvailableCountries(); +``` + +### News + +```typescript +// Latest news by asset class +const stockNews = await fmp.news.getStockNews({ from: '2024-01-01', to: '2024-01-31' }); +const cryptoNews = await fmp.news.getCryptoNews({ limit: 50 }); +const forexNews = await fmp.news.getForexNews({ limit: 50 }); + +// FMP-authored articles +const articles = await fmp.news.getArticles({ page: 1, limit: 20 }); + +// News for specific symbols +const aaplNews = await fmp.news.getStockNewsBySymbol({ symbols: ['AAPL', 'MSFT'] }); +``` + ## Testing This package includes comprehensive tests for all endpoints. There are two test scripts available: @@ -317,16 +352,6 @@ pnpm test - Automatically loads API key from `.env` file - Requires a valid FMP API key in your `.env` file -### CI Environment - -```bash -pnpm test:ci -``` - -- Uses Jest with explicit environment variable passing -- Requires `FMP_API_KEY` environment variable to be set -- Used by CI/CD pipelines - ### Running Specific Tests ```bash @@ -349,10 +374,7 @@ pnpm test:company # Run all endpoint tests pnpm test:endpoints -# Manual testing with real API calls -pnpm test:manual - -# Run specific endpoint test +# Manual testing against the real API pnpm test:endpoint ``` @@ -433,9 +455,6 @@ import { formatLargeNumber, formatDate, formatVolume, - formatNumber, - formatTimestamp, - formatReadableDate, } from 'fmp-node-api'; // Format financial data @@ -444,7 +463,6 @@ const formattedChange = formatPercentage(1.45); // "1.45%" const formattedMarketCap = formatLargeNumber(2500000000); // "2.50B" const formattedVolume = formatVolume(1500000); // "1.50M" const formattedDate = formatDate('2024-01-15'); // "2024-01-15" -const formattedTimestamp = formatTimestamp(1705276800); // "1/15/2024, 12:00:00 AM" ``` ## Advanced Usage @@ -501,15 +519,14 @@ pnpm test:calendar # Calendar endpoint tests only pnpm test:company # Company endpoint tests only # Manual testing with real API calls -pnpm test:manual # Test real API integration -pnpm test:endpoint # Run specific endpoint test +pnpm test:endpoint # Run a specific endpoint against the live API # Development pnpm test:watch # Watch mode for development pnpm test:coverage # Generate coverage report ``` -**Note**: Additional endpoint-specific test scripts (crypto, etf, mutual-fund) are available in the package-level scripts but not exposed at the root level. +**Note**: Additional endpoint-specific test scripts (`test:etf`, `test:mutual-fund`, `test:senate-house`, `test:institutional`, `test:insider`, `test:sec`) are also available, both here and at the repo root. ## Development @@ -551,8 +568,7 @@ pnpm dev # Watch mode for development pnpm test # Run all tests pnpm test:watch # Watch mode pnpm test:coverage # Coverage report -pnpm test:manual # Manual API testing -pnpm test:endpoint # Run specific endpoint test +pnpm test:endpoint # Run specific endpoint test against the live API pnpm test:unit # Run unit tests pnpm test:integration # Run integration tests pnpm test:endpoints # Run all endpoint tests @@ -580,52 +596,39 @@ pnpm clean # Clean build artifacts ``` src/ -├── client.ts # Base HTTP client -├── fmp.ts # Main FMP class +├── client.ts # Base HTTP client (v3/v4/stable axios instances) +├── fmp.ts # Main FMP class (wires up all endpoint classes) ├── index.ts # Main exports -├── shared.ts # Shared types and utilities -├── endpoints/ # API endpoint classes -│ ├── index.ts +├── types-only.ts # Types-only entry point (fmp-node-api/types) +├── shared.ts # Shared error types +├── endpoints/ # API endpoint classes (one per category) +│ ├── quote.ts │ ├── stock.ts │ ├── financial.ts -│ ├── quote.ts +│ ├── company.ts │ ├── etf.ts │ ├── mutual-fund.ts │ ├── economic.ts │ ├── market.ts │ ├── list.ts +│ ├── screener.ts │ ├── calendar.ts -│ ├── company.ts │ ├── senate-house.ts │ ├── institutional.ts │ ├── insider.ts -│ └── sec.ts +│ ├── sec.ts +│ └── news.ts ├── utils/ # Utility functions -│ ├── index.ts │ ├── validation.ts # Input validation │ ├── formatting.ts # Data formatting -│ └── constants.ts # API constants -├── __tests__/ # Test files -│ ├── client.test.ts -│ ├── fmp.test.ts -│ ├── integration.test.ts -│ ├── endpoints/ # Endpoint tests -│ │ ├── stock.test.ts -│ │ ├── financial.test.ts -│ │ ├── market.test.ts -│ │ ├── quote.test.ts -│ │ ├── economic.test.ts -│ │ ├── etf.test.ts -│ │ ├── mutual-fund.test.ts -│ │ ├── list.test.ts -│ │ ├── calendar.test.ts -│ │ └── company.test.ts -│ └── utils/ # Test utilities -│ ├── formatting.test.ts -│ ├── validation.test.ts -│ └── test-setup.ts -└── scripts/ # Build and utility scripts - └── test-manual.ts # Manual API testing script +│ ├── constants.ts # API constants +│ ├── helpers.ts # Shared helpers +│ ├── debug.ts # Debug logging +│ └── utils.ts # Misc utilities +└── __tests__/ # Jest tests (client, fmp, integration, endpoints/, utils/) + +scripts/ +└── test-endpoint.ts # Manual live-API endpoint testing script ``` ## Contributing diff --git a/packages/tools/README.md b/packages/tools/README.md index 4effcb8..31ed473 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -38,9 +38,7 @@ yarn add ai zod ### OpenAI Agents Compatibility -**⚠️ Important**: This package requires `@openai/agents` version `^0.1.0` or higher due to breaking changes in the API. - -If you're using an older version, you'll encounter errors like: +**⚠️ Important**: This package requires `@openai/agents` version `^0.1.0` or higher due to breaking changes in the API. Older versions are not supported and will fail when the tools are imported. ## Quick Start @@ -68,7 +66,7 @@ export async function POST(req: Request) { ### OpenAI Agents ```typescript -import { Agent } from '@openai/agents'; +import { Agent, run } from '@openai/agents'; import { fmpTools } from 'fmp-ai-tools/openai'; const agent = new Agent({ @@ -77,15 +75,10 @@ const agent = new Agent({ tools: fmpTools, }); -const result = await agent.run({ - messages: [ - { - role: 'user', - content: - 'Get the current stock quote for Apple (AAPL) and show me their latest balance sheet', - }, - ], -}); +const result = await run( + agent, + 'Get the current stock quote for Apple (AAPL) and show me their latest balance sheet', +); ``` ## Configuration @@ -148,13 +141,22 @@ Logs: result summary and formatted JSON response data. ### Company Tools - `getCompanyProfile` - Get comprehensive company profile and information +- `getCompanySharesFloat` - Get shares float and outstanding share data +- `getCompanyExecutiveCompensation` - Get executive compensation data ### Financial Tools - `getBalanceSheet` - Get balance sheet statements (annual/quarterly) - `getIncomeStatement` - Get income statements (annual/quarterly) - `getCashFlowStatement` - Get cash flow statements (annual/quarterly) +- `getKeyMetrics` - Get key financial metrics (annual/quarterly) - `getFinancialRatios` - Get financial ratios and metrics (annual/quarterly) +- `getEnterpriseValue` - Get enterprise value data +- `getIncomeGrowth` - Get income statement growth metrics +- `getBalanceSheetGrowth` - Get balance sheet growth metrics +- `getCashflowGrowth` - Get cash flow growth metrics +- `getFinancialGrowth` - Get overall financial growth metrics +- `getEarningsHistorical` - Get historical earnings data ### Stock Tools diff --git a/packages/types/README.md b/packages/types/README.md index e38fdd7..da071ad 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -23,7 +23,7 @@ pnpm add fmp-node-api ## Usage (for end users) ```typescript -import type { Quote, CompanyProfile, FinancialStatement } from 'fmp-node-api'; +import type { Quote, CompanyProfile, IncomeStatement } from 'fmp-node-api'; // Use types in your code const quote: Quote = { @@ -56,19 +56,22 @@ const quote: Quote = { - `IncomeStatement` - Income statement data - `BalanceSheet` - Balance sheet data - `CashFlowStatement` - Cash flow statement data -- `FinancialRatio` - Financial ratios -- `Earnings` - Earnings data +- `FinancialRatios` - Financial ratios +- `KeyMetrics` - Key financial metrics ### Company Types - `CompanyProfile` - Company information -- `Executive` - Executive data -- `Employee` - Employee information +- `ExecutiveCompensation` - Executive compensation data +- `SharesFloat` - Shares float and outstanding data +- `HistoricalEmployeeCount` - Historical employee counts ### Market Types - `MarketIndex` - Market index data -- `SectorPerformance` - Sector performance data +- `MarketPerformance` - Market performance data +- `MarketSectorPerformance` - Sector performance data +- `MarketHours` - Market trading hours ### And many more... @@ -79,7 +82,7 @@ This package is part of the FMP Node Wrapper monorepo. To contribute: 1. Clone the repository 2. Install dependencies: `pnpm install` 3. Build the package: `pnpm build` -4. Run tests: `pnpm test` +4. Type-check: `pnpm type-check` ## License From cf754f5c39a3db4fa72b977693615c76e8d9c769 Mon Sep 17 00:00:00 2001 From: e-roy Date: Sun, 24 May 2026 21:46:50 -0400 Subject: [PATCH 02/13] feat(api): live-API drift-check tool, schema-first types, deterministic mocked tests fmp-node-types is now schema-first: Zod schemas are canonical and TS types are derived via z.infer (zod runtime dep; ts-to-zod bootstraps generation via gen:schemas). All 16 categories converted, with ~25 corrections to match the live FMP API (string-vs-number, nullable fields, added/removed fields, MarketIndex reshaped to the /quotes/index quote shape, new IntradayPrice). Add a live-API shape-check tool (packages/api/scripts/live + src/live/validate.ts): validates all ~85 endpoint methods against the canonical schemas, classifying PASS/FAIL/SKIP/DRIFT; sequential with throttle, --max-calls budget, and --sample/--category/--dry-run flags. Run via 'pnpm test:live'. Classifier is unit-tested. Make the Jest suite fully mocked and deterministic (no network or API key): convert the 15 live endpoint integration tests to mocked unit tests (assert client path/version/params), remove integration.test.ts and utils/test-setup.ts, and drop the test:integration script. api 378 tests + tools 97 run in ~5s. Add a scheduled GitHub Action (live-check.yml) that runs the live drift check weekly (and on demand), keeping live calls off the PR path while mocked tests gate PRs. Docs: sync both READMEs and all 15 docs-site category pages to the corrected types; remove examples for non-existent methods (getFederalFundsRate, getSP500, searchCompany, etc.). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/live-check.yml | 46 + CLAUDE.md | 9 +- README.md | 10 +- apps/docs/src/app/docs/api/calendar/page.mdx | 14 +- apps/docs/src/app/docs/api/company/page.mdx | 56 +- apps/docs/src/app/docs/api/economic/page.mdx | 3 + apps/docs/src/app/docs/api/etf/page.mdx | 54 +- apps/docs/src/app/docs/api/financial/page.mdx | 470 ++++---- apps/docs/src/app/docs/api/list/page.mdx | 40 +- apps/docs/src/app/docs/api/market/page.mdx | 183 ++- apps/docs/src/app/docs/api/screener/page.mdx | 12 +- apps/docs/src/app/docs/api/sec/page.mdx | 6 +- .../src/app/docs/api/senate-house/page.mdx | 2 +- package.json | 2 +- packages/api/README.md | 91 +- packages/api/package.json | 8 +- packages/api/scripts/live/manifest.ts | 276 +++++ packages/api/scripts/live/run.ts | 205 ++++ packages/api/scripts/live/tsconfig.json | 13 + .../src/__tests__/endpoints/calendar.test.ts | 834 ++++--------- .../src/__tests__/endpoints/company.test.ts | 407 +++---- .../src/__tests__/endpoints/economic.test.ts | 169 ++- .../api/src/__tests__/endpoints/etf.test.ts | 263 ++--- .../src/__tests__/endpoints/financial.test.ts | 1048 ++++------------- .../src/__tests__/endpoints/insider.test.ts | 826 +++++-------- .../__tests__/endpoints/institutional.test.ts | 117 +- .../api/src/__tests__/endpoints/list.test.ts | 371 ++---- .../src/__tests__/endpoints/market.test.ts | 238 ++-- .../__tests__/endpoints/mutual-fund.test.ts | 62 +- .../api/src/__tests__/endpoints/news.test.ts | 705 +++++------ .../src/__tests__/endpoints/screener.test.ts | 315 ++--- .../api/src/__tests__/endpoints/sec.test.ts | 849 ++++--------- .../__tests__/endpoints/senate-house.test.ts | 469 ++------ .../api/src/__tests__/endpoints/stock.test.ts | 438 ++----- packages/api/src/__tests__/fmp.test.ts | 14 - .../api/src/__tests__/integration.test.ts | 81 -- .../api/src/__tests__/live/validate.test.ts | 96 ++ .../api/src/__tests__/utils/test-setup.ts | 59 - packages/api/src/endpoints/market.ts | 2 +- packages/api/src/endpoints/quote.ts | 4 +- packages/api/src/live/validate.ts | 150 +++ packages/types/package.json | 5 + packages/types/src/calendar.ts | 141 +-- packages/types/src/company.ts | 207 ++-- packages/types/src/economic.ts | 77 +- packages/types/src/etf.ts | 170 +-- packages/types/src/financial.ts | 1002 ++++++++-------- packages/types/src/insider.ts | 209 ++-- packages/types/src/institutional.ts | 51 +- packages/types/src/list.ts | 85 +- packages/types/src/market.ts | 122 +- packages/types/src/mutual-fund.ts | 22 +- packages/types/src/news.ts | 49 +- packages/types/src/quote.ts | 111 +- packages/types/src/screener.ts | 123 +- packages/types/src/sec.ts | 215 ++-- packages/types/src/senate-house.ts | 115 +- packages/types/src/stock.ts | 113 +- pnpm-lock.yaml | 597 +++++++++- turbo.json | 5 - 60 files changed, 5508 insertions(+), 6928 deletions(-) create mode 100644 .github/workflows/live-check.yml create mode 100644 packages/api/scripts/live/manifest.ts create mode 100644 packages/api/scripts/live/run.ts create mode 100644 packages/api/scripts/live/tsconfig.json delete mode 100644 packages/api/src/__tests__/integration.test.ts create mode 100644 packages/api/src/__tests__/live/validate.test.ts delete mode 100644 packages/api/src/__tests__/utils/test-setup.ts create mode 100644 packages/api/src/live/validate.ts diff --git a/.github/workflows/live-check.yml b/.github/workflows/live-check.yml new file mode 100644 index 0000000..97c5ef7 --- /dev/null +++ b/.github/workflows/live-check.yml @@ -0,0 +1,46 @@ +name: Live API Drift Check + +# Validates the live FMP API against the canonical Zod schemas in fmp-node-types. +# Runs on a schedule (and on demand) — NOT on every push/PR — so the deterministic +# mocked unit tests gate PRs while this job catches real API contract drift. + +on: + schedule: + - cron: '0 12 * * 1' # Mondays 12:00 UTC + workflow_dispatch: + inputs: + category: + description: 'Optional category filter (e.g. quote,stock). Blank = all.' + required: false + default: '' + +jobs: + live-check: + runs-on: ubuntu-latest + env: + FMP_API_KEY: ${{ secrets.FMP_API_KEY }} + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.2.0 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build types (schemas the runner validates against) + run: pnpm --filter fmp-node-types build + + - name: Run live shape check + run: | + pnpm --filter fmp-node-api exec tsx scripts/live/run.ts \ + --sample 25 --delay 300 --fail-on-drift \ + ${{ github.event.inputs.category && format('--category {0}', github.event.inputs.category) || '' }} diff --git a/CLAUDE.md b/CLAUDE.md index 9f8423a..6bddf1e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ pnpm lint:all # turbo lint across every package (incl. tools) pnpm format # prettier --write ``` -Per-endpoint Jest runs (filter to `fmp-node-api`), e.g. `pnpm test:quote`, `pnpm test:stock`, `pnpm test:financial`, `pnpm test:sec`, etc. Also `pnpm test:unit` (client + fmp), `pnpm test:integration`, `pnpm test:endpoints`, `pnpm test:tools`. +Per-endpoint Jest runs (filter to `fmp-node-api`), e.g. `pnpm test:quote`, `pnpm test:stock`, `pnpm test:financial`, `pnpm test:sec`, etc. Also `pnpm test:unit` (client + fmp), `pnpm test:endpoints`, `pnpm test:tools`. **All Jest tests are fully mocked and deterministic — no network or API key required.** Live API validation is a separate concern handled by `pnpm test:live` (shape-checks responses against the canonical Zod schemas; see below). Run a single test file directly: ```bash @@ -36,13 +36,16 @@ pnpm --filter fmp-node-api exec jest src/__tests__/endpoints/quote.test.ts Manual live-API smoke tests (hit the real FMP API, need `FMP_API_KEY`): ```bash -pnpm test:endpoint # packages/api/scripts/test-endpoint.ts +pnpm test:endpoint # packages/api/scripts/test-endpoint.ts (raw JSON, one endpoint) pnpm test:tool # packages/tools manual tool runner +pnpm test:live [flags] # live-API shape check vs Zod schemas; PASS/FAIL/SKIP/DRIFT (packages/api/scripts/live) ``` +`test:live` validates real responses against the canonical Zod schemas in `fmp-node-types` (schema-first; types are `z.infer`). It is sequential + throttled and supports `--category`, `--endpoint`, `--delay`, `--max-calls`, `--dry-run`, `--include-locked`, `--fail-on-drift`. Seeded for `quote`/`stock`/`financial`/`market`; classifier logic is unit-tested in `packages/api/src/__tests__/live/`. Add new cases in `scripts/live/manifest.ts`. + ### API key for tests -Integration tests and manual scripts read `FMP_API_KEY` from a root `.env` (`cp .env.example .env`). Turbo passes `FMP_API_KEY` through to test tasks (see `turbo.json` `env`). Constructing `new FMP()` with no key falls back to `process.env.FMP_API_KEY` and throws if absent or malformed (`FMPValidation.isValidApiKey`). +The live-check tool (`test:live`) and manual scripts read `FMP_API_KEY` from a root `.env` (`cp .env.example .env`); the mocked Jest suite does not need it. Turbo passes `FMP_API_KEY` through to test tasks (see `turbo.json` `env`). Constructing `new FMP()` with no key falls back to `process.env.FMP_API_KEY` and throws if absent or malformed (`FMPValidation.isValidApiKey`). ## Architecture diff --git a/README.md b/README.md index 5044a26..982117a 100644 --- a/README.md +++ b/README.md @@ -182,9 +182,9 @@ const treasury = await fmp.economic.getTreasuryRates({ to: '2024-12-31', }); -// Stock lists and indices -const sp500 = await fmp.list.getSP500(); -const nasdaq = await fmp.list.getNasdaq100(); +// Symbol lists +const stocks = await fmp.list.getStockList(); +const etfs = await fmp.list.getETFList(); // Earnings calendar const earnings = await fmp.calendar.getEarningsCalendar({ @@ -192,8 +192,8 @@ const earnings = await fmp.calendar.getEarningsCalendar({ to: '2024-12-31', }); -// Company search -const companies = await fmp.company.searchCompany({ query: 'Apple' }); +// Company profile +const companyProfile = await fmp.company.getCompanyProfile('AAPL'); ``` ### Stock Screener diff --git a/apps/docs/src/app/docs/api/calendar/page.mdx b/apps/docs/src/app/docs/api/calendar/page.mdx index bf450e6..3b81c14 100644 --- a/apps/docs/src/app/docs/api/calendar/page.mdx +++ b/apps/docs/src/app/docs/api/calendar/page.mdx @@ -77,10 +77,10 @@ Retrieve upcoming earnings announcements with estimated and actual earnings per date: '2024-01-25', symbol: 'AAPL', eps: 2.18, - estimatedEps: 2.10, + epsEstimated: 2.10, time: 'AMC', revenue: 119575000000, - estimatedRevenue: 117000000000, + revenueEstimated: 117000000000, fiscalDateEnding: '2023-12-31', updatedFromDate: '2024-01-15' }, @@ -88,10 +88,10 @@ Retrieve upcoming earnings announcements with estimated and actual earnings per date: '2024-01-26', symbol: 'MSFT', eps: 2.93, - estimatedEps: 2.78, + epsEstimated: 2.78, time: 'AMC', revenue: 62020000000, - estimatedRevenue: 61100000000, + revenueEstimated: 61100000000, fiscalDateEnding: '2023-12-31', updatedFromDate: '2024-01-16' } @@ -259,7 +259,7 @@ Retrieve economic events and indicators from various countries with estimates an actual: 216000, change: 43000, impact: 'High', - changePercent: 24.86, + changePercentage: 24.86, unit: 'K' }, { @@ -272,7 +272,7 @@ Retrieve economic events and indicators from various countries with estimates an actual: 0.3, change: 0.2, impact: 'High', - changePercent: 200.00, + changePercentage: 200.00, unit: '%' } ] @@ -452,7 +452,7 @@ Always check the success property before accessing data: if (earnings.success) { console.log('Earnings events:', earnings.data.length); earnings.data.forEach(event => { -console.log(\`\${event.symbol}: \${event.estimatedEps} EPS\`); +console.log(\`\${event.symbol}: \${event.epsEstimated} EPS\`); }); } else { console.error('Error:', earnings.error); diff --git a/apps/docs/src/app/docs/api/company/page.mdx b/apps/docs/src/app/docs/api/company/page.mdx index 4ed5aab..9fcb9c7 100644 --- a/apps/docs/src/app/docs/api/company/page.mdx +++ b/apps/docs/src/app/docs/api/company/page.mdx @@ -76,19 +76,21 @@ Retrieve comprehensive company profile information including financial metrics, data: { symbol: 'AAPL', price: 150.25, + marketCap: 2375000000000, beta: 1.28, - volAvg: 52345600, - mktCap: 2375000000000, - lastDiv: 0.24, + lastDividend: 0.24, range: '124.17-198.23', - changes: 3.15, + change: 3.15, + changePercentage: 2.14, + volume: 52345600, + averageVolume: 54321000, companyName: 'Apple Inc.', currency: 'USD', cik: '0000320193', isin: 'US0378331005', cusip: '037833100', + exchangeFullName: 'NASDAQ Global Select', exchange: 'NASDAQ', - exchangeShortName: 'NASDAQ', industry: 'Consumer Electronics', website: 'https://www.apple.com', description: 'Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide.', @@ -101,8 +103,6 @@ Retrieve comprehensive company profile information including financial metrics, city: 'Cupertino', state: 'CA', zip: '95014', - dcfDiff: 0.00, - dcf: 0.00, image: 'https://financialmodelingprep.com/image-stock/AAPL.png', ipoDate: '1980-12-12', defaultImage: false, @@ -143,18 +143,18 @@ Retrieve detailed executive compensation data including salary, bonuses, stock a cik: '0000320193', symbol: 'AAPL', companyName: 'Apple Inc.', - industryTitle: 'Consumer Electronics', - acceptedDate: '2024-01-25', filingDate: '2024-01-25', + acceptedDate: '2024-01-25', nameAndPosition: 'Timothy D. Cook, Chief Executive Officer', year: 2023, salary: 3000000, bonus: 0, - stock_award: 50000000, - incentive_plan_compensation: 0, - all_other_compensation: 0, + stockAward: 50000000, + optionAward: 0, + incentivePlanCompensation: 0, + allOtherCompensation: 0, total: 53000000, - url: 'https://www.sec.gov/Archives/edgar/data/320193/000032019324000006/aap-20231230.htm' + link: 'https://www.sec.gov/Archives/edgar/data/320193/000032019324000006/aap-20231230.htm' } ] }`} @@ -409,19 +409,21 @@ Retrieve available transcript dates and data for a company's earnings calls. {`interface CompanyProfile { symbol: string; price: number; + marketCap: number; beta: number; - volAvg: number; - mktCap: number; - lastDiv: number; + lastDividend: number; range: string; - changes: number; + change: number; + changePercentage: number; + volume: number; + averageVolume: number; companyName: string; currency: string; cik: string; isin: string; cusip: string; + exchangeFullName: string; exchange: string; - exchangeShortName: string; industry: string; website: string; description: string; @@ -434,8 +436,6 @@ Retrieve available transcript dates and data for a company's earnings calls. city: string; state: string; zip: string; - dcfDiff: number; - dcf: number; image: string; ipoDate: string; defaultImage: boolean; @@ -453,18 +453,18 @@ Retrieve available transcript dates and data for a company's earnings calls. cik: string; symbol: string; companyName: string; - industryTitle: string; - acceptedDate: string; filingDate: string; + acceptedDate: string; nameAndPosition: string; year: number; salary: number; bonus: number; - stock_award: number; - incentive_plan_compensation: number; - all_other_compensation: number; + stockAward: number; + optionAward: number; + incentivePlanCompensation: number; + allOtherCompensation: number; total: number; - url: string; + link: string; }`} @@ -489,7 +489,7 @@ Retrieve available transcript dates and data for a company's earnings calls. freeFloat: number; floatShares: string; outstandingShares: string; - source: string; + source: string | null; date: string; }`} @@ -545,7 +545,7 @@ const [profile, compensation, employeeData, sharesFloat, transcriptData] = await // Analyze company metrics if (profile.success && profile.data) { console.log('Company:', profile.data.companyName); -console.log('Market Cap:', profile.data.mktCap); +console.log('Market Cap:', profile.data.marketCap); console.log('Industry:', profile.data.industry); console.log('Employees:', profile.data.fullTimeEmployees); } diff --git a/apps/docs/src/app/docs/api/economic/page.mdx b/apps/docs/src/app/docs/api/economic/page.mdx index 6567d1c..8b10f47 100644 --- a/apps/docs/src/app/docs/api/economic/page.mdx +++ b/apps/docs/src/app/docs/api/economic/page.mdx @@ -116,10 +116,12 @@ Retrieve economic indicators with optional filtering by indicator type. success: true, data: [ { + name: 'GDP', date: '2023-12-31', value: 27.36 }, { + name: 'GDP', date: '2023-11-30', value: 27.35 } @@ -181,6 +183,7 @@ The following economic indicators are available: {`interface EconomicIndicator { + name: string; date: string; value: number; }`} diff --git a/apps/docs/src/app/docs/api/etf/page.mdx b/apps/docs/src/app/docs/api/etf/page.mdx index 2282f04..31ebbff 100644 --- a/apps/docs/src/app/docs/api/etf/page.mdx +++ b/apps/docs/src/app/docs/api/etf/page.mdx @@ -73,26 +73,28 @@ Retrieve ETF profile and basic information including expense ratio, AUM, and fun data: { symbol: 'SPY', name: 'SPDR S&P 500 ETF Trust', - assetClass: 'Equity', - aum: 500000000000, - avgVolume: 75000000, - cusip: '78462F103', description: 'The SPDR S&P 500 ETF Trust tracks the S&P 500 Index', + isin: 'US78462F1030', + assetClass: 'Equity', + securityCusip: '78462F103', domicile: 'United States', + website: 'https://www.ssga.com', etfCompany: 'State Street Global Advisors', expenseRatio: 0.0945, + assetsUnderManagement: 500000000000, + avgVolume: 75000000, inceptionDate: '1993-01-29', - isin: 'US78462F1030', nav: 450.25, navCurrency: 'USD', + holdingsCount: 500, + updatedAt: '2024-01-15T16:30:00.000Z', + isActivelyTrading: true, sectorsList: [ { - exposure: 'Technology', - industry: 'Software' + exposure: 29.85, + industry: 'Technology' } - ], - website: 'https://www.ssga.com', - holdingsCount: 500 + ] } }`} @@ -258,16 +260,19 @@ Retrieve sector breakdown of ETF holdings. success: true, data: [ { + symbol: 'SPY', sector: 'Technology', - weightPercentage: '32.62%' + weightPercentage: 32.62 }, { + symbol: 'SPY', sector: 'Financial Services', - weightPercentage: '12.48%' + weightPercentage: 12.48 }, { + symbol: 'SPY', sector: 'Healthcare', - weightPercentage: '12.1%' + weightPercentage: 12.1 } ] }`} @@ -364,25 +369,27 @@ Retrieve which ETFs hold a specific stock. {`interface ETFProfile { symbol: string; - assetClass: string; - aum: number; - avgVolume: number; - cusip: string; + name: string; description: string; + isin: string; + assetClass: string; + securityCusip: string; domicile: string; + website: string; etfCompany: string; expenseRatio: number; + assetsUnderManagement: number; + avgVolume: number; inceptionDate: string; - isin: string; - name: string; nav: number; navCurrency: string; + holdingsCount: number; + updatedAt: string; + isActivelyTrading: boolean; sectorsList: { - exposure: string; + exposure: number; industry: string; }[]; - website: string; - holdingsCount: number; }`} @@ -435,8 +442,9 @@ Retrieve which ETFs hold a specific stock. {`interface ETFWeighting { + symbol: string; sector: string; - weightPercentage: string; + weightPercentage: number; }`} diff --git a/apps/docs/src/app/docs/api/financial/page.mdx b/apps/docs/src/app/docs/api/financial/page.mdx index ad76ea2..76bec64 100644 --- a/apps/docs/src/app/docs/api/financial/page.mdx +++ b/apps/docs/src/app/docs/api/financial/page.mdx @@ -59,8 +59,8 @@ and fundamental data for companies. }, { method: 'GET', - path: '/stable/historical/earning_calendar?symbol={symbol}', - description: 'Get historical earnings calendar', + path: '/stable/earnings?symbol={symbol}', + description: 'Get historical earnings data', }, { method: 'GET', @@ -117,14 +117,13 @@ metrics. symbol: 'AAPL', reportedCurrency: 'USD', cik: '0000320193', - fillingDate: '2023-10-27', + filingDate: '2023-10-27', acceptedDate: '2023-10-27T16:30:00.000+00:00', - calendarYear: '2023', + fiscalYear: '2023', period: 'FY', revenue: 383285000000, costOfRevenue: 214137000000, grossProfit: 169148000000, - grossProfitRatio: 0.4414, researchAndDevelopmentExpenses: 29915000000, generalAndAdministrativeExpenses: 0, sellingAndMarketingExpenses: 0, @@ -132,24 +131,27 @@ metrics. otherExpenses: 0, operatingExpenses: 54724000000, costAndExpenses: 268861000000, + netInterestIncome: -183000000, + interestIncome: 3750000000, interestExpense: 3933000000, depreciationAndAmortization: 11104000000, ebitda: 130000000000, - ebitdaratio: 0.3392, + ebit: 114301000000, + nonOperatingIncomeExcludingInterest: 0, operatingIncome: 114301000000, - operatingIncomeRatio: 0.2983, totalOtherIncomeExpensesNet: -1000000000, incomeBeforeTax: 113301000000, - incomeBeforeTaxRatio: 0.2957, incomeTaxExpense: 16741000000, + netIncomeFromContinuingOperations: 96995000000, + netIncomeFromDiscontinuedOperations: 0, + otherAdjustmentsToNetIncome: 0, netIncome: 96995000000, - netIncomeRatio: 0.2531, + netIncomeDeductions: 0, + bottomLineNetIncome: 96995000000, eps: 6.16, epsDiluted: 6.13, - ebit: 114301000000, - ebitRatio: 0.2983, - link: 'https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/aapl-20230930.htm', - finalLink: 'https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/aapl-20230930.htm' + weightedAverageShsOut: 15744231000, + weightedAverageShsOutDil: 15812547000 } ] }`} @@ -201,15 +203,19 @@ Retrieve balance sheet data including assets, liabilities, and equity. symbol: 'AAPL', reportedCurrency: 'USD', cik: '0000320193', - fillingDate: '2023-10-27', + filingDate: '2023-10-27', acceptedDate: '2023-10-27T16:30:00.000+00:00', - calendarYear: '2023', + fiscalYear: '2023', period: 'FY', cashAndCashEquivalents: 29965000000, shortTermInvestments: 31592000000, cashAndShortTermInvestments: 61557000000, netReceivables: 29508000000, + accountsReceivables: 29508000000, + otherReceivables: 0, inventory: 6331000000, + prepaids: 0, + otherCurrentAssets: 14695000000, totalCurrentAssets: 143713000000, propertyPlantEquipmentNet: 43663000000, goodwill: 0, @@ -217,41 +223,43 @@ Retrieve balance sheet data including assets, liabilities, and equity. goodwillAndIntangibleAssets: 0, longTermInvestments: 100544000000, taxAssets: 0, - otherNonCurrentAssets: 0, - totalNonCurrentAssets: 144226000000, + otherNonCurrentAssets: 64758000000, + totalNonCurrentAssets: 209017000000, otherAssets: 0, - totalAssets: 352755000000, + totalAssets: 352583000000, + totalPayables: 62611000000, accountPayables: 62611000000, - shortTermDebt: 9598000000, + otherPayables: 0, + accruedExpenses: 0, + shortTermDebt: 15807000000, + capitalLeaseObligationsCurrent: 0, taxPayables: 0, - deferredRevenue: 8091000000, - otherCurrentLiabilities: 0, + deferredRevenue: 8061000000, + otherCurrentLiabilities: 58829000000, totalCurrentLiabilities: 145308000000, - longTermDebt: 95057000000, + longTermDebt: 95281000000, deferredRevenueNonCurrent: 0, deferredTaxLiabilitiesNonCurrent: 0, - otherNonCurrentLiabilities: 0, - totalNonCurrentLiabilities: 95057000000, + capitalLeaseObligationsNonCurrent: 0, + otherNonCurrentLiabilities: 49848000000, + totalNonCurrentLiabilities: 145129000000, otherLiabilities: 0, capitalLeaseObligations: 0, - totalLiabilities: 240365000000, + totalLiabilities: 290437000000, + treasuryStock: 0, preferredStock: 0, - commonStock: 64849000000, - retainedEarnings: 21400000000, - accumulatedOtherComprehensiveIncomeLoss: -1100000000, - othertotalStockholdersEquity: 0, - totalStockholdersEquity: 112390000000, - totalEquityGrossMinorityInterest: 0, + commonStock: 73812000000, + retainedEarnings: -214000000, + additionalPaidInCapital: 0, + accumulatedOtherComprehensiveIncomeLoss: -11452000000, + otherTotalStockholdersEquity: 0, + totalStockholdersEquity: 62146000000, + totalEquity: 62146000000, minorityInterest: 0, - totalEquity: 112390000000, - totalLiabilitiesAndStockholdersEquity: 352755000000, - minorityInterest: 0, - totalLiabilitiesAndTotalEquity: 352755000000, - totalInvestments: 100544000000, - totalDebt: 104655000000, - netDebt: 74698000000, - link: 'https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/aapl-20230930.htm', - finalLink: 'https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/aapl-20230930.htm' + totalLiabilitiesAndTotalEquity: 352583000000, + totalInvestments: 132136000000, + totalDebt: 111088000000, + netDebt: 81123000000 } ] }`} @@ -304,42 +312,49 @@ financing activities. symbol: 'AAPL', reportedCurrency: 'USD', cik: '0000320193', - fillingDate: '2023-10-27', + filingDate: '2023-10-27', acceptedDate: '2023-10-27T16:30:00.000+00:00', - calendarYear: '2023', + fiscalYear: '2023', period: 'FY', netIncome: 96995000000, depreciationAndAmortization: 11104000000, deferredIncomeTax: 0, - stockBasedCompensation: 0, - changeInWorkingCapital: 0, - accountsReceivables: 0, - inventory: 0, - accountsPayables: 0, - otherWorkingCapital: 0, - otherNonCashItems: 0, + stockBasedCompensation: 10833000000, + changeInWorkingCapital: -6577000000, + accountsReceivables: -1688000000, + inventory: -1618000000, + accountsPayables: -1889000000, + otherWorkingCapital: -1382000000, + otherNonCashItems: -2227000000, netCashProvidedByOperatingActivities: 110543000000, - investmentsInPropertyPlantAndEquipment: -10955900000, + investmentsInPropertyPlantAndEquipment: -10959000000, acquisitionsNet: 0, - purchasesOfInvestments: 0, - salesMaturitiesOfInvestments: 0, - otherInvestingActivites: 0, - netCashUsedForInvestingActivites: -10955900000, - debtRepayment: -9500000000, - commonStockIssued: 0, + purchasesOfInvestments: -29513000000, + salesMaturitiesOfInvestments: 39686000000, + otherInvestingActivities: 1337000000, + netCashProvidedByInvestingActivities: 3705000000, + netDebtIssuance: -9901000000, + longTermNetDebtIssuance: -11151000000, + shortTermNetDebtIssuance: 1250000000, + netStockIssuance: -77550000000, + netCommonStockIssuance: -77550000000, + commonStockIssuance: 0, commonStockRepurchased: -77550000000, - dividendsPaid: -15000000000, - otherFinancingActivites: 0, - netCashUsedProvidedByFinancingActivities: -99550000000, + netPreferredStockIssuance: 0, + netDividendsPaid: -15025000000, + commonDividendsPaid: -15025000000, + preferredDividendsPaid: 0, + otherFinancingActivities: -1606000000, + netCashProvidedByFinancingActivities: -108488000000, effectOfForexChangesOnCash: 0, - netChangeInCash: 0, - cashAtEndOfPeriod: 29965000000, - cashAtBeginningOfPeriod: 29965000000, + netChangeInCash: 5760000000, + cashAtEndOfPeriod: 30737000000, + cashAtBeginningOfPeriod: 24977000000, operatingCashFlow: 110543000000, - capitalExpenditure: -10955900000, - freeCashFlow: 99587000000, - link: 'https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/aapl-20230930.htm', - finalLink: 'https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/aapl-20230930.htm' + capitalExpenditure: -10959000000, + freeCashFlow: 99584000000, + incomeTaxesPaid: 18679000000, + interestPaid: 3803000000 } ] }`} @@ -387,63 +402,70 @@ Retrieve key financial ratios and metrics for analysis. success: true, data: [ { - date: '2023-09-30', symbol: 'AAPL', + date: '2023-09-30', + fiscalYear: '2023', period: 'FY', - currentRatio: 0.99, - quickRatio: 0.95, - cashRatio: 0.42, - daysOfSalesOutstanding: 22.11, - daysOfInventoryOutstanding: 6.02, - operatingCycle: 28.13, - daysOfPayablesOutstanding: 106.67, - cashConversionCycle: -78.54, - grossProfitMargin: 0.44, - operatingProfitMargin: 0.30, - pretaxProfitMargin: 0.30, - netProfitMargin: 0.25, - effectiveTaxRate: 0.15, - returnOnAssets: 0.27, - returnOnEquity: 0.86, - returnOnCapitalEmployed: 0.32, - netIncomePerEBT: 0.85, - ebtPerEbit: 0.99, - ebitPerRevenue: 0.30, - debtRatio: 0.68, - debtEquityRatio: 2.14, - longTermDebtToCapitalization: 0.46, - totalDebtToCapitalization: 0.48, - interestCoverage: 29.07, - cashFlowToDebtRatio: 1.06, - companyEquityMultiplier: 3.14, + reportedCurrency: 'USD', + grossProfitMargin: 0.4413, + ebitMargin: 0.2982, + ebitdaMargin: 0.3392, + operatingProfitMargin: 0.2982, + pretaxProfitMargin: 0.2957, + continuousOperationsProfitMargin: 0.2531, + netProfitMargin: 0.2531, + bottomLineProfitMargin: 0.2531, receivablesTurnover: 16.51, payablesTurnover: 3.42, inventoryTurnover: 60.67, fixedAssetTurnover: 8.78, assetTurnover: 1.09, - operatingCashFlowPerShare: 7.00, - freeCashFlowPerShare: 6.31, - cashPerShare: 1.90, - payoutRatio: 0.15, - operatingCashFlowSalesRatio: 0.29, + currentRatio: 0.99, + quickRatio: 0.84, + solvencyRatio: 0.34, + cashRatio: 0.21, + priceToEarningsRatio: 31.44, + priceToEarningsGrowthRatio: 2.62, + forwardPriceToEarningsGrowthRatio: 2.45, + priceToBookRatio: 35.73, + priceToSalesRatio: 8.95, + priceToFreeCashFlowRatio: 23.82, + priceToOperatingCashFlowRatio: 21.46, + debtToAssetsRatio: 0.31, + debtToEquityRatio: 1.79, + debtToCapitalRatio: 0.64, + longTermDebtToCapitalRatio: 0.60, + financialLeverageRatio: 5.67, + workingCapitalTurnoverRatio: -240.13, + operatingCashFlowRatio: 0.76, + operatingCashFlowSalesRatio: 0.28, freeCashFlowOperatingCashFlowRatio: 0.90, - cashFlowCoverageRatios: 1.06, - shortTermCoverageRatios: 1.06, + debtServiceCoverageRatio: 5.12, + interestCoverageRatio: 29.07, + shortTermOperatingCashFlowCoverageRatio: 6.99, + operatingCashFlowCoverageRatio: 0.99, capitalExpenditureCoverageRatio: 10.09, - dividendPaidAndCapexCoverageRatio: 8.77, + dividendPaidAndCapexCoverageRatio: 4.25, dividendPayoutRatio: 0.15, - priceBookValueRatio: 35.73, - priceToBookRatio: 35.73, - priceToSalesRatio: 8.95, - priceEarningsRatio: 31.44, - priceToFreeCashFlowsRatio: 23.82, - priceToOperatingCashFlowsRatio: 21.46, - priceCashFlowRatio: 21.46, - priceEarningsToGrowthRatio: 2.62, - priceSalesRatio: 8.95, - dividendYield: 0.48, - enterpriseValueMultiple: 26.89, - priceFairValue: 35.73 + dividendYield: 0.0048, + dividendYieldPercentage: 0.48, + dividendPerShare: 0.95, + revenuePerShare: 24.34, + netIncomePerShare: 6.16, + interestDebtPerShare: 7.05, + cashPerShare: 3.90, + bookValuePerShare: 3.94, + tangibleBookValuePerShare: 3.94, + shareholdersEquityPerShare: 3.94, + operatingCashFlowPerShare: 7.02, + capexPerShare: 0.69, + freeCashFlowPerShare: 6.32, + netIncomePerEBT: 0.85, + ebtPerEbit: 0.99, + priceToFairValue: 35.73, + debtToMarketCap: 0.04, + effectiveTaxRate: 0.147, + enterpriseValueMultiple: 26.89 } ] }`} @@ -491,66 +513,53 @@ Retrieve key financial metrics including valuation ratios, profitability metrics success: true, data: [ { - date: '2023-09-30', symbol: 'AAPL', + date: '2023-09-30', + fiscalYear: '2023', period: 'FY', - revenuePerShare: 24.16, - netIncomePerShare: 6.16, - operatingCashFlowPerShare: 7.00, - freeCashFlowPerShare: 6.31, - cashPerShare: 1.90, - bookValuePerShare: 3.58, - tangibleBookValuePerShare: 3.58, - shareholdersEquityPerShare: 3.58, - interestDebtPerShare: 6.65, + reportedCurrency: 'USD', marketCap: 3000000000000, enterpriseValue: 3100000000000, - peRatio: 31.44, - priceToSalesRatio: 8.95, - pocfratio: 21.46, - pfcfRatio: 23.82, - pbRatio: 35.73, - ptbRatio: 35.73, - evToSales: 8.95, - enterpriseValueOverEBITDA: 26.89, - evToOperatingCashFlow: 21.46, - evToFreeCashFlow: 23.82, - earningsYield: 0.032, - freeCashFlowYield: 0.042, - debtToEquity: 2.14, - debtToAssets: 0.68, - netDebtToEBITDA: 0.57, + evToSales: 8.09, + evToOperatingCashFlow: 28.04, + evToFreeCashFlow: 31.13, + evToEBITDA: 23.85, + netDebtToEBITDA: 0.62, currentRatio: 0.99, - interestCoverage: 29.07, incomeQuality: 1.14, - dividendYield: 0.0048, - payoutRatio: 0.15, + grahamNumber: 23.40, + grahamNetNet: -12.28, + taxBurden: 0.85, + interestBurden: 0.99, + workingCapital: -1742000000, + investedCapital: 173427000000, + returnOnAssets: 0.2751, + operatingReturnOnAssets: 0.3242, + returnOnTangibleAssets: 0.2751, + returnOnEquity: 1.5607, + returnOnInvestedCapital: 0.5592, + returnOnCapitalEmployed: 0.5286, + earningsYield: 0.0318, + freeCashFlowYield: 0.0322, + capexToOperatingCashFlow: 0.0991, + capexToDepreciation: 0.9869, + capexToRevenue: 0.0286, salesGeneralAndAdministrativeToRevenue: 0.065, - researchAndDevelopmentToRevenue: 0.078, + researchAndDevelopementToRevenue: 0.078, + stockBasedCompensationToRevenue: 0.0283, intangiblesToTotalAssets: 0, - capexToOperatingCashFlow: 0.099, - capexToRevenue: 0.029, - capexToDepreciation: 0.99, - stockBasedCompensationToRevenue: 0.065, - grahamNumber: 15.23, - roic: 0.32, - returnOnTangibleAssets: 0.27, - grahamNetNet: 2.68, - workingCapital: -1600000000, - tangibleAssetValue: 352755000000, - netCurrentAssetValue: -96695000000, - investedCapital: 245000000000, - averageReceivables: 29508000000, - averagePayables: 62611000000, - averageInventory: 6331000000, - daysSalesOutstanding: 22.11, - daysPayablesOutstanding: 106.67, - daysOfInventoryOnHand: 6.02, - receivablesTurnover: 16.51, - payablesTurnover: 3.42, - inventoryTurnover: 60.67, - roe: 0.86, - capexPerShare: 0.69 + averageReceivables: 30727000000, + averagePayables: 60519000000, + averageInventory: 6909000000, + daysOfSalesOutstanding: 29.27, + daysOfPayablesOutstanding: 103.16, + daysOfInventoryOutstanding: 11.77, + operatingCycle: 41.04, + cashConversionCycle: -62.12, + freeCashFlowToEquity: 30269000000, + freeCashFlowToFirm: 102685000000, + tangibleAssetValue: 62146000000, + netCurrentAssetValue: -148129000000 } ] }`} @@ -653,40 +662,48 @@ Retrieve cash flow growth metrics showing period-over-period changes in cash flo success: true, data: [ { - date: '2023-09-30', symbol: 'AAPL', - calendarYear: '2023', + date: '2023-09-30', + fiscalYear: '2023', period: 'FY', + reportedCurrency: 'USD', growthNetIncome: 0.028, growthDepreciationAndAmortization: 0.089, growthDeferredIncomeTax: 0, - growthStockBasedCompensation: 0, - growthChangeInWorkingCapital: 0, - growthAccountsReceivables: 0, - growthInventory: 0, - growthAccountsPayables: 0, - growthOtherWorkingCapital: 0, - growthOtherNonCashItems: 0, + growthStockBasedCompensation: 0.115, + growthChangeInWorkingCapital: -0.42, + growthAccountsReceivables: 0.13, + growthInventory: -0.05, + growthAccountsPayables: -0.11, + growthOtherWorkingCapital: 0.02, + growthOtherNonCashItems: 0.31, growthNetCashProvidedByOperatingActivites: 0.013, growthInvestmentsInPropertyPlantAndEquipment: -0.089, growthAcquisitionsNet: 0, - growthPurchasesOfInvestments: 0, - growthSalesMaturitiesOfInvestments: 0, - growthOtherInvestingActivites: 0, + growthPurchasesOfInvestments: -0.12, + growthSalesMaturitiesOfInvestments: 0.08, + growthOtherInvestingActivites: 0.04, growthNetCashUsedForInvestingActivites: -0.089, growthDebtRepayment: -0.095, growthCommonStockIssued: 0, growthCommonStockRepurchased: -0.089, growthDividendsPaid: 0.071, - growthOtherFinancingActivites: 0, + growthOtherFinancingActivites: 0.03, growthNetCashUsedProvidedByFinancingActivities: -0.089, growthEffectOfForexChangesOnCash: 0, - growthNetChangeInCash: 0, - growthCashAtEndOfPeriod: 0, - growthCashAtBeginningOfPeriod: 0, + growthNetChangeInCash: 0.21, + growthCashAtEndOfPeriod: 0.23, + growthCashAtBeginningOfPeriod: -0.18, growthOperatingCashFlow: 0.013, growthCapitalExpenditure: -0.089, - growthFreeCashFlow: 0.028 + growthFreeCashFlow: 0.028, + growthNetDebtIssuance: -0.07, + growthLongTermNetDebtIssuance: -0.09, + growthShortTermNetDebtIssuance: 0.12, + growthNetStockIssuance: -0.089, + growthPreferredDividendsPaid: 0, + growthIncomeTaxesPaid: 0.05, + growthInterestPaid: 0.06 } ] }`} @@ -734,10 +751,11 @@ Retrieve income statement growth metrics showing period-over-period changes in r success: true, data: [ { - date: '2023-09-30', symbol: 'AAPL', - calendarYear: '2023', + date: '2023-09-30', + fiscalYear: '2023', period: 'FY', + reportedCurrency: 'USD', growthRevenue: -0.028, growthCostOfRevenue: -0.032, growthGrossProfit: -0.023, @@ -748,22 +766,25 @@ Retrieve income statement growth metrics showing period-over-period changes in r growthOtherExpenses: 0, growthOperatingExpenses: 0.134, growthCostAndExpenses: -0.028, + growthInterestIncome: 0.092, growthInterestExpense: 0.089, growthDepreciationAndAmortization: 0.089, growthEBITDA: -0.023, - growthEBITDARatio: 0.005, growthOperatingIncome: -0.023, - growthOperatingIncomeRatio: 0.005, - growthTotalOtherIncomeExpensesNet: 0, growthIncomeBeforeTax: -0.023, - growthIncomeBeforeTaxRatio: 0.005, growthIncomeTaxExpense: -0.023, growthNetIncome: -0.023, - growthNetIncomeRatio: 0.005, growthEPS: -0.023, growthEPSDiluted: -0.023, growthWeightedAverageShsOut: 0, - growthWeightedAverageShsOutDil: 0 + growthWeightedAverageShsOutDil: 0, + growthEBIT: -0.023, + growthNonOperatingIncomeExcludingInterest: 0, + growthNetInterestIncome: 0.041, + growthTotalOtherIncomeExpensesNet: 0, + growthNetIncomeFromContinuingOperations: -0.023, + growthOtherAdjustmentsToNetIncome: 0, + growthNetIncomeDeductions: 0 } ] }`} @@ -811,10 +832,11 @@ Retrieve balance sheet growth metrics showing period-over-period changes in asse success: true, data: [ { - date: '2023-09-30', symbol: 'AAPL', - calendarYear: '2023', + date: '2023-09-30', + fiscalYear: '2023', period: 'FY', + reportedCurrency: 'USD', growthCashAndCashEquivalents: -0.089, growthShortTermInvestments: -0.089, growthCashAndShortTermInvestments: -0.089, @@ -840,20 +862,32 @@ Retrieve balance sheet growth metrics showing period-over-period changes in asse growthTotalCurrentLiabilities: -0.089, growthLongTermDebt: -0.089, growthDeferredRevenueNonCurrent: 0, - growthDeferrredTaxLiabilitiesNonCurrent: 0, + growthDeferredTaxLiabilitiesNonCurrent: 0, growthOtherNonCurrentLiabilities: 0, growthTotalNonCurrentLiabilities: -0.089, growthOtherLiabilities: 0, growthTotalLiabilities: -0.089, + growthPreferredStock: 0, growthCommonStock: 0, growthRetainedEarnings: 0.089, growthAccumulatedOtherComprehensiveIncomeLoss: 0, growthOthertotalStockholdersEquity: 0, growthTotalStockholdersEquity: 0.089, + growthMinorityInterest: 0, + growthTotalEquity: 0.089, growthTotalLiabilitiesAndStockholdersEquity: 0, growthTotalInvestments: 0.089, growthTotalDebt: -0.089, - growthNetDebt: -0.089 + growthNetDebt: -0.089, + growthAccountsReceivables: -0.089, + growthOtherReceivables: 0, + growthPrepaids: 0, + growthTotalPayables: -0.089, + growthOtherPayables: 0, + growthAccruedExpenses: 0, + growthCapitalLeaseObligationsCurrent: 0, + growthAdditionalPaidInCapital: 0, + growthTreasuryStock: 0 } ] }`} @@ -901,10 +935,11 @@ Retrieve comprehensive financial growth metrics including revenue, earnings, and success: true, data: [ { - date: '2023-09-30', symbol: 'AAPL', - calendarYear: '2023', + date: '2023-09-30', + fiscalYear: '2023', period: 'FY', + reportedCurrency: 'USD', revenueGrowth: -0.028, grossProfitGrowth: -0.023, ebitgrowth: -0.023, @@ -914,8 +949,15 @@ Retrieve comprehensive financial growth metrics including revenue, earnings, and epsdilutedGrowth: -0.023, weightedAverageSharesGrowth: 0, weightedAverageSharesDilutedGrowth: 0, - dividendsperShareGrowth: 0.071, + dividendsPerShareGrowth: 0.071, operatingCashFlowGrowth: 0.013, + receivablesGrowth: -0.089, + inventoryGrowth: -0.089, + assetGrowth: 0, + bookValueperShareGrowth: 0.089, + debtGrowth: -0.089, + rdexpenseGrowth: 0.134, + sgaexpensesGrowth: 0.134, freeCashFlowGrowth: 0.028, tenYRevenueGrowthPerShare: 0.089, fiveYRevenueGrowthPerShare: 0.089, @@ -932,13 +974,11 @@ Retrieve comprehensive financial growth metrics including revenue, earnings, and tenYDividendperShareGrowthPerShare: 0.089, fiveYDividendperShareGrowthPerShare: 0.089, threeYDividendperShareGrowthPerShare: 0.089, - receivablesGrowth: -0.089, - inventoryGrowth: -0.089, - assetGrowth: 0, - bookValueperShareGrowth: 0.089, - debtGrowth: -0.089, - rdexpenseGrowth: 0.134, - sgaexpensesGrowth: 0.134 + ebitdaGrowth: -0.023, + growthCapitalExpenditure: -0.089, + tenYBottomLineNetIncomeGrowthPerShare: 0.089, + fiveYBottomLineNetIncomeGrowthPerShare: 0.089, + threeYBottomLineNetIncomeGrowthPerShare: 0.089 } ] }`} @@ -981,24 +1021,20 @@ Retrieve historical and upcoming earnings announcements for a specific company, { date: '2024-01-25', symbol: 'AAPL', - eps: 2.18, + epsActual: 2.18, epsEstimated: 2.10, - time: 'AMC', - revenue: 119575000000, + revenueActual: 119575000000, revenueEstimated: 117910000000, - updatedFromDate: '2024-01-25', - fiscalDateEnding: '2023-12-31' + lastUpdated: '2024-01-26' }, { - date: '2023-10-26', + date: '2024-05-02', symbol: 'AAPL', - eps: 1.46, - epsEstimated: 1.39, - time: 'AMC', - revenue: 89498000000, - revenueEstimated: 89280000000, - updatedFromDate: '2023-10-26', - fiscalDateEnding: '2023-09-30' + epsActual: null, + epsEstimated: 1.50, + revenueActual: null, + revenueEstimated: 90500000000, + lastUpdated: '2024-01-26' } ] }`} diff --git a/apps/docs/src/app/docs/api/list/page.mdx b/apps/docs/src/app/docs/api/list/page.mdx index ddec0ff..c1c8bc4 100644 --- a/apps/docs/src/app/docs/api/list/page.mdx +++ b/apps/docs/src/app/docs/api/list/page.mdx @@ -48,31 +48,35 @@ Retrieve a comprehensive list of all available stocks with their basic informati data: [ { symbol: 'AAPL', - name: 'Apple Inc.', exchange: 'NASDAQ', exchangeShortName: 'NASDAQ', - price: '150.25' + price: 150.25, + name: 'Apple Inc.', + type: 'stock' }, { symbol: 'MSFT', - name: 'Microsoft Corporation', exchange: 'NASDAQ', exchangeShortName: 'NASDAQ', - price: '320.50' + price: 320.50, + name: 'Microsoft Corporation', + type: 'stock' }, { symbol: 'GOOGL', - name: 'Alphabet Inc.', exchange: 'NASDAQ', exchangeShortName: 'NASDAQ', - price: '2750.00' + price: 2750.00, + name: 'Alphabet Inc.', + type: 'stock' }, { symbol: 'TSLA', - name: 'Tesla, Inc.', exchange: 'NASDAQ', exchangeShortName: 'NASDAQ', - price: '850.75' + price: 850.75, + name: 'Tesla, Inc.', + type: 'stock' } ] }`} @@ -92,31 +96,35 @@ Retrieve a list of all available Exchange-Traded Funds (ETFs). data: [ { symbol: 'SPY', - name: 'SPDR S&P 500 ETF Trust', exchange: 'NYSE ARCA', exchangeShortName: 'ARCA', - price: 450.25 + price: 450.25, + name: 'SPDR S&P 500 ETF Trust', + type: 'etf' }, { symbol: 'QQQ', - name: 'Invesco QQQ Trust', exchange: 'NASDAQ', exchangeShortName: 'NASDAQ', - price: 380.50 + price: 380.50, + name: 'Invesco QQQ Trust', + type: 'etf' }, { symbol: 'IWM', - name: 'iShares Russell 2000 ETF', exchange: 'NYSE ARCA', exchangeShortName: 'ARCA', - price: 185.75 + price: 185.75, + name: 'iShares Russell 2000 ETF', + type: 'etf' }, { symbol: 'VTI', - name: 'Vanguard Total Stock Market ETF', exchange: 'NYSE ARCA', exchangeShortName: 'ARCA', - price: 225.00 + price: 225.00, + name: 'Vanguard Total Stock Market ETF', + type: 'etf' } ] }`} diff --git a/apps/docs/src/app/docs/api/market/page.mdx b/apps/docs/src/app/docs/api/market/page.mdx index c4e4c2e..e5137f1 100644 --- a/apps/docs/src/app/docs/api/market/page.mdx +++ b/apps/docs/src/app/docs/api/market/page.mdx @@ -61,23 +61,27 @@ Retrieve overall market performance data including major indices. { symbol: '^DJI', name: 'Dow Jones Industrial Average', - change: 125.45, price: 37592.98, - changesPercentage: 0.34 - }, - { - symbol: '^GSPC', - name: 'S&P 500', - change: 18.76, - price: 4783.83, - changesPercentage: 0.39 - }, - { - symbol: '^IXIC', - name: 'NASDAQ Composite', - change: 78.23, - price: 15011.35, - changesPercentage: 0.52 + changesPercentage: 0.34, + change: 125.45, + dayLow: 37450.75, + dayHigh: 37650.25, + yearHigh: 37850.00, + yearLow: 31429.82, + marketCap: null, + priceAvg50: 37250.45, + priceAvg200: 36890.12, + exchange: 'INDEX', + volume: 0, + avgVolume: 0, + open: 37500.00, + previousClose: 37467.53, + eps: null, + pe: null, + earningsAnnouncement: null, + sharesOutstanding: null, + timestamp: 1705352400, + type: 'index' } ] }`} @@ -140,23 +144,7 @@ Retrieve the top gaining stocks for the current trading day. change: 12.45, price: 248.50, changesPercentage: 5.28, - dayLow: 235.20, - dayHigh: 250.10, - yearHigh: 299.29, - yearLow: 138.80, - marketCap: 789000000000, - priceAvg50: 245.67, - priceAvg200: 242.89, - volume: 45678900, - avgVolume: 52345600, - exchange: 'NASDAQ', - open: 236.50, - previousClose: 236.05, - eps: 3.12, - pe: 79.65, - earningsAnnouncement: '2024-01-24T21:30:00.000+00:00', - sharesOutstanding: 3180000000, - timestamp: 1703123456 + exchange: 'NASDAQ' } ] }`} @@ -180,23 +168,7 @@ Retrieve the top losing stocks for the current trading day. change: -8.75, price: 485.25, changesPercentage: -1.77, - dayLow: 480.10, - dayHigh: 495.30, - yearHigh: 639.00, - yearLow: 285.33, - marketCap: 215000000000, - priceAvg50: 492.45, - priceAvg200: 478.12, - volume: 23456700, - avgVolume: 25678900, - exchange: 'NASDAQ', - open: 494.00, - previousClose: 494.00, - eps: 12.03, - pe: 40.34, - earningsAnnouncement: '2024-01-23T21:30:00.000+00:00', - sharesOutstanding: 443000000, - timestamp: 1703123456 + exchange: 'NASDAQ' } ] }`} @@ -220,23 +192,7 @@ Retrieve the most actively traded stocks by volume. change: 2.15, price: 150.25, changesPercentage: 1.45, - dayLow: 147.50, - dayHigh: 151.75, - yearHigh: 198.23, - yearLow: 124.17, - marketCap: 2375000000000, - priceAvg50: 145.67, - priceAvg200: 142.89, - volume: 78901200, - avgVolume: 52345600, - exchange: 'NASDAQ', - open: 148.50, - previousClose: 148.10, - eps: 6.16, - pe: 24.39, - earningsAnnouncement: '2024-01-25T21:30:00.000+00:00', - sharesOutstanding: 15800000000, - timestamp: 1703123456 + exchange: 'NASDAQ' } ] }`} @@ -258,23 +214,23 @@ Retrieve performance data organized by market sectors. data: [ { sector: 'Technology', - changesPercentage: 1.25 + changesPercentage: '1.25%' }, { sector: 'Healthcare', - changesPercentage: 0.87 + changesPercentage: '0.87%' }, { sector: 'Financial Services', - changesPercentage: 0.45 + changesPercentage: '0.45%' }, { sector: 'Consumer Cyclical', - changesPercentage: 0.32 + changesPercentage: '0.32%' }, { sector: 'Communication Services', - changesPercentage: 0.18 + changesPercentage: '0.18%' } ] }`} @@ -294,27 +250,28 @@ Retrieve detailed market index data. data: [ { symbol: '^DJI', + name: 'Dow Jones Industrial Average', price: 37592.98, - extendedPrice: null, + changesPercentage: 0.34, change: 125.45, - dayHigh: 37650.25, dayLow: 37450.75, - previousClose: 37467.53, - volume: null, - open: 37500.00, - close: null, - lastTradeTime: '2024-01-15T21:00:00.000+00:00', - lastExtendedTradeTime: null, - updatedAt: '2024-01-15T21:00:00.000+00:00', - createdAt: '2024-01-15T21:00:00.000+00:00', - type: 'index', - name: 'Dow Jones Industrial Average', - range: '37450.75-37650.25', + dayHigh: 37650.25, yearHigh: 37850.00, yearLow: 31429.82, + marketCap: null, priceAvg50: 37250.45, priceAvg200: 36890.12, - changesPercentage: 0.34 + exchange: 'INDEX', + volume: 0, + avgVolume: 0, + open: 37500.00, + previousClose: 37467.53, + eps: null, + pe: null, + earningsAnnouncement: null, + sharesOutstanding: null, + timestamp: 1705352400, + type: 'index' } ] }`} @@ -331,6 +288,7 @@ Retrieve detailed market index data. change: number; price: number; changesPercentage: number; + exchange?: string; }`} @@ -352,15 +310,15 @@ Retrieve detailed market index data. interface MarketHoliday { year: number; -'Martin Luther King, Jr. Day': string; -"Presidents' Day": string; -'Good Friday': string; -'Memorial Day': string; -Juneteenth: string; -'Independence Day': string; -'Labor Day': string; -'Thanksgiving Day': string; -Christmas: string; +'Martin Luther King, Jr. Day'?: string; +"Presidents' Day"?: string; +'Good Friday'?: string; +'Memorial Day'?: string; +Juneteenth?: string; +'Independence Day'?: string; +'Labor Day'?: string; +'Thanksgiving Day'?: string; +Christmas?: string; }`} @@ -370,7 +328,7 @@ Christmas: string; {`interface MarketSectorPerformance { sector: string; - changesPercentage: number | string; // Note: API may return this as a string + changesPercentage: string; // Formatted string, e.g. "1.23%" }`} @@ -379,27 +337,28 @@ Christmas: string; {`interface MarketIndex { symbol: string; + name: string; price: number; - extendedPrice: number | null; + changesPercentage: number; change: number; - dayHigh: number; dayLow: number; - previousClose: number; - volume: number | null; - open: number; - close: number | null; - lastTradeTime: string; - lastExtendedTradeTime: string | null; - updatedAt: string; - createdAt: string; - type: string; - name: string; - range: string; + dayHigh: number; yearHigh: number; yearLow: number; - priceAvg50: number | null; - priceAvg200: number | null; - changesPercentage: number; + marketCap: number | null; + priceAvg50: number; + priceAvg200: number; + exchange: string; + volume: number; + avgVolume: number; + open: number; + previousClose: number; + eps: number | null; + pe: number | null; + earningsAnnouncement: string | null; + sharesOutstanding: number | null; + timestamp: number; + type?: string; }`} diff --git a/apps/docs/src/app/docs/api/screener/page.mdx b/apps/docs/src/app/docs/api/screener/page.mdx index b630504..995b171 100644 --- a/apps/docs/src/app/docs/api/screener/page.mdx +++ b/apps/docs/src/app/docs/api/screener/page.mdx @@ -347,12 +347,12 @@ Retrieve all supported countries that can be used as filters in the company scre {`interface Screener { symbol: string; companyName: string; - marketCap: number; - sector: string; - industry: string; - beta: number; + marketCap: number | null; + sector: string | null; + industry: string | null; + beta: number | null; price: number; - lastAnnualDividend: number; + lastAnnualDividend: number | null; volume: number; exchange: string; exchangeShortName: string; @@ -372,7 +372,7 @@ Retrieve all supported countries that can be used as filters in the company scre countryName: string; countryCode: string; symbolSuffix: string; - delay: string; + delay: string | null; }`} diff --git a/apps/docs/src/app/docs/api/sec/page.mdx b/apps/docs/src/app/docs/api/sec/page.mdx index 2c7785b..192de03 100644 --- a/apps/docs/src/app/docs/api/sec/page.mdx +++ b/apps/docs/src/app/docs/api/sec/page.mdx @@ -186,6 +186,8 @@ console.log(\`Final Link: \${filing.finalLink}\`); link: string; finalLink: string; date: string; + process: string; + hasFinancials: string; }`} @@ -422,8 +424,8 @@ const classification = await fmp.sec.getIndividualIndustryClassification({ symbol: symbol }); -if (classification.success && classification.data.length > 0) { -const company = classification.data[0]; +if (classification.success && classification.data) { +const company = classification.data; console.log(\`\${company.symbol}: \${company.industryTitle} (SIC: \${company.sicCode})\`); } }`} 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 c560efb..d4842e1 100644 --- a/apps/docs/src/app/docs/api/senate-house/page.mdx +++ b/apps/docs/src/app/docs/api/senate-house/page.mdx @@ -371,7 +371,7 @@ Retrieve house trading data for a specific representative by name. assetType: string; type: string; amount: string; - capitalGainsOver200USD: string; + capitalGainsOver200USD?: string; comment: string; link: string; }`} diff --git a/package.json b/package.json index 37a5ffe..85e9613 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,11 @@ "example:dev": "turbo run dev --filter=fmp-ai-tools-vercel-ai-example", "example:openai:dev": "turbo run dev --filter=fmp-ai-tools-openai-example", "test:endpoint": "cd packages/api && pnpm test:endpoint", + "test:live": "pnpm --filter fmp-node-types build && pnpm --filter fmp-node-api test:live", "test": "turbo run test", "test:watch": "turbo run test:watch", "test:coverage": "turbo run test:coverage", "test:unit": "turbo run test:unit --filter=fmp-node-api", - "test:integration": "turbo run test:integration --filter=fmp-node-api", "test:endpoints": "turbo run test:endpoints --filter=fmp-node-api", "test:quote": "turbo run test:quote --filter=fmp-node-api", "test:stock": "turbo run test:stock --filter=fmp-node-api", diff --git a/packages/api/README.md b/packages/api/README.md index c879336..b667a19 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -259,17 +259,14 @@ const treasury = await fmp.economic.getTreasuryRates({ to: '2024-12-31', }); -// Get federal funds rate -const fedRate = await fmp.economic.getFederalFundsRate(); - -// Get CPI data -const cpi = await fmp.economic.getCPI(); - -// Get GDP data -const gdp = await fmp.economic.getGDP(); - -// Get unemployment data -const unemployment = await fmp.economic.getUnemployment(); +// Get economic indicators by name (GDP, CPI, federalFunds, unemploymentRate, …) +const gdp = await fmp.economic.getEconomicIndicators({ + name: 'GDP', + from: '2024-01-01', + to: '2024-12-31', +}); +const cpi = await fmp.economic.getEconomicIndicators({ name: 'CPI' }); +const unemployment = await fmp.economic.getEconomicIndicators({ name: 'unemploymentRate' }); ``` ### Other Asset Classes @@ -281,10 +278,11 @@ const forexQuote = await fmp.quote.getQuote('EURUSD'); // Cryptocurrency const cryptoQuote = await fmp.quote.getQuote('BTCUSD'); -// Stock lists and indices -const sp500 = await fmp.list.getSP500(); -const nasdaq = await fmp.list.getNasdaq100(); -const dowJones = await fmp.list.getDowJones(); +// Symbol lists +const stocks = await fmp.list.getStockList(); +const etfs = await fmp.list.getETFList(); +const crypto = await fmp.list.getCryptoList(); +const indexes = await fmp.list.getAvailableIndexes(); // Earnings calendar const earnings = await fmp.calendar.getEarningsCalendar({ @@ -298,9 +296,6 @@ const economic = await fmp.calendar.getEconomicCalendar({ to: '2024-12-31', }); -// Company search -const companies = await fmp.company.searchCompany({ query: 'Apple' }); - // Company profile const companyProfile = await fmp.company.getCompanyProfile('AAPL'); ``` @@ -349,18 +344,15 @@ pnpm test ``` - Uses Jest to run all tests -- Automatically loads API key from `.env` file -- Requires a valid FMP API key in your `.env` file +- **Fully mocked and deterministic** — no network and no API key required +- Live API validation is handled separately by `pnpm test:live` (see below) ### Running Specific Tests ```bash -# Run only unit tests +# Run only unit tests (client + main FMP class) pnpm test:unit -# Run only integration tests -pnpm test:integration - # Run tests for specific endpoints pnpm test:stock pnpm test:financial @@ -374,8 +366,11 @@ pnpm test:company # Run all endpoint tests pnpm test:endpoints -# Manual testing against the real API -pnpm test:endpoint +# Inspect one endpoint's raw response against the live API +pnpm test:endpoint + +# Validate response shapes against the live API (needs FMP_API_KEY) +pnpm test:live ``` ## Response Format @@ -507,8 +502,7 @@ pnpm test # Run specific test categories pnpm test:unit # Unit tests (client, main FMP class) -pnpm test:integration # Integration tests -pnpm test:endpoints # All endpoint tests +pnpm test:endpoints # All endpoint tests (mocked) pnpm test:stock # Stock endpoint tests only pnpm test:financial # Financial endpoint tests only pnpm test:market # Market endpoint tests only @@ -528,6 +522,31 @@ pnpm test:coverage # Generate coverage report **Note**: Additional endpoint-specific test scripts (`test:etf`, `test:mutual-fund`, `test:senate-house`, `test:institutional`, `test:insider`, `test:sec`) are also available, both here and at the repo root. +### Live API shape check (`test:live`) + +`test:live` validates real FMP responses against the canonical Zod schemas in `fmp-node-types` — catching renamed/missing/changed fields and version-mismatch 404s, not just "did it respond". Unlike `test:endpoint` (which prints one endpoint's raw JSON), `test:live` runs many cases and prints a classified summary. + +Run from the repo root (it builds `fmp-node-types` first so the runner uses fresh schemas): + +```bash +pnpm test:live # all seeded cases +pnpm test:live --category quote,stock # only these categories +pnpm test:live --endpoint getQuote # cases whose name matches +pnpm test:live --dry-run # list cases, make NO API calls +pnpm test:live --category financial --max-calls 10 +``` + +Each result is classified: + +- **PASS** — response matches the schema +- **FAIL** — an expected field is missing or has the wrong (non-null) type +- **SKIP** — plan-locked (HTTP 402/403) or rate/quota limited (429) +- **DRIFT** — extra top-level fields, or a non-nullable field came back `null` + +Flags: `--delay ` (default 400) paces calls; `--max-calls ` (default 50) caps live calls; `--include-locked` also runs cases marked `planLocked`; `--fail-on-drift` makes DRIFT fail the exit code. The runner is sequential and throttled to stay within limited API plans — the first full run doubles as a calibration pass: mark any consistently plan-locked endpoints `planLocked: true` in `scripts/live/manifest.ts` so later default runs skip them. Exits non-zero when any case FAILs. + +> Coverage note: the seeded manifest currently covers the `quote`, `stock`, `financial`, and `market` categories. The classifier logic is unit-tested in `src/__tests__/live/validate.test.ts` (no API key needed). + ## Development ### Prerequisites @@ -565,12 +584,12 @@ pnpm build # Build the package pnpm dev # Watch mode for development # Testing -pnpm test # Run all tests +pnpm test # Run all tests (mocked, deterministic) pnpm test:watch # Watch mode pnpm test:coverage # Coverage report -pnpm test:endpoint # Run specific endpoint test against the live API +pnpm test:live # Validate response shapes against the live API +pnpm test:endpoint # Inspect one endpoint's raw response (live) pnpm test:unit # Run unit tests -pnpm test:integration # Run integration tests pnpm test:endpoints # Run all endpoint tests pnpm test:stock # Run stock endpoint tests pnpm test:financial # Run financial endpoint tests @@ -625,10 +644,16 @@ src/ │ ├── helpers.ts # Shared helpers │ ├── debug.ts # Debug logging │ └── utils.ts # Misc utilities -└── __tests__/ # Jest tests (client, fmp, integration, endpoints/, utils/) +├── live/ # Live-check classifier (dev-only; not exported/shipped) +│ └── validate.ts # Relaxed validator / classifier (PASS/FAIL/SKIP/DRIFT) +└── __tests__/ # Jest tests (client, fmp, integration, endpoints/, utils/, live/) scripts/ -└── test-endpoint.ts # Manual live-API endpoint testing script +├── test-endpoint.ts # Manual live-API endpoint inspector (raw JSON, one endpoint) +└── live/ # Live-API shape-check tool (pnpm test:live) + ├── run.ts # CLI runner (flags, throttle, budget, summary) + ├── manifest.ts # Data-driven registry of cases (endpoint + inputs + schema) + └── tsconfig.json # Type-checks the runner (pnpm type-check:live) ``` ## Contributing diff --git a/packages/api/package.json b/packages/api/package.json index e91d1b5..d0a67fb 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -35,7 +35,6 @@ "test:watch": "jest --watch", "test:coverage": "jest --coverage", "test:unit": "jest --testPathPattern=\"src/__tests__/(client|fmp)\\.test\\.ts\"", - "test:integration": "jest --testPathPattern=\"src/__tests__/integration\\.test\\.ts\"", "test:endpoints": "jest --testPathPattern=\"src/__tests__/endpoints\"", "test:quote": "jest --testPathPattern=\"src/__tests__/endpoints/quote\\.test\\.ts\"", "test:stock": "jest --testPathPattern=\"src/__tests__/endpoints/stock\\.test\\.ts\"", @@ -52,7 +51,9 @@ "test:insider": "jest --testPathPattern=\"src/__tests__/endpoints/insider\\.test\\.ts\"", "test:sec": "jest --testPathPattern=\"src/__tests__/endpoints/sec\\.test\\.ts\"", "test:endpoint": "tsx scripts/test-endpoint.ts", + "test:live": "tsx scripts/live/run.ts", "type-check": "tsc --noEmit", + "type-check:live": "tsc -p scripts/live/tsconfig.json", "format": "prettier --write src/**/*.ts", "format:check": "prettier --check src/**/*.ts", "clean": "rm -rf dist" @@ -78,13 +79,14 @@ "devDependencies": { "@types/jest": "^29.5.12", "@types/node": "^20.11.0", - "fmp-node-types": "workspace:*", "dotenv": "^16.3.1", + "fmp-node-types": "workspace:*", "jest": "^29.7.0", "prettier": "^3.2.5", "ts-jest": "^29.1.2", "tsup": "^8.0.0", "tsx": "^4.7.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "zod": "^3.25.76" } } diff --git a/packages/api/scripts/live/manifest.ts b/packages/api/scripts/live/manifest.ts new file mode 100644 index 0000000..bc2bf3d --- /dev/null +++ b/packages/api/scripts/live/manifest.ts @@ -0,0 +1,276 @@ +// Data-driven registry of live-API test cases. +// +// Each entry calls one endpoint method with curated inputs (lifted from +// scripts/test-endpoint.ts) and names the schema + payload kind to validate +// against. Mark `planLocked: true` for endpoints confirmed unavailable on the +// current FMP plan so default runs skip them (see --include-locked). + +import { z } from 'zod'; +import { + // quote + QuoteSchema, + HistoricalPriceResponseSchema, + IntradayPriceSchema, + // stock + MarketCapSchema, + StockSplitResponseSchema, + StockDividendResponseSchema, + StockRealTimePriceSchema, + StockRealTimePriceFullSchema, + // market + MarketHoursSchema, + MarketPerformanceSchema, + MarketSectorPerformanceSchema, + MarketIndexSchema, + // financial + IncomeStatementSchema, + BalanceSheetSchema, + CashFlowStatementSchema, + KeyMetricsSchema, + FinancialRatiosSchema, + EnterpriseValueSchema, + CashflowGrowthSchema, + IncomeGrowthSchema, + BalanceSheetGrowthSchema, + FinancialGrowthSchema, + EarningsHistoricalSchema, + EarningsSurprisesSchema, + // calendar + EarningsCalendarSchema, + EarningsConfirmedSchema, + DividendsCalendarSchema, + EconomicsCalendarSchema, + IPOCalendarSchema, + SplitsCalendarSchema, + // company + CompanyProfileSchema, + ExecutiveCompensationSchema, + CompanyNotesSchema, + HistoricalEmployeeCountSchema, + SharesFloatSchema, + HistoricalSharesFloatSchema, + EarningsCallTranscriptSchema, + CompanyTranscriptDataSchema, + // economic + TreasuryRateSchema, + EconomicIndicatorSchema, + // etf + ETFHoldingDatesSchema, + ETFHoldingSchema, + ETFHolderSchema, + ETFProfileSchema, + ETFWeightingSchema, + ETFCountryWeightingSchema, + ETFStockExposureSchema, + // insider + InsiderTradingRSSResponseSchema, + InsiderTradingSearchResponseSchema, + TransactionTypeRecordSchema, + InsidersBySymbolResponseSchema, + InsiderTradeStatisticsResponseSchema, + CikMapperResponseSchema, + CikMapperBySymbolResponseSchema, + BeneficialOwnershipResponseSchema, + FailToDeliverResponseSchema, + // institutional + Form13FResponseSchema, + InstitutionalHolderResponseSchema, + // list + StockListSchema, + ETFListSchema, + CryptoListSchema, + ForexListSchema, + AvailableIndexesListSchema, + // mutual-fund + MutualFundHoldingSchema, + // news + ArticleSchema, + NewsSchema, + // screener + ScreenerSchema, + AvailableExchangesSchema, + AvailableSectorsSchema, + AvailableIndustriesSchema, + AvailableCountriesSchema, + // sec + RSSFeedItemSchema, + RSSFeedAllItemSchema, + RSSFeedV3ItemSchema, + RSSFeed8KItemSchema, + SECFilingSchema, + IndustryClassificationSchema, + IndustryClassificationCodeSchema, + // senate-house + SenateTradingResponseSchema, + HouseTradingResponseSchema, + SenateHouseTradingByNameResponseSchema, +} from 'fmp-node-types'; +import type { FMP } from '../../src/fmp'; + +export type Category = + | 'quote' + | 'stock' + | 'financial' + | 'market' + | 'calendar' + | 'company' + | 'economic' + | 'etf' + | 'insider' + | 'institutional' + | 'list' + | 'mutual-fund' + | 'news' + | 'screener' + | 'sec' + | 'senate-house'; + +export interface LiveCase { + category: Category; + /** Human label, e.g. 'getQuote(AAPL)'. */ + name: string; + /** Canonical schema for one record. */ + schema: z.ZodTypeAny; + /** 'object' => validate the payload; 'array' => validate a sample of elements. */ + kind: 'object' | 'array'; + /** Performs the live call; returns the APIResponse. */ + call: (fmp: FMP) => Promise<{ success: boolean; data: unknown; error: string | null; status: number }>; + /** Skip by default (known plan-locked); run only with --include-locked. */ + planLocked?: boolean; +} + +const RANGE = { from: '2024-01-01', to: '2024-01-31' }; + +export const manifest: LiveCase[] = [ + // ---- quote ---- + { category: 'quote', name: 'getQuote(AAPL)', schema: QuoteSchema, kind: 'object', call: (fmp) => fmp.quote.getQuote('AAPL') }, + { category: 'quote', name: 'getQuotes([AAPL,GOOGL])', schema: QuoteSchema, kind: 'array', call: (fmp) => fmp.quote.getQuotes(['AAPL', 'GOOGL']) }, + { category: 'quote', name: 'getHistoricalPrice(AAPL)', schema: HistoricalPriceResponseSchema, kind: 'object', call: (fmp) => fmp.quote.getHistoricalPrice({ symbol: 'AAPL', ...RANGE }) }, + { category: 'quote', name: 'getIntraday(AAPL,5min)', schema: IntradayPriceSchema, kind: 'array', call: (fmp) => fmp.quote.getIntraday({ symbol: 'AAPL', interval: '5min', from: '2024-01-02', to: '2024-01-03' }) }, + + // ---- stock ---- + { category: 'stock', name: 'getMarketCap(AAPL)', schema: MarketCapSchema, kind: 'object', call: (fmp) => fmp.stock.getMarketCap('AAPL') }, + { category: 'stock', name: 'getStockSplits(AAPL)', schema: StockSplitResponseSchema, kind: 'object', call: (fmp) => fmp.stock.getStockSplits('AAPL') }, + { category: 'stock', name: 'getDividendHistory(AAPL)', schema: StockDividendResponseSchema, kind: 'object', call: (fmp) => fmp.stock.getDividendHistory('AAPL') }, + { category: 'stock', name: 'getRealTimePrice([AAPL,MSFT])', schema: StockRealTimePriceSchema, kind: 'array', call: (fmp) => fmp.stock.getRealTimePrice(['AAPL', 'MSFT']) }, + { category: 'stock', name: 'getRealTimePriceForMultipleStocks([AAPL,MSFT])', schema: StockRealTimePriceFullSchema, kind: 'array', call: (fmp) => fmp.stock.getRealTimePriceForMultipleStocks(['AAPL', 'MSFT']) }, + + // ---- market ---- + { category: 'market', name: 'getMarketHours()', schema: MarketHoursSchema, kind: 'object', call: (fmp) => fmp.market.getMarketHours() }, + { category: 'market', name: 'getMarketPerformance()', schema: MarketIndexSchema, kind: 'array', call: (fmp) => fmp.market.getMarketPerformance() }, + { category: 'market', name: 'getGainers()', schema: MarketPerformanceSchema, kind: 'array', call: (fmp) => fmp.market.getGainers() }, + { category: 'market', name: 'getLosers()', schema: MarketPerformanceSchema, kind: 'array', call: (fmp) => fmp.market.getLosers() }, + { category: 'market', name: 'getMostActive()', schema: MarketPerformanceSchema, kind: 'array', call: (fmp) => fmp.market.getMostActive() }, + { category: 'market', name: 'getSectorPerformance()', schema: MarketSectorPerformanceSchema, kind: 'array', call: (fmp) => fmp.market.getSectorPerformance() }, + { category: 'market', name: 'getMarketIndex()', schema: MarketIndexSchema, kind: 'array', call: (fmp) => fmp.market.getMarketIndex() }, + + // ---- financial ---- + { category: 'financial', name: 'getIncomeStatement(AAPL,annual,2)', schema: IncomeStatementSchema, kind: 'array', call: (fmp) => fmp.financial.getIncomeStatement({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'financial', name: 'getBalanceSheet(AAPL,annual,2)', schema: BalanceSheetSchema, kind: 'array', call: (fmp) => fmp.financial.getBalanceSheet({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'financial', name: 'getCashFlowStatement(AAPL,annual,2)', schema: CashFlowStatementSchema, kind: 'array', call: (fmp) => fmp.financial.getCashFlowStatement({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'financial', name: 'getKeyMetrics(AAPL,annual,2)', schema: KeyMetricsSchema, kind: 'array', call: (fmp) => fmp.financial.getKeyMetrics({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'financial', name: 'getFinancialRatios(AAPL,annual,2)', schema: FinancialRatiosSchema, kind: 'array', call: (fmp) => fmp.financial.getFinancialRatios({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'financial', name: 'getEnterpriseValue(AAPL,annual,2)', schema: EnterpriseValueSchema, kind: 'array', call: (fmp) => fmp.financial.getEnterpriseValue({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'financial', name: 'getCashflowGrowth(AAPL,annual,2)', schema: CashflowGrowthSchema, kind: 'array', call: (fmp) => fmp.financial.getCashflowGrowth({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'financial', name: 'getIncomeGrowth(AAPL,annual,2)', schema: IncomeGrowthSchema, kind: 'array', call: (fmp) => fmp.financial.getIncomeGrowth({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'financial', name: 'getBalanceSheetGrowth(AAPL,annual,2)', schema: BalanceSheetGrowthSchema, kind: 'array', call: (fmp) => fmp.financial.getBalanceSheetGrowth({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'financial', name: 'getFinancialGrowth(AAPL,annual,2)', schema: FinancialGrowthSchema, kind: 'array', call: (fmp) => fmp.financial.getFinancialGrowth({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'financial', name: 'getEarningsHistorical(AAPL,2)', schema: EarningsHistoricalSchema, kind: 'array', call: (fmp) => fmp.financial.getEarningsHistorical({ symbol: 'AAPL', limit: 2 }) }, + { category: 'financial', name: 'getEarningsSurprises(AAPL)', schema: EarningsSurprisesSchema, kind: 'array', call: (fmp) => fmp.financial.getEarningsSurprises('AAPL') }, + + // ---- calendar ---- + { category: 'calendar', name: 'getEarningsCalendar()', schema: EarningsCalendarSchema, kind: 'array', call: (fmp) => fmp.calendar.getEarningsCalendar({ from: '2024-01-15', to: '2024-01-21' }) }, + { category: 'calendar', name: 'getEarningsConfirmed()', schema: EarningsConfirmedSchema, kind: 'array', call: (fmp) => fmp.calendar.getEarningsConfirmed(RANGE) }, + { category: 'calendar', name: 'getDividendsCalendar()', schema: DividendsCalendarSchema, kind: 'array', call: (fmp) => fmp.calendar.getDividendsCalendar(RANGE) }, + { category: 'calendar', name: 'getEconomicsCalendar()', schema: EconomicsCalendarSchema, kind: 'array', call: (fmp) => fmp.calendar.getEconomicsCalendar(RANGE) }, + { category: 'calendar', name: 'getIPOCalendar()', schema: IPOCalendarSchema, kind: 'array', call: (fmp) => fmp.calendar.getIPOCalendar(RANGE) }, + { category: 'calendar', name: 'getSplitsCalendar()', schema: SplitsCalendarSchema, kind: 'array', call: (fmp) => fmp.calendar.getSplitsCalendar(RANGE) }, + + // ---- company ---- + { category: 'company', name: 'getCompanyProfile(AAPL)', schema: CompanyProfileSchema, kind: 'object', call: (fmp) => fmp.company.getCompanyProfile('AAPL') }, + { category: 'company', name: 'getExecutiveCompensation(AAPL)', schema: ExecutiveCompensationSchema, kind: 'array', call: (fmp) => fmp.company.getExecutiveCompensation('AAPL') }, + { category: 'company', name: 'getCompanyNotes(AAPL)', schema: CompanyNotesSchema, kind: 'array', call: (fmp) => fmp.company.getCompanyNotes('AAPL') }, + { category: 'company', name: 'getHistoricalEmployeeCount(AAPL)', schema: HistoricalEmployeeCountSchema, kind: 'array', call: (fmp) => fmp.company.getHistoricalEmployeeCount('AAPL') }, + { category: 'company', name: 'getSharesFloat(AAPL)', schema: SharesFloatSchema, kind: 'object', call: (fmp) => fmp.company.getSharesFloat('AAPL') }, + { category: 'company', name: 'getHistoricalSharesFloat(AAPL)', schema: HistoricalSharesFloatSchema, kind: 'array', call: (fmp) => fmp.company.getHistoricalSharesFloat('AAPL') }, + { category: 'company', name: 'getEarningsCallTranscript(AAPL,2020,3)', schema: EarningsCallTranscriptSchema, kind: 'object', call: (fmp) => fmp.company.getEarningsCallTranscript({ symbol: 'AAPL', year: 2020, quarter: 3 }) }, + { category: 'company', name: 'getCompanyTranscriptData(AAPL)', schema: CompanyTranscriptDataSchema, kind: 'array', call: (fmp) => fmp.company.getCompanyTranscriptData('AAPL') }, + + // ---- economic ---- + { category: 'economic', name: 'getTreasuryRates()', schema: TreasuryRateSchema, kind: 'array', call: (fmp) => fmp.economic.getTreasuryRates({ from: '2024-01-01', to: '2024-12-31' }) }, + { category: 'economic', name: 'getEconomicIndicators(CPI)', schema: EconomicIndicatorSchema, kind: 'array', call: (fmp) => fmp.economic.getEconomicIndicators({ name: 'CPI', from: '2024-01-01', to: '2024-12-31' }) }, + + // ---- etf ---- + { category: 'etf', name: 'getHoldingDates(SPY)', schema: ETFHoldingDatesSchema, kind: 'array', call: (fmp) => fmp.etf.getHoldingDates('SPY') }, + { category: 'etf', name: 'getHoldings(SPY)', schema: ETFHoldingSchema, kind: 'array', call: (fmp) => fmp.etf.getHoldings({ symbol: 'SPY', date: '2023-09-30' }) }, + { category: 'etf', name: 'getHolder(SPY)', schema: ETFHolderSchema, kind: 'array', call: (fmp) => fmp.etf.getHolder('SPY') }, + { category: 'etf', name: 'getProfile(SPY)', schema: ETFProfileSchema, kind: 'object', call: (fmp) => fmp.etf.getProfile('SPY') }, + { category: 'etf', name: 'getSectorWeighting(SPY)', schema: ETFWeightingSchema, kind: 'array', call: (fmp) => fmp.etf.getSectorWeighting('SPY') }, + { category: 'etf', name: 'getCountryWeighting(SPY)', schema: ETFCountryWeightingSchema, kind: 'array', call: (fmp) => fmp.etf.getCountryWeighting('SPY') }, + { category: 'etf', name: 'getStockExposure(SPY)', schema: ETFStockExposureSchema, kind: 'array', call: (fmp) => fmp.etf.getStockExposure('SPY') }, + + // ---- insider ---- + { category: 'insider', name: 'getInsiderTradingRSS()', schema: InsiderTradingRSSResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getInsiderTradingRSS({ page: 0, limit: 10 }) }, + { category: 'insider', name: 'searchInsiderTrading(AAPL)', schema: InsiderTradingSearchResponseSchema, kind: 'array', call: (fmp) => fmp.insider.searchInsiderTrading({ symbol: 'AAPL', page: 0, limit: 10 }) }, + { category: 'insider', name: 'getTransactionTypes()', schema: TransactionTypeRecordSchema, kind: 'array', call: (fmp) => fmp.insider.getTransactionTypes() }, + { category: 'insider', name: 'getInsidersBySymbol(AAPL)', schema: InsidersBySymbolResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getInsidersBySymbol({ symbol: 'AAPL' }) }, + { category: 'insider', name: 'getInsiderTradeStatistics(AAPL)', schema: InsiderTradeStatisticsResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getInsiderTradeStatistics({ symbol: 'AAPL' }) }, + { category: 'insider', name: 'getCikMapper()', schema: CikMapperResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getCikMapper({ page: 0 }) }, + { category: 'insider', name: 'getCikMapperByName(zuckerberg)', schema: CikMapperResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getCikMapperByName({ name: 'zuckerberg', page: 0 }) }, + { category: 'insider', name: 'getCikMapperBySymbol(MSFT)', schema: CikMapperBySymbolResponseSchema, kind: 'object', call: (fmp) => fmp.insider.getCikMapperBySymbol({ symbol: 'MSFT' }) }, + { category: 'insider', name: 'getBeneficialOwnership(AAPL)', schema: BeneficialOwnershipResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getBeneficialOwnership({ symbol: 'AAPL', limit: 10 }) }, + { category: 'insider', name: 'getFailToDeliver(GE)', schema: FailToDeliverResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getFailToDeliver({ symbol: 'GE', page: 0 }) }, + { category: 'insider', name: 'getInsiderTradesBySymbol(AAPL)', schema: InsiderTradingSearchResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getInsiderTradesBySymbol('AAPL') }, + { category: 'insider', name: 'getInsiderTradesByType(P-Purchase)', schema: InsiderTradingSearchResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getInsiderTradesByType('P-Purchase') }, + { category: 'insider', name: 'getInsiderTradesByReportingCik', schema: InsiderTradingSearchResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getInsiderTradesByReportingCik('0001767094') }, + { category: 'insider', name: 'getInsiderTradesByCompanyCik', schema: InsiderTradingSearchResponseSchema, kind: 'array', call: (fmp) => fmp.insider.getInsiderTradesByCompanyCik('0000320193') }, + + // ---- institutional ---- + { category: 'institutional', name: 'getForm13F(cik,date)', schema: Form13FResponseSchema, kind: 'array', call: (fmp) => fmp.institutional.getForm13F({ cik: '0001388838', date: '2021-09-30' }) }, + { category: 'institutional', name: 'getForm13FDates(cik)', schema: z.string(), kind: 'array', call: (fmp) => fmp.institutional.getForm13FDates({ cik: '0001067983' }) }, + { category: 'institutional', name: 'getInstitutionalHolders(AAPL)', schema: InstitutionalHolderResponseSchema, kind: 'array', call: (fmp) => fmp.institutional.getInstitutionalHolders({ symbol: 'AAPL' }) }, + + // ---- list ---- + { category: 'list', name: 'getStockList()', schema: StockListSchema, kind: 'array', call: (fmp) => fmp.list.getStockList() }, + { category: 'list', name: 'getETFList()', schema: ETFListSchema, kind: 'array', call: (fmp) => fmp.list.getETFList() }, + { category: 'list', name: 'getCryptoList()', schema: CryptoListSchema, kind: 'array', call: (fmp) => fmp.list.getCryptoList() }, + { category: 'list', name: 'getForexList()', schema: ForexListSchema, kind: 'array', call: (fmp) => fmp.list.getForexList() }, + { category: 'list', name: 'getAvailableIndexes()', schema: AvailableIndexesListSchema, kind: 'array', call: (fmp) => fmp.list.getAvailableIndexes() }, + + // ---- mutual-fund ---- + { category: 'mutual-fund', name: 'getHolders(AAPL)', schema: MutualFundHoldingSchema, kind: 'array', call: (fmp) => fmp.mutualFund.getHolders('AAPL') }, + + // ---- news ---- + { category: 'news', name: 'getArticles()', schema: ArticleSchema, kind: 'array', call: (fmp) => fmp.news.getArticles({ page: 0, limit: 10 }) }, + { category: 'news', name: 'getStockNews()', schema: NewsSchema, kind: 'array', call: (fmp) => fmp.news.getStockNews({ from: '2025-07-01', to: '2025-07-31', page: 0, limit: 10 }) }, + { category: 'news', name: 'getCryptoNews()', schema: NewsSchema, kind: 'array', call: (fmp) => fmp.news.getCryptoNews({ from: '2025-07-01', to: '2025-07-31', page: 0, limit: 10 }) }, + { category: 'news', name: 'getForexNews()', schema: NewsSchema, kind: 'array', call: (fmp) => fmp.news.getForexNews({ from: '2025-07-01', to: '2025-07-31', page: 0, limit: 10 }) }, + { category: 'news', name: 'getStockNewsBySymbol([TSLA,GOOGL])', schema: NewsSchema, kind: 'array', call: (fmp) => fmp.news.getStockNewsBySymbol({ symbols: ['TSLA', 'GOOGL'], from: '2025-07-01', to: '2025-07-31', page: 0, limit: 10 }) }, + { category: 'news', name: 'getCryptoNewsBySymbol([BTCUSD])', schema: NewsSchema, kind: 'array', call: (fmp) => fmp.news.getCryptoNewsBySymbol({ symbols: ['BTCUSD', 'ETHUSD'], from: '2025-07-01', to: '2025-07-31', page: 0, limit: 10 }) }, + { category: 'news', name: 'getForexNewsBySymbol([EURUSD])', schema: NewsSchema, kind: 'array', call: (fmp) => fmp.news.getForexNewsBySymbol({ symbols: ['EURUSD', 'GBPUSD'], from: '2025-07-01', to: '2025-07-31', page: 0, limit: 10 }) }, + + // ---- screener ---- + { category: 'screener', name: 'getScreener(limit=10)', schema: ScreenerSchema, kind: 'array', call: (fmp) => fmp.screener.getScreener({ limit: 10 }) }, + { category: 'screener', name: 'getAvailableExchanges()', schema: AvailableExchangesSchema, kind: 'array', call: (fmp) => fmp.screener.getAvailableExchanges() }, + { category: 'screener', name: 'getAvailableSectors()', schema: AvailableSectorsSchema, kind: 'array', call: (fmp) => fmp.screener.getAvailableSectors() }, + { category: 'screener', name: 'getAvailableIndustries()', schema: AvailableIndustriesSchema, kind: 'array', call: (fmp) => fmp.screener.getAvailableIndustries() }, + { category: 'screener', name: 'getAvailableCountries()', schema: AvailableCountriesSchema, kind: 'array', call: (fmp) => fmp.screener.getAvailableCountries() }, + + // ---- sec ---- + { category: 'sec', name: 'getRSSFeed()', schema: RSSFeedItemSchema, kind: 'array', call: (fmp) => fmp.sec.getRSSFeed({ limit: 5, type: '10-K', from: '2024-01-01', to: '2024-12-31', isDone: true }) }, + { category: 'sec', name: 'getRSSFeedAll()', schema: RSSFeedAllItemSchema, kind: 'array', call: (fmp) => fmp.sec.getRSSFeedAll({ page: 0 }) }, + { category: 'sec', name: 'getRSSFeedV3()', schema: RSSFeedV3ItemSchema, kind: 'array', call: (fmp) => fmp.sec.getRSSFeedV3({ page: 0 }) }, + { category: 'sec', name: 'getRSSFeed8K()', schema: RSSFeed8KItemSchema, kind: 'array', call: (fmp) => fmp.sec.getRSSFeed8K({ page: 0 }) }, + { category: 'sec', name: 'getSECFilings(AAPL)', schema: SECFilingSchema, kind: 'array', call: (fmp) => fmp.sec.getSECFilings({ symbol: 'AAPL', params: { page: 0 } }) }, + { category: 'sec', name: 'getIndividualIndustryClassification(AAPL)', schema: IndustryClassificationSchema, kind: 'object', call: (fmp) => fmp.sec.getIndividualIndustryClassification({ symbol: 'AAPL' }) }, + { category: 'sec', name: 'getAllIndustryClassifications()', schema: IndustryClassificationSchema, kind: 'array', call: (fmp) => fmp.sec.getAllIndustryClassifications() }, + { category: 'sec', name: 'getIndustryClassificationCodes(Software)', schema: IndustryClassificationCodeSchema, kind: 'array', call: (fmp) => fmp.sec.getIndustryClassificationCodes({ industryTitle: 'Software' }) }, + + // ---- senate-house ---- + { category: 'senate-house', name: 'getSenateTrading(AAPL)', schema: SenateTradingResponseSchema, kind: 'array', call: (fmp) => fmp.senateHouse.getSenateTrading({ symbol: 'AAPL' }) }, + { category: 'senate-house', name: 'getSenateTradingRSSFeed()', schema: SenateTradingResponseSchema, kind: 'array', call: (fmp) => fmp.senateHouse.getSenateTradingRSSFeed({ page: 0 }) }, + { category: 'senate-house', name: 'getSenateTradingByName(Jerry)', schema: SenateHouseTradingByNameResponseSchema, kind: 'array', call: (fmp) => fmp.senateHouse.getSenateTradingByName({ name: 'Jerry' }) }, + { category: 'senate-house', name: 'getHouseTrading(AAPL)', schema: HouseTradingResponseSchema, kind: 'array', call: (fmp) => fmp.senateHouse.getHouseTrading({ symbol: 'AAPL' }) }, + { category: 'senate-house', name: 'getHouseTradingRSSFeed()', schema: HouseTradingResponseSchema, kind: 'array', call: (fmp) => fmp.senateHouse.getHouseTradingRSSFeed({ page: 0 }) }, + { category: 'senate-house', name: 'getHouseTradingByName(nancy pelosi)', schema: SenateHouseTradingByNameResponseSchema, kind: 'array', call: (fmp) => fmp.senateHouse.getHouseTradingByName({ name: 'nancy pelosi' }) }, +]; diff --git a/packages/api/scripts/live/run.ts b/packages/api/scripts/live/run.ts new file mode 100644 index 0000000..7ed894a --- /dev/null +++ b/packages/api/scripts/live/run.ts @@ -0,0 +1,205 @@ +#!/usr/bin/env node +// +// Live-API shape-check runner. +// +// Calls fmp-node-api endpoints against the REAL FMP API (throttled, budgeted) +// and validates each response against its canonical Zod schema, classifying +// results PASS / FAIL / SKIP / DRIFT. Exits non-zero if any FAIL occurred +// (or any DRIFT with --fail-on-drift). +// +// Usage: +// pnpm test:live [flags] +// pnpm test:live --category quote,stock --delay 400 --max-calls 20 +// pnpm test:live --endpoint getQuote --dry-run +// +// Flags: +// --category only run these categories (quote,stock,financial,market) +// --endpoint only run cases whose name includes +// --delay delay between calls (default 400) +// --max-calls hard cap on live calls this run (default 50) +// --sample array elements validated per response (default 3) +// --include-locked also run cases marked planLocked +// --fail-on-drift treat DRIFT as a failure for the exit code +// --dry-run list selected cases and planned call count; make no calls +// --help, -h show this help + +import { config } from 'dotenv'; +import { resolve } from 'path'; +import { existsSync } from 'fs'; + +const envPath = resolve(__dirname, '../../../../.env'); +if (existsSync(envPath)) { + config({ path: envPath }); +} + +import { FMP } from '../../src/fmp'; +import { manifest, type Category, type LiveCase } from './manifest'; +import { classifyResult, type Outcome } from '../../src/live/validate'; + +type DisplayOutcome = Outcome | 'NRUN'; + +interface Row { + name: string; + category: Category; + outcome: DisplayOutcome; + detail: string; + ms: number | null; +} + +const COLORS: Record = { + PASS: '\x1b[32m', + FAIL: '\x1b[31m', + SKIP: '\x1b[33m', + DRIFT: '\x1b[35m', + NRUN: '\x1b[90m', +}; +const RESET = '\x1b[0m'; + +function color(outcome: DisplayOutcome, text: string): string { + return `${COLORS[outcome]}${text}${RESET}`; +} + +function getFlag(args: string[], name: string): string | undefined { + const i = args.indexOf(`--${name}`); + return i !== -1 ? args[i + 1] : undefined; +} + +function hasFlag(args: string[], name: string): boolean { + return args.includes(`--${name}`); +} + +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} + +const HELP = `Live-API shape-check runner + +Usage: pnpm test:live [flags] + + --category only run these categories (quote,stock,financial,market) + --endpoint only run cases whose name includes + --delay delay between calls (default 400) + --max-calls hard cap on live calls this run (default 50) + --sample array elements validated per response (default 3) + --include-locked also run cases marked planLocked + --fail-on-drift treat DRIFT as a failure for the exit code + --dry-run list selected cases and planned call count; make no calls + --help, -h show this help +`; + +async function main(): Promise { + const args = process.argv.slice(2); + + if (hasFlag(args, 'help') || args.includes('-h')) { + console.log(HELP); + return; + } + + const categoryFilter = getFlag(args, 'category')?.split(',').map((s) => s.trim()).filter(Boolean); + const endpointFilter = getFlag(args, 'endpoint'); + const delay = Number(getFlag(args, 'delay') ?? 400); + const maxCalls = Number(getFlag(args, 'max-calls') ?? 50); + const includeLocked = hasFlag(args, 'include-locked'); + const failOnDrift = hasFlag(args, 'fail-on-drift'); + const dryRun = hasFlag(args, 'dry-run'); + const sampleSize = Number(getFlag(args, 'sample') ?? 3); + + let selected: LiveCase[] = manifest; + if (categoryFilter && categoryFilter.length > 0) { + selected = selected.filter((c) => categoryFilter.includes(c.category)); + } + if (endpointFilter) { + selected = selected.filter((c) => c.name.toLowerCase().includes(endpointFilter.toLowerCase())); + } + if (!includeLocked) { + selected = selected.filter((c) => !c.planLocked); + } + + if (selected.length === 0) { + console.log('No cases match the given filters.'); + process.exit(1); + } + + const plannedCalls = Math.min(selected.length, maxCalls); + + if (dryRun) { + console.log(`\nDRY RUN — ${selected.length} case(s) selected, would make ${plannedCalls} call(s):\n`); + for (const c of selected) { + console.log(` ${c.category.padEnd(10)} ${c.name}${c.planLocked ? ' (plan-locked)' : ''}`); + } + console.log(''); + return; + } + + const apiKey = process.env.FMP_API_KEY; + if (!apiKey) { + console.error('❌ FMP_API_KEY not found. Set it in the repo-root .env (cp .env.example .env).'); + process.exit(1); + } + + const fmp = new FMP({ apiKey }); + const rows: Row[] = []; + let calls = 0; + let stopped = false; + + console.log(`\nRunning ${plannedCalls} live call(s) (delay ${delay}ms, budget ${maxCalls})...\n`); + + for (const c of selected) { + if (stopped) { + rows.push({ name: c.name, category: c.category, outcome: 'NRUN', detail: 'stopped (rate limit)', ms: null }); + continue; + } + if (calls >= maxCalls) { + rows.push({ name: c.name, category: c.category, outcome: 'NRUN', detail: 'budget reached', ms: null }); + continue; + } + if (calls > 0) await sleep(delay); + + const start = Date.now(); + let res: { success: boolean; data: unknown; error: string | null; status: number }; + try { + res = await c.call(fmp); + } catch (err) { + res = { success: false, data: null, error: err instanceof Error ? err.message : String(err), status: 0 }; + } + calls++; + const ms = Date.now() - start; + + const cls = classifyResult(res, c.schema, c.kind, sampleSize); + rows.push({ name: c.name, category: c.category, outcome: cls.outcome, detail: cls.detail, ms }); + if (cls.stopRun) stopped = true; + } + + // Results table + const nameWidth = Math.max(...rows.map((r) => r.name.length), 20); + for (const r of rows) { + const ms = r.ms === null ? '—' : `${r.ms}ms`; + const line = `${r.name.padEnd(nameWidth)} ${color(r.outcome, r.outcome.padEnd(5))} ${ms.padStart(7)} ${r.detail}`; + console.log(line); + } + + // Summary + const count = (o: DisplayOutcome) => rows.filter((r) => r.outcome === o).length; + const pass = count('PASS'); + const fail = count('FAIL'); + const skip = count('SKIP'); + const drift = count('DRIFT'); + const nrun = count('NRUN'); + + console.log( + `\n${calls} call(s) made · ` + + `${color('PASS', `${pass} pass`)} · ` + + `${color('FAIL', `${fail} fail`)} · ` + + `${color('SKIP', `${skip} skip`)} · ` + + `${color('DRIFT', `${drift} drift`)}` + + (nrun > 0 ? ` · ${color('NRUN', `${nrun} not run`)}` : ''), + ); + + const failed = fail > 0 || (failOnDrift && drift > 0); + process.exit(failed ? 1 : 0); +} + +main().catch((err) => { + console.error('❌ Runner error:', err instanceof Error ? err.message : String(err)); + process.exit(1); +}); diff --git a/packages/api/scripts/live/tsconfig.json b/packages/api/scripts/live/tsconfig.json new file mode 100644 index 0000000..e39fb62 --- /dev/null +++ b/packages/api/scripts/live/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types": ["node"], + "baseUrl": ".", + "paths": { + "@/*": ["../../src/*"], + "fmp-node-types": ["../../../types/dist"] + } + }, + "include": ["./**/*"] +} diff --git a/packages/api/src/__tests__/endpoints/calendar.test.ts b/packages/api/src/__tests__/endpoints/calendar.test.ts index 2f6da82..1bff996 100644 --- a/packages/api/src/__tests__/endpoints/calendar.test.ts +++ b/packages/api/src/__tests__/endpoints/calendar.test.ts @@ -1,633 +1,243 @@ -import { FMP } from '@/fmp'; -import { - shouldSkipTests, - createTestClient, - API_TIMEOUT, - FAST_TIMEOUT, - TEST_DATE_RANGES, -} from '../utils/test-setup'; -import type { - EarningsCalendar, - DividendsCalendar, - EconomicsCalendar, - IPOCalendar, - SplitsCalendar, - EarningsConfirmed, -} from 'fmp-node-types'; - -// Helper function to safely access data that could be an array or single object -function getFirstItem(data: T | T[]): T { - return Array.isArray(data) ? data[0] : data; -} - -// Helper function to validate date format (YYYY-MM-DD) -function isValidDateFormat(date: string): boolean { - if (typeof date !== 'string') return false; - if (date.length === 0) return false; - - // More flexible date validation - accept various formats - const dateRegex = /^\d{4}-\d{2}-\d{2}$/; - if (dateRegex.test(date)) { - const dateObj = new Date(date); - return dateObj instanceof Date && !isNaN(dateObj.getTime()); - } - - // Also accept other date formats that might be returned by the API - return date.includes('-') || date.includes('/') || date.includes('.'); -} - -// Helper function to validate numeric value -function isValidNumber(value: any): boolean { - return typeof value === 'number' && !isNaN(value) && isFinite(value); -} - -// Helper function to validate positive number -function isValidPositiveNumber(value: any): boolean { - return isValidNumber(value) && value > 0; -} - -// Helper function to validate non-negative number -function isValidNonNegativeNumber(value: any): boolean { - return isValidNumber(value) && value >= 0; -} - -// Test data cache to avoid duplicate API calls -interface TestDataCache { - earnings?: any; - earningsConfirmed?: any; - dividends?: any; - economics?: any; - ipo?: any; - splits?: any; -} - -describe('Calendar Endpoints', () => { - let fmp: FMP; - let testDataCache: TestDataCache = {}; - - beforeAll(async () => { - if (shouldSkipTests()) { - console.log('Skipping calendar tests - no API key available'); - return; - } - fmp = createTestClient(); - - try { - // Use smaller, focused date ranges to reduce API usage - const testDateRange = { - from: '2024-01-15', // Single week instead of full month - to: '2024-01-21', - }; +import { CalendarEndpoints } from '../../endpoints/calendar'; +import { FMPClient } from '../../client'; - // Fetch all calendar data in parallel with timeout - const [earnings, earningsConfirmed, dividends, economics, ipo, splits] = await Promise.all([ - fmp.calendar.getEarningsCalendar(testDateRange), - fmp.calendar.getEarningsConfirmed(testDateRange), - fmp.calendar.getDividendsCalendar(testDateRange), - fmp.calendar.getEconomicsCalendar(testDateRange), - fmp.calendar.getIPOCalendar(testDateRange), - fmp.calendar.getSplitsCalendar(testDateRange), - ]); - - testDataCache = { - earnings, - earningsConfirmed, - dividends, - economics, - ipo, - splits, - }; - } catch (error) { - console.warn('Failed to pre-fetch test data:', error); - // Continue with tests - they will fetch data individually if needed - } - }, API_TIMEOUT); // Add timeout to beforeAll hook +// Mock the FMPClient +jest.mock('../../client'); + +describe('CalendarEndpoints', () => { + let calendarEndpoints: CalendarEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + calendarEndpoints = new CalendarEndpoints(mockClient); + }); describe('getEarningsCalendar', () => { - it( - 'should fetch earnings calendar with date range and validate structure', - async () => { - if (shouldSkipTests()) { - console.log('Skipping earnings calendar test - no API key available'); - return; - } - - const result = - testDataCache.earnings || - (await fmp.calendar.getEarningsCalendar({ - from: TEST_DATE_RANGES.RECENT.from, - to: TEST_DATE_RANGES.RECENT.to, - })); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - // Validate data structure if data exists - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const earnings = getFirstItem(result.data) as EarningsCalendar; - - // Required properties - expect(earnings).toHaveProperty('date'); - expect(earnings).toHaveProperty('symbol'); - expect(earnings).toHaveProperty('eps'); - expect(earnings).toHaveProperty('time'); - expect(earnings).toHaveProperty('fiscalDateEnding'); - expect(earnings).toHaveProperty('updatedFromDate'); - - // Optional properties - expect(earnings).toHaveProperty('epsEstimated'); - expect(earnings).toHaveProperty('revenue'); - expect(earnings).toHaveProperty('revenueEstimated'); - - // Validate data types and formats - expect(typeof earnings.date).toBe('string'); - expect(isValidDateFormat(earnings.date)).toBe(true); - - expect(typeof earnings.symbol).toBe('string'); - expect(earnings.symbol.length).toBeGreaterThan(0); - - expect(earnings.eps === null || isValidNumber(earnings.eps)).toBe(true); - expect(earnings.epsEstimated === null || isValidNumber(earnings.epsEstimated)).toBe(true); - - expect(typeof earnings.time).toBe('string'); - - expect(earnings.revenue === null || isValidNumber(earnings.revenue)).toBe(true); - expect( - earnings.revenueEstimated === null || isValidNumber(earnings.revenueEstimated), - ).toBe(true); - - expect(typeof earnings.fiscalDateEnding).toBe('string'); - expect(isValidDateFormat(earnings.fiscalDateEnding)).toBe(true); - - expect(typeof earnings.updatedFromDate).toBe('string'); - expect(isValidDateFormat(earnings.updatedFromDate)).toBe(true); - - // Validate business logic - if (earnings.revenue !== null && earnings.revenueEstimated !== null) { - expect(earnings.revenue).toBeGreaterThanOrEqual(0); - expect(earnings.revenueEstimated).toBeGreaterThanOrEqual(0); - } - } - }, - API_TIMEOUT, - ); - - it( - 'should handle edge cases gracefully', - async () => { - if (shouldSkipTests()) { - console.log('Skipping earnings calendar edge cases test - no API key available'); - return; - } - - // Test multiple edge cases with minimal API calls - // Note: Empty strings ('', '') returns massive data (25k+ items) which consumes too many API calls - const edgeCaseTests = [ - { from: '2024-12-31', to: '2024-12-31', description: 'empty range' }, - { from: '2024-01-15', to: '2024-01-15', description: 'single day' }, - { from: '2030-01-01', to: '2030-01-31', description: 'future dates' }, - { from: '1990-01-01', to: '1990-01-31', description: 'old dates' }, - ]; - - for (const testCase of edgeCaseTests) { - const result = await fmp.calendar.getEarningsCalendar({ - from: testCase.from, - to: testCase.to, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - // Edge cases might return empty array or no data - expect(Array.isArray(result.data) ? result.data.length >= 0 : true).toBe(true); - } - }, - FAST_TIMEOUT, - ); + it('should get earnings calendar with date range using /earning_calendar endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', date: '2024-01-15' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getEarningsCalendar({ + from: '2024-01-01', + to: '2024-01-31', + }); + + expect(mockClient.get).toHaveBeenCalledWith('/earning_calendar', 'v3', { + from: '2024-01-01', + to: '2024-01-31', + }); + expect(result).toEqual(mockResponse); + }); + + it('should get earnings calendar without params', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getEarningsCalendar(); + + expect(mockClient.get).toHaveBeenCalledWith('/earning_calendar', 'v3', {}); + expect(result).toEqual(mockResponse); + }); }); describe('getEarningsConfirmed', () => { - it( - 'should fetch earnings confirmed and validate structure', - async () => { - if (shouldSkipTests()) { - console.log('Skipping earnings confirmed test - no API key available'); - return; - } - - const result = - testDataCache.earningsConfirmed || - (await fmp.calendar.getEarningsConfirmed({ - from: '2024-01-15', - to: '2024-01-21', - })); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - // Validate data structure if data exists - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const earnings = getFirstItem(result.data) as EarningsConfirmed; - - // Required properties - expect(earnings).toHaveProperty('symbol'); - expect(earnings).toHaveProperty('exchange'); - expect(earnings).toHaveProperty('time'); - expect(earnings).toHaveProperty('when'); - expect(earnings).toHaveProperty('date'); - expect(earnings).toHaveProperty('publicationDate'); - expect(earnings).toHaveProperty('title'); - expect(earnings).toHaveProperty('url'); - - // Validate data types and formats - expect(typeof earnings.symbol).toBe('string'); - expect(earnings.symbol.length).toBeGreaterThan(0); - - expect(typeof earnings.exchange).toBe('string'); - expect(earnings.exchange.length).toBeGreaterThan(0); - - expect(typeof earnings.time).toBe('string'); - expect(earnings.time.length).toBeGreaterThan(0); - - expect(typeof earnings.when).toBe('string'); - expect(earnings.when.length).toBeGreaterThan(0); - - expect(typeof earnings.date).toBe('string'); - expect(isValidDateFormat(earnings.date)).toBe(true); - - expect(typeof earnings.publicationDate).toBe('string'); - expect(isValidDateFormat(earnings.publicationDate)).toBe(true); - - expect(typeof earnings.title).toBe('string'); - expect(earnings.title.length).toBeGreaterThan(0); - - expect(typeof earnings.url).toBe('string'); - expect(earnings.url.length).toBeGreaterThan(0); - expect(earnings.url.startsWith('http')).toBe(true); - } - }, - API_TIMEOUT, - ); + it('should get confirmed earnings using /earning-calendar-confirmed v4 endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', date: '2024-01-15' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getEarningsConfirmed({ + from: '2024-01-01', + to: '2024-01-31', + }); + + expect(mockClient.get).toHaveBeenCalledWith('/earning-calendar-confirmed', 'v4', { + from: '2024-01-01', + to: '2024-01-31', + }); + expect(result).toEqual(mockResponse); + }); + + it('should get confirmed earnings without params', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getEarningsConfirmed(); + + expect(mockClient.get).toHaveBeenCalledWith('/earning-calendar-confirmed', 'v4', {}); + expect(result).toEqual(mockResponse); + }); }); describe('getDividendsCalendar', () => { - it( - 'should fetch dividends calendar and validate structure', - async () => { - if (shouldSkipTests()) { - console.log('Skipping dividends calendar test - no API key available'); - return; - } - - const result = - testDataCache.dividends || - (await fmp.calendar.getDividendsCalendar({ - from: '2024-01-15', - to: '2024-01-21', - })); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - // Validate data structure if data exists - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const dividend = getFirstItem(result.data) as DividendsCalendar; - - // Required properties - expect(dividend).toHaveProperty('date'); - expect(dividend).toHaveProperty('label'); - expect(dividend).toHaveProperty('adjDividend'); - expect(dividend).toHaveProperty('symbol'); - expect(dividend).toHaveProperty('dividend'); - expect(dividend).toHaveProperty('recordDate'); - expect(dividend).toHaveProperty('paymentDate'); - expect(dividend).toHaveProperty('declarationDate'); - - // Validate data types - expect(typeof dividend.date).toBe('string'); - expect(isValidDateFormat(dividend.date)).toBe(true); - - expect(typeof dividend.label).toBe('string'); - expect(dividend.label.length).toBeGreaterThan(0); - - expect(isValidNonNegativeNumber(dividend.adjDividend)).toBe(true); - expect(isValidNonNegativeNumber(dividend.dividend)).toBe(true); - - expect(typeof dividend.symbol).toBe('string'); - expect(dividend.symbol.length).toBeGreaterThan(0); - - // Date fields can be string or object (API inconsistency) - expect( - typeof dividend.recordDate === 'string' || typeof dividend.recordDate === 'object', - ).toBe(true); - expect( - typeof dividend.paymentDate === 'string' || typeof dividend.paymentDate === 'object', - ).toBe(true); - expect( - typeof dividend.declarationDate === 'string' || - typeof dividend.declarationDate === 'object', - ).toBe(true); - - // Validate business logic - expect(dividend.adjDividend).toBeGreaterThanOrEqual(dividend.dividend); - } - }, - API_TIMEOUT, - ); + it('should get dividends calendar using /stock_dividend_calendar endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'KO', date: '2024-01-15' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getDividendsCalendar({ + from: '2024-01-01', + to: '2024-01-31', + }); + + expect(mockClient.get).toHaveBeenCalledWith('/stock_dividend_calendar', 'v3', { + from: '2024-01-01', + to: '2024-01-31', + }); + expect(result).toEqual(mockResponse); + }); + + it('should get dividends calendar without params', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getDividendsCalendar(); + + expect(mockClient.get).toHaveBeenCalledWith('/stock_dividend_calendar', 'v3', {}); + expect(result).toEqual(mockResponse); + }); }); describe('getEconomicsCalendar', () => { - it( - 'should fetch economics calendar and validate structure', - async () => { - if (shouldSkipTests()) { - console.log('Skipping economics calendar test - no API key available'); - return; - } - - const result = - testDataCache.economics || - (await fmp.calendar.getEconomicsCalendar({ - from: TEST_DATE_RANGES.RECENT.from, - to: TEST_DATE_RANGES.RECENT.to, - })); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - // Validate data structure if data exists - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const economic = getFirstItem(result.data) as EconomicsCalendar; - - // Required properties - expect(economic).toHaveProperty('date'); - expect(economic).toHaveProperty('country'); - expect(economic).toHaveProperty('event'); - expect(economic).toHaveProperty('currency'); - expect(economic).toHaveProperty('previous'); - expect(economic).toHaveProperty('actual'); - expect(economic).toHaveProperty('change'); - expect(economic).toHaveProperty('impact'); - expect(economic).toHaveProperty('changePercentage'); - expect(economic).toHaveProperty('unit'); - - // Validate data types - expect(typeof economic.date).toBe('string'); - expect(isValidDateFormat(economic.date)).toBe(true); - - expect(typeof economic.country).toBe('string'); - expect(economic.country.length).toBeGreaterThan(0); - - expect(typeof economic.event).toBe('string'); - expect(economic.event.length).toBeGreaterThan(0); - - expect(typeof economic.currency).toBe('string'); - expect(economic.currency.length).toBeGreaterThan(0); - - // These fields can be null/undefined, so check if they exist before validating - expect(economic.previous === null || isValidNumber(economic.previous)).toBe(true); - expect(economic.actual === null || isValidNumber(economic.actual)).toBe(true); - expect(economic.change === null || isValidNumber(economic.change)).toBe(true); - expect( - economic.changePercentage === null || isValidNumber(economic.changePercentage), - ).toBe(true); - - expect(typeof economic.impact).toBe('string'); - expect(['High', 'Medium', 'Low'].includes(economic.impact)).toBe(true); - - expect(economic.unit === null || typeof economic.unit === 'string').toBe(true); - expect(economic.estimate === null || isValidNumber(economic.estimate)).toBe(true); - } - }, - API_TIMEOUT, - ); - - it( - 'should validate economics calendar impact levels', - async () => { - if (shouldSkipTests()) { - console.log('Skipping economics calendar impact test - no API key available'); - return; - } - - const result = - testDataCache.economics || - (await fmp.calendar.getEconomicsCalendar({ - from: '2024-01-15', - to: '2024-01-21', - })); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const validImpacts = ['High', 'Medium', 'Low']; - - // Check first 5 items for impact validation (reduced from 10) - const itemsToCheck = Math.min(5, result.data.length); - - for (let i = 0; i < itemsToCheck; i++) { - const economic = result.data[i] as EconomicsCalendar; - expect(validImpacts).toContain(economic.impact); - } - } - }, - FAST_TIMEOUT, - ); + it('should get economics calendar using /economic_calendar endpoint', async () => { + const mockResponse = { + success: true, + data: [{ event: 'CPI', date: '2024-01-15' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getEconomicsCalendar({ + from: '2024-01-01', + to: '2024-01-31', + }); + + expect(mockClient.get).toHaveBeenCalledWith('/economic_calendar', 'v3', { + from: '2024-01-01', + to: '2024-01-31', + }); + expect(result).toEqual(mockResponse); + }); + + it('should get economics calendar without params', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getEconomicsCalendar(); + + expect(mockClient.get).toHaveBeenCalledWith('/economic_calendar', 'v3', {}); + expect(result).toEqual(mockResponse); + }); }); describe('getIPOCalendar', () => { - it( - 'should fetch IPO calendar and validate structure', - async () => { - if (shouldSkipTests()) { - console.log('Skipping IPO calendar test - no API key available'); - return; - } - - const result = - testDataCache.ipo || - (await fmp.calendar.getIPOCalendar({ - from: '2024-01-15', - to: '2024-01-21', - })); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - // Validate data structure if data exists - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const ipo = getFirstItem(result.data) as IPOCalendar; - - // Required properties - expect(ipo).toHaveProperty('date'); - expect(ipo).toHaveProperty('company'); - expect(ipo).toHaveProperty('symbol'); - expect(ipo).toHaveProperty('exchange'); - expect(ipo).toHaveProperty('actions'); - expect(ipo).toHaveProperty('shares'); - expect(ipo).toHaveProperty('priceRange'); - expect(ipo).toHaveProperty('marketCap'); - - // Validate data types - expect(typeof ipo.date).toBe('string'); - expect(isValidDateFormat(ipo.date)).toBe(true); - - expect(typeof ipo.company).toBe('string'); - expect(ipo.company.length).toBeGreaterThan(0); - - expect(typeof ipo.symbol).toBe('string'); - expect(ipo.symbol.length).toBeGreaterThan(0); - - expect(typeof ipo.exchange).toBe('string'); - expect(ipo.exchange.length).toBeGreaterThan(0); - - expect(typeof ipo.actions).toBe('string'); - expect(ipo.actions.length).toBeGreaterThan(0); - - // priceRange can be object or string - expect(typeof ipo.priceRange === 'string' || typeof ipo.priceRange === 'object').toBe( - true, - ); - - // shares and marketCap can be null - expect(ipo.shares === null || isValidPositiveNumber(ipo.shares)).toBe(true); - expect(ipo.marketCap === null || isValidPositiveNumber(ipo.marketCap)).toBe(true); - } - }, - API_TIMEOUT, - ); + it('should get IPO calendar using /ipo_calendar endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'NEW', date: '2024-01-15' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getIPOCalendar({ + from: '2024-01-01', + to: '2024-01-31', + }); + + expect(mockClient.get).toHaveBeenCalledWith('/ipo_calendar', 'v3', { + from: '2024-01-01', + to: '2024-01-31', + }); + expect(result).toEqual(mockResponse); + }); + + it('should get IPO calendar without params', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getIPOCalendar(); + + expect(mockClient.get).toHaveBeenCalledWith('/ipo_calendar', 'v3', {}); + expect(result).toEqual(mockResponse); + }); }); describe('getSplitsCalendar', () => { - it( - 'should fetch splits calendar and validate structure', - async () => { - if (shouldSkipTests()) { - console.log('Skipping splits calendar test - no API key available'); - return; - } - - const result = - testDataCache.splits || - (await fmp.calendar.getSplitsCalendar({ - from: '2024-01-15', - to: '2024-01-21', - })); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - // Validate data structure if data exists - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const split = getFirstItem(result.data) as SplitsCalendar; - - // Required properties - expect(split).toHaveProperty('date'); - expect(split).toHaveProperty('label'); - expect(split).toHaveProperty('symbol'); - expect(split).toHaveProperty('numerator'); - expect(split).toHaveProperty('denominator'); - - // Validate data types - expect(typeof split.date).toBe('string'); - expect(isValidDateFormat(split.date)).toBe(true); - - expect(typeof split.label).toBe('string'); - expect(split.label.length).toBeGreaterThan(0); - - expect(typeof split.symbol).toBe('string'); - expect(split.symbol.length).toBeGreaterThan(0); - - expect(isValidPositiveNumber(split.numerator)).toBe(true); - expect(isValidPositiveNumber(split.denominator)).toBe(true); - - // Validate split ratio logic - expect(split.numerator).toBeGreaterThan(0); - expect(split.denominator).toBeGreaterThan(0); - - // Validate split ratio format (e.g., "2:1", "3:2") - const ratioMatch = split.label.match(/(\d+):(\d+)/); - if (ratioMatch) { - const labelNumerator = parseInt(ratioMatch[1]); - const labelDenominator = parseInt(ratioMatch[2]); - expect(split.numerator).toBe(labelNumerator); - expect(split.denominator).toBe(labelDenominator); - } - } - }, - API_TIMEOUT, - ); - }); + it('should get splits calendar using /stock_split_calendar endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', date: '2024-01-15' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getSplitsCalendar({ + from: '2024-01-01', + to: '2024-01-31', + }); + + expect(mockClient.get).toHaveBeenCalledWith('/stock_split_calendar', 'v3', { + from: '2024-01-01', + to: '2024-01-31', + }); + expect(result).toEqual(mockResponse); + }); + + it('should get splits calendar without params', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await calendarEndpoints.getSplitsCalendar(); - describe('Data Consistency and Performance', () => { - it( - 'should maintain consistent data structure across all calendar endpoints', - async () => { - if (shouldSkipTests()) { - console.log('Skipping data consistency test - no API key available'); - return; - } - - // Use cached data if available, otherwise make minimal API calls - const endpoints = [ - () => Promise.resolve(testDataCache.earnings || { success: false, data: [] }), - () => Promise.resolve(testDataCache.earningsConfirmed || { success: false, data: [] }), - () => Promise.resolve(testDataCache.dividends || { success: false, data: [] }), - () => Promise.resolve(testDataCache.economics || { success: false, data: [] }), - () => Promise.resolve(testDataCache.ipo || { success: false, data: [] }), - () => Promise.resolve(testDataCache.splits || { success: false, data: [] }), - ]; - - for (const endpoint of endpoints) { - const result = await endpoint(); - - if (result.success) { - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const firstItem = result.data[0]; - expect(firstItem).toHaveProperty('date'); - expect(typeof firstItem.date).toBe('string'); - expect(isValidDateFormat(firstItem.date)).toBe(true); - } - } - } - }, - FAST_TIMEOUT, - ); - - it( - 'should complete calendar requests within reasonable time', - async () => { - if (shouldSkipTests()) { - console.log('Skipping performance test - no API key available'); - return; - } - - const startTime = Date.now(); - - const result = await fmp.calendar.getEarningsCalendar({ - from: '2024-01-15', - to: '2024-01-21', - }); - - const endTime = Date.now(); - const duration = endTime - startTime; - - expect(result.success).toBe(true); - expect(duration).toBeLessThan(API_TIMEOUT); - expect(duration).toBeLessThan(10000); // Should complete within 10 seconds - }, - API_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/stock_split_calendar', 'v3', {}); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/company.test.ts b/packages/api/src/__tests__/endpoints/company.test.ts index 6d60198..e053052 100644 --- a/packages/api/src/__tests__/endpoints/company.test.ts +++ b/packages/api/src/__tests__/endpoints/company.test.ts @@ -1,295 +1,168 @@ -import { FMP } from '../../fmp'; -import { - shouldSkipTests, - createTestClient, - API_TIMEOUT, - FAST_TIMEOUT, - TEST_SYMBOLS, -} from '../utils/test-setup'; +import { CompanyEndpoints } from '../../endpoints/company'; +import { FMPClient } from '../../client'; -describe('Company Endpoints', () => { - let fmp: FMP; +// Mock the FMPClient +jest.mock('../../client'); - beforeAll(() => { - if (shouldSkipTests()) { - console.log('Skipping company tests - no API key available'); - return; - } - fmp = createTestClient(); +describe('CompanyEndpoints', () => { + let companyEndpoints: CompanyEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + companyEndpoints = new CompanyEndpoints(mockClient); }); describe('getCompanyProfile', () => { - it( - 'should fetch company profile for AAPL', - async () => { - if (shouldSkipTests()) { - console.log('Skipping company profile test - no API key available'); - return; - } - const result = await fmp.company.getCompanyProfile(TEST_SYMBOLS.STOCK); - - expect(result.success).toBe(true); - expect(result.data).toBeTruthy(); - - if (result.data) { - const profile = result.data; - expect(profile.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(profile.companyName).toBeDefined(); - expect(profile.price).toBeGreaterThan(0); - expect(profile.marketCap).toBeGreaterThan(0); - expect(profile.industry).toBeDefined(); - expect(profile.sector).toBeDefined(); - expect(profile.country).toBeDefined(); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should handle invalid symbol gracefully', - async () => { - if (shouldSkipTests()) { - console.log('Skipping invalid symbol test - no API key available'); - return; - } - const result = await fmp.company.getCompanyProfile('INVALID_SYMBOL_12345'); - - expect(Object.keys(result.data || {}).length === 0 || result.success === false).toBe(true); - }, - FAST_TIMEOUT, - ); + it('should get company profile using /profile stable endpoint with symbol param', async () => { + const mockResponse = { + success: true, + data: { symbol: 'AAPL', companyName: 'Apple Inc.' }, + error: null, + status: 200, + }; + mockClient.getSingle.mockResolvedValue(mockResponse); + + const result = await companyEndpoints.getCompanyProfile('AAPL'); + + expect(mockClient.getSingle).toHaveBeenCalledWith('/profile', 'stable', { symbol: 'AAPL' }); + expect(result).toEqual(mockResponse); + }); }); describe('getExecutiveCompensation', () => { - it( - 'should fetch executive compensation for AAPL', - async () => { - if (shouldSkipTests()) { - console.log('Skipping executive compensation test - no API key available'); - return; - } - const result = await fmp.company.getExecutiveCompensation(TEST_SYMBOLS.STOCK); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - expect(result.data.length).toBeGreaterThan(0); - const compensation = result.data[0]; - expect(compensation.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(compensation.cik).toBeDefined(); - expect(compensation.companyName).toBeDefined(); - expect(compensation.nameAndPosition).toBeDefined(); - expect(compensation.year).toBeGreaterThan(0); - expect(compensation.total).toBeGreaterThan(0); - } - }, - API_TIMEOUT, - ); + it('should get executive compensation using /governance-executive-compensation stable endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', nameAndPosition: 'Tim Cook' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await companyEndpoints.getExecutiveCompensation('AAPL'); + + expect(mockClient.get).toHaveBeenCalledWith('/governance-executive-compensation', 'stable', { + symbol: 'AAPL', + }); + expect(result).toEqual(mockResponse); + }); }); describe('getCompanyNotes', () => { - it( - 'should fetch company notes for AAPL', - async () => { - if (shouldSkipTests()) { - console.log('Skipping company notes test - no API key available'); - return; - } - const result = await fmp.company.getCompanyNotes(TEST_SYMBOLS.STOCK); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - expect(result.data.length).toBeGreaterThan(0); - const note = result.data[0]; - expect(note.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(note.cik).toBeDefined(); - expect(note.title).toBeDefined(); - expect(note.exchange).toBeDefined(); - } - }, - API_TIMEOUT, - ); + it('should get company notes using /company-notes stable endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', title: 'Note' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await companyEndpoints.getCompanyNotes('AAPL'); + + expect(mockClient.get).toHaveBeenCalledWith('/company-notes', 'stable', { symbol: 'AAPL' }); + expect(result).toEqual(mockResponse); + }); }); describe('getHistoricalEmployeeCount', () => { - it( - 'should fetch historical employee count for AAPL', - async () => { - if (shouldSkipTests()) { - console.log('Skipping historical employee count test - no API key available'); - return; - } - const result = await fmp.company.getHistoricalEmployeeCount(TEST_SYMBOLS.STOCK); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - expect(result.data.length).toBeGreaterThan(0); - const employeeData = result.data[0]; - expect(employeeData.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(employeeData.cik).toBeDefined(); - expect(employeeData.companyName).toBeDefined(); - expect(employeeData.employeeCount).toBeGreaterThan(0); - expect(employeeData.filingDate).toBeDefined(); - expect(employeeData.formType).toBeDefined(); - } - }, - API_TIMEOUT, - ); + it('should get historical employee count using /historical-employee-count stable endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', employeeCount: 150000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await companyEndpoints.getHistoricalEmployeeCount('AAPL'); + + expect(mockClient.get).toHaveBeenCalledWith('/historical-employee-count', 'stable', { + symbol: 'AAPL', + }); + expect(result).toEqual(mockResponse); + }); }); describe('getSharesFloat', () => { - it( - 'should fetch shares float for AAPL', - async () => { - if (shouldSkipTests()) { - console.log('Skipping shares float test - no API key available'); - return; - } - const result = await fmp.company.getSharesFloat(TEST_SYMBOLS.STOCK); - - expect(result.success).toBe(true); - expect(result.data).toBeTruthy(); - - if (result.data) { - const sharesFloat = Array.isArray(result.data) ? result.data[0] : result.data; - expect(sharesFloat.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(sharesFloat.freeFloat).toBeGreaterThan(0); - expect(sharesFloat.floatShares).toBeGreaterThan(0); - expect(sharesFloat.outstandingShares).toBeGreaterThan(0); - expect(sharesFloat.source).toBeDefined(); - expect(sharesFloat.date).toBeDefined(); - } - }, - FAST_TIMEOUT, - ); + it('should get shares float using /shares-float stable endpoint', async () => { + const mockResponse = { + success: true, + data: { symbol: 'AAPL', floatShares: 1000000 }, + error: null, + status: 200, + }; + mockClient.getSingle.mockResolvedValue(mockResponse); + + const result = await companyEndpoints.getSharesFloat('AAPL'); + + expect(mockClient.getSingle).toHaveBeenCalledWith('/shares-float', 'stable', { + symbol: 'AAPL', + }); + expect(result).toEqual(mockResponse); + }); }); describe('getHistoricalSharesFloat', () => { - it( - 'should fetch historical shares float for AAPL', - async () => { - if (shouldSkipTests()) { - console.log('Skipping historical shares float test - no API key available'); - return; - } - const result = await fmp.company.getHistoricalSharesFloat(TEST_SYMBOLS.STOCK); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - expect(result.data.length).toBeGreaterThan(0); - const sharesFloat = result.data[0]; - expect(sharesFloat.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(sharesFloat.freeFloat).toBeGreaterThan(0); - expect(sharesFloat.floatShares).toBeDefined(); - expect(sharesFloat.floatShares.length).toBeGreaterThan(0); - expect(sharesFloat.outstandingShares).toBeDefined(); - expect(sharesFloat.outstandingShares.length).toBeGreaterThan(0); - expect(sharesFloat.source).toBeDefined(); - expect(sharesFloat.date).toBeDefined(); - } - }, - API_TIMEOUT, - ); + it('should get historical shares float using /historical/shares_float v4 endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', date: '2024-01-01', floatShares: '1000000' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await companyEndpoints.getHistoricalSharesFloat('AAPL'); + + expect(mockClient.get).toHaveBeenCalledWith('/historical/shares_float', 'v4', { + symbol: 'AAPL', + }); + expect(result).toEqual(mockResponse); + }); }); describe('getEarningsCallTranscript', () => { - it( - 'should fetch earnings call transcript for AAPL', - async () => { - if (shouldSkipTests()) { - console.log('Skipping earnings call transcript test - no API key available'); - return; - } - const result = await fmp.company.getEarningsCallTranscript({ - symbol: TEST_SYMBOLS.STOCK, - year: 2024, - quarter: 1, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeTruthy(); - - if (result.data) { - const transcript = result.data; - expect(transcript.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(transcript.quarter).toBe(1); - expect(transcript.year).toBe(2024); - expect(transcript.date).toBeDefined(); - expect(transcript.content).toBeDefined(); - expect(transcript.content.length).toBeGreaterThan(0); - } - }, - API_TIMEOUT, - ); - - it( - 'should handle non-existent transcript gracefully', - async () => { - if (shouldSkipTests()) { - console.log('Skipping non-existent transcript test - no API key available'); - return; - } - const result = await fmp.company.getEarningsCallTranscript({ - symbol: TEST_SYMBOLS.STOCK, - year: 1900, - quarter: 1, - }); - - // Should either return empty array or handle gracefully - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - }, - FAST_TIMEOUT, - ); + it('should get earnings call transcript using /earning_call_transcript/{symbol} v3 endpoint', async () => { + const mockResponse = { + success: true, + data: { symbol: 'AAPL', content: 'transcript' }, + error: null, + status: 200, + }; + mockClient.getSingle.mockResolvedValue(mockResponse); + + const result = await companyEndpoints.getEarningsCallTranscript({ + symbol: 'AAPL', + year: 2024, + quarter: 1, + }); + + expect(mockClient.getSingle).toHaveBeenCalledWith('/earning_call_transcript/AAPL', 'v3', { + year: 2024, + quarter: 1, + }); + expect(result).toEqual(mockResponse); + }); }); describe('getCompanyTranscriptData', () => { - it( - 'should fetch company transcript data for AAPL', - async () => { - if (shouldSkipTests()) { - console.log('Skipping company transcript data test - no API key available'); - return; - } - const result = await fmp.company.getCompanyTranscriptData(TEST_SYMBOLS.STOCK); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - expect(result.data.length).toBeGreaterThan(0); - const transcriptData = result.data[0]; - expect(Array.isArray(transcriptData)).toBe(true); - expect(transcriptData.length).toBe(3); - expect(typeof transcriptData[0]).toBe('number'); // year - expect(typeof transcriptData[1]).toBe('number'); // quarter - expect(typeof transcriptData[2]).toBe('string'); // date - } - }, - API_TIMEOUT, - ); - - it( - 'should handle invalid symbol gracefully', - async () => { - if (shouldSkipTests()) { - console.log('Skipping invalid symbol transcript data test - no API key available'); - return; - } - const result = await fmp.company.getCompanyTranscriptData('INVALID_SYMBOL_12345'); - - // Should either return empty array or handle gracefully - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - }, - FAST_TIMEOUT, - ); + it('should get transcript data using /earning_call_transcript v4 endpoint with symbol param', async () => { + const mockResponse = { + success: true, + data: { quarter: 1, year: 2024 }, + error: null, + status: 200, + }; + mockClient.getSingle.mockResolvedValue(mockResponse); + + const result = await companyEndpoints.getCompanyTranscriptData('AAPL'); + + expect(mockClient.getSingle).toHaveBeenCalledWith('/earning_call_transcript', 'v4', { + symbol: 'AAPL', + }); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/economic.test.ts b/packages/api/src/__tests__/endpoints/economic.test.ts index 5b0a28b..5618d7c 100644 --- a/packages/api/src/__tests__/endpoints/economic.test.ts +++ b/packages/api/src/__tests__/endpoints/economic.test.ts @@ -1,120 +1,97 @@ -import { FMP } from '../../fmp'; -import { API_KEY, isCI } from '../utils/test-setup'; +import { EconomicEndpoints } from '../../endpoints/economic'; +import { FMPClient } from '../../client'; -// Helper function to safely access data that could be an array or single object -function getFirstItem(data: T | T[]): T { - return Array.isArray(data) ? data[0] : data; -} +// Mock the FMPClient +jest.mock('../../client'); -describe('Economic Endpoints', () => { - if (!API_KEY || isCI) { - it('should skip tests when no API key is provided or running in CI', () => { - expect(true).toBe(true); - }); - return; - } - - let fmp: FMP; +describe('EconomicEndpoints', () => { + let economicEndpoints: EconomicEndpoints; + let mockClient: jest.Mocked; - beforeAll(() => { - if (!API_KEY) { - throw new Error('FMP_API_KEY is required for testing'); - } - fmp = new FMP({ apiKey: API_KEY }); + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + economicEndpoints = new EconomicEndpoints(mockClient); }); describe('getTreasuryRates', () => { - it('should fetch treasury rates', async () => { - const result = await fmp.economic.getTreasuryRates({ - from: '2024-01-01', - to: '2024-01-31', + it('should get treasury rates with from and to using /treasury-rates endpoint', async () => { + const mockResponse = { + success: true, + data: [{ date: '2023-12-31', month1: 5.4, year1: 4.8, year10: 3.88 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await economicEndpoints.getTreasuryRates({ + from: '2023-01-01', + to: '2023-12-31', }); - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); + expect(mockClient.get).toHaveBeenCalledWith('/treasury-rates', 'stable', { + from: '2023-01-01', + to: '2023-12-31', + }); + expect(result).toEqual(mockResponse); + }); + + it('should get treasury rates without date range', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const rate = getFirstItem(result.data); - expect(rate.date).toBeDefined(); - expect(rate.month1).toBeDefined(); - expect(rate.year1).toBeDefined(); - expect(rate.year10).toBeDefined(); - } - }, 15000); + const result = await economicEndpoints.getTreasuryRates({}); + + expect(mockClient.get).toHaveBeenCalledWith('/treasury-rates', 'stable', {}); + expect(result).toEqual(mockResponse); + }); }); describe('getEconomicIndicators', () => { - it('should fetch GDP data', async () => { - const result = await fmp.economic.getEconomicIndicators({ + it('should get economic indicators with name, from, and to using /economic-indicators endpoint', async () => { + const mockResponse = { + success: true, + data: [{ date: '2023-12-31', value: 27000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await economicEndpoints.getEconomicIndicators({ name: 'GDP', - from: '2020-01-01', - to: '2024-01-31', + from: '2023-01-01', + to: '2023-12-31', }); - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const gdp = getFirstItem(result.data); - expect(gdp.date).toBeDefined(); - expect(gdp.value).toBeDefined(); - } - }, 15000); - - it('should fetch CPI data', async () => { - const result = await fmp.economic.getEconomicIndicators({ - name: 'CPI', - from: '2024-01-01', - to: '2024-01-31', + expect(mockClient.get).toHaveBeenCalledWith('/economic-indicators', 'stable', { + name: 'GDP', + from: '2023-01-01', + to: '2023-12-31', }); + expect(result).toEqual(mockResponse); + }); - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const cpi = getFirstItem(result.data); - expect(cpi.date).toBeDefined(); - expect(cpi.value).toBeDefined(); - } - }, 15000); + it('should get economic indicators with only name', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - it('should fetch federal funds rate', async () => { - const result = await fmp.economic.getEconomicIndicators({ - name: 'federalFunds', - from: '2024-01-01', - to: '2024-01-31', + const result = await economicEndpoints.getEconomicIndicators({ + name: 'unemploymentRate', }); - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const rate = getFirstItem(result.data); - expect(rate.date).toBeDefined(); - expect(rate.value).toBeDefined(); - } - }, 15000); - - it('should fetch unemployment data', async () => { - const result = await fmp.economic.getEconomicIndicators({ + expect(mockClient.get).toHaveBeenCalledWith('/economic-indicators', 'stable', { name: 'unemploymentRate', - from: '2024-01-01', - to: '2024-01-31', }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const unemployment = getFirstItem(result.data); - expect(unemployment.date).toBeDefined(); - expect(unemployment.value).toBeDefined(); - } - }, 15000); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/etf.test.ts b/packages/api/src/__tests__/endpoints/etf.test.ts index 3c5189c..fde192a 100644 --- a/packages/api/src/__tests__/endpoints/etf.test.ts +++ b/packages/api/src/__tests__/endpoints/etf.test.ts @@ -1,156 +1,145 @@ -import { FMP } from '../../fmp'; -import { createTestClient, shouldSkipTests } from '../utils/test-setup'; - -describe('ETF Endpoints', () => { - let fmp: FMP; - - beforeAll(() => { - if (shouldSkipTests()) { - console.log('Skipping ETF tests - running in CI environment'); - return; - } - fmp = createTestClient(); +import { ETFEndpoints } from '../../endpoints/etf'; +import { FMPClient } from '../../client'; + +// Mock the FMPClient +jest.mock('../../client'); + +describe('ETFEndpoints', () => { + let etfEndpoints: ETFEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + etfEndpoints = new ETFEndpoints(mockClient); }); - describe('getProfile', () => { - it('should fetch ETF profile', async () => { - if (shouldSkipTests()) { - console.log('Skipping ETF profile test - running in CI environment'); - return; - } - - const result = await fmp.etf.getProfile('SPY'); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data) { - // getProfile uses getSingle, so it returns a single object, not an array - expect(result.data.symbol).toBeDefined(); - expect(result.data.name).toBeDefined(); - expect(result.data.expenseRatio).toBeDefined(); - } - }, 10000); + describe('getHoldingDates', () => { + it('should get holding dates using /etf-holdings/portfolio-date endpoint', async () => { + const mockResponse = { + success: true, + data: [{ date: '2024-01-15' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await etfEndpoints.getHoldingDates('SPY'); + + expect(mockClient.get).toHaveBeenCalledWith('/etf-holdings/portfolio-date', 'v4', { + symbol: 'SPY', + }); + expect(result).toEqual(mockResponse); + }); }); describe('getHoldings', () => { - it('should fetch ETF holdings', async () => { - if (shouldSkipTests()) { - console.log('Skipping ETF holdings test - running in CI environment'); - return; - } - - // First get holding dates to get a valid date - const datesResult = await fmp.etf.getHoldingDates('SPY'); - if (!datesResult.success || !datesResult.data || datesResult.data.length === 0) { - console.log('No holding dates available for SPY, skipping holdings test'); - return; - } - - const date = datesResult.data[0].date; - const result = await fmp.etf.getHoldings({ symbol: 'SPY', date }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && result.data.length > 0) { - const holding = result.data[0]; - // ETFHolding has these properties - expect(holding.symbol).toBeDefined(); - expect(holding.name).toBeDefined(); - expect(holding.cik).toBeDefined(); - expect(holding.balance).toBeDefined(); - } - }, 10000); + it('should get holdings using /etf-holdings endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', name: 'Apple Inc.', cik: '0000320193', balance: 1000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await etfEndpoints.getHoldings({ symbol: 'SPY', date: '2024-01-15' }); + + expect(mockClient.get).toHaveBeenCalledWith('/etf-holdings', 'v4', { + symbol: 'SPY', + date: '2024-01-15', + }); + expect(result).toEqual(mockResponse); + }); }); describe('getHolder', () => { - it('should fetch ETF holder', async () => { - if (shouldSkipTests()) { - console.log('Skipping ETF holder test - running in CI environment'); - return; - } - - const result = await fmp.etf.getHolder('SPY'); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && result.data.length > 0) { - const holder = result.data[0]; - expect(holder.asset).toBeDefined(); - expect(holder.sharesNumber).toBeDefined(); - expect(holder.weightPercentage).toBeDefined(); - } - }, 10000); + it('should get holder using /etf-holder/{symbol} endpoint', async () => { + const mockResponse = { + success: true, + data: [{ asset: 'AAPL', sharesNumber: 1000, weightPercentage: 7.1 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await etfEndpoints.getHolder('SPY'); + + expect(mockClient.get).toHaveBeenCalledWith('/etf-holder/SPY', 'v3'); + expect(result).toEqual(mockResponse); + }); + }); + + describe('getProfile', () => { + it('should get profile using /etf/info endpoint via getSingle', async () => { + const mockResponse = { + success: true, + data: { symbol: 'SPY', name: 'SPDR S&P 500 ETF Trust', expenseRatio: 0.0945 }, + error: null, + status: 200, + }; + mockClient.getSingle.mockResolvedValue(mockResponse); + + const result = await etfEndpoints.getProfile('SPY'); + + expect(mockClient.getSingle).toHaveBeenCalledWith('/etf/info', 'stable', { symbol: 'SPY' }); + expect(result).toEqual(mockResponse); + }); }); describe('getSectorWeighting', () => { - it('should fetch ETF sector weighting', async () => { - if (shouldSkipTests()) { - console.log('Skipping ETF sector weighting test - running in CI environment'); - return; - } - - const result = await fmp.etf.getSectorWeighting('SPY'); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (Array.isArray(result.data) && result.data.length > 0) { - const weighting = result.data[0]; - expect(weighting.sector).toBeDefined(); - expect(weighting.weightPercentage).toBeDefined(); - } else { - // Accept empty, undefined, or null as valid for this test - expect(Array.isArray(result.data) || result.data == null).toBe(true); - } - }, 10000); + it('should get sector weighting using /etf/sector-weightings endpoint', async () => { + const mockResponse = { + success: true, + data: [{ sector: 'Technology', weightPercentage: 30 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await etfEndpoints.getSectorWeighting('SPY'); + + expect(mockClient.get).toHaveBeenCalledWith('/etf/sector-weightings', 'stable', { + symbol: 'SPY', + }); + expect(result).toEqual(mockResponse); + }); }); describe('getCountryWeighting', () => { - it('should fetch ETF country weighting', async () => { - if (shouldSkipTests()) { - console.log('Skipping ETF country weighting test - running in CI environment'); - return; - } - - const result = await fmp.etf.getCountryWeighting('SPY'); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (Array.isArray(result.data) && result.data.length > 0) { - const weighting = result.data[0]; - expect(weighting.country).toBeDefined(); - expect(weighting.weightPercentage).toBeDefined(); - } else { - // Accept empty, undefined, or null as valid for this test - expect(Array.isArray(result.data) || result.data == null).toBe(true); - } - }, 10000); + it('should get country weighting using /etf/country-weightings endpoint', async () => { + const mockResponse = { + success: true, + data: [{ country: 'United States', weightPercentage: 98 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await etfEndpoints.getCountryWeighting('SPY'); + + expect(mockClient.get).toHaveBeenCalledWith('/etf/country-weightings', 'stable', { + symbol: 'SPY', + }); + expect(result).toEqual(mockResponse); + }); }); describe('getStockExposure', () => { - it('should fetch ETF stock exposure', async () => { - if (shouldSkipTests()) { - console.log('Skipping ETF stock exposure test - running in CI environment'); - return; - } - - // getStockExposure takes a stock symbol to find which ETFs hold that stock - const result = await fmp.etf.getStockExposure('AAPL'); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && result.data.length > 0) { - const exposure = result.data[0]; - expect(exposure.etfSymbol).toBeDefined(); - expect(exposure.assetExposure).toBeDefined(); - expect(exposure.sharesNumber).toBeDefined(); - expect(exposure.weightPercentage).toBeDefined(); - } - }, 10000); + it('should get stock exposure using /etf-stock-exposure/{symbol} endpoint', async () => { + const mockResponse = { + success: true, + data: [ + { etfSymbol: 'SPY', assetExposure: 'AAPL', sharesNumber: 1000, weightPercentage: 7.1 }, + ], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await etfEndpoints.getStockExposure('AAPL'); + + expect(mockClient.get).toHaveBeenCalledWith('/etf-stock-exposure/AAPL', 'v3'); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/financial.test.ts b/packages/api/src/__tests__/endpoints/financial.test.ts index 17a5e83..9f0a51e 100644 --- a/packages/api/src/__tests__/endpoints/financial.test.ts +++ b/packages/api/src/__tests__/endpoints/financial.test.ts @@ -1,838 +1,286 @@ -import { FMP } from '../../fmp'; -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 { - return Array.isArray(data) ? data[0] : data; -} - -// Helper function to validate financial statement base fields for stable API -function validateFinancialStatementBase(statement: any, symbol: string) { - expect(statement.symbol).toBe(symbol); - expect(statement.date).toBeDefined(); - expect(typeof statement.date).toBe('string'); - - // Stable API fields - check if they exist and validate type - if (statement.reportedCurrency !== undefined) { - expect(typeof statement.reportedCurrency).toBe('string'); - } - if (statement.cik !== undefined) { - expect(typeof statement.cik).toBe('string'); - } - if (statement.filingDate !== undefined) { - expect(statement.filingDate).toBeDefined(); - } - if (statement.acceptedDate !== undefined) { - expect(statement.acceptedDate).toBeDefined(); - } - if (statement.fiscalYear !== undefined) { - expect(statement.fiscalYear).toBeDefined(); - } - if (statement.period !== undefined) { - expect(statement.period).toBeDefined(); - } -} - -// Helper function to validate growth statement base fields for stable API -function validateGrowthStatementBase(statement: any, symbol: string) { - expect(statement.symbol).toBe(symbol); - expect(statement.date).toBeDefined(); - expect(typeof statement.date).toBe('string'); - - // Stable API might use different field names - if (statement.fiscalYear !== undefined) { - expect(statement.fiscalYear).toBeDefined(); - } - if (statement.calendarYear !== undefined) { - expect(statement.calendarYear).toBeDefined(); - } - if (statement.period !== undefined) { - expect(statement.period).toBeDefined(); - } -} - -// Helper function to validate numeric fields are numbers (not null/undefined) -function validateNumericField(value: any, _fieldName: string) { - expect(value).toBeDefined(); - expect(typeof value).toBe('number'); - expect(isNaN(value)).toBe(false); -} - -// Helper function to validate optional numeric fields -function validateOptionalNumericField(value: any, _fieldName: string) { - if (value !== null && value !== undefined) { - expect(typeof value).toBe('number'); - expect(isNaN(value)).toBe(false); - } -} - -// 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(async () => { - if (shouldSkipTests()) { - console.log('Skipping financial tests - no API key available'); - return; - } - 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); +import { FinancialEndpoints } from '../../endpoints/financial'; +import { FMPClient } from '../../client'; + +// Mock the FMPClient +jest.mock('../../client'); + +describe('FinancialEndpoints', () => { + let financialEndpoints: FinancialEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + financialEndpoints = new FinancialEndpoints(mockClient); + }); describe('getIncomeStatement', () => { - 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, - ); + it('should get income statement with default period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', revenue: 1000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getIncomeStatement({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/income-statement?symbol=AAPL&period=annual&limit=5', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); + + it('should get income statement with explicit period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'MSFT', revenue: 2000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getIncomeStatement({ + symbol: 'MSFT', + period: 'quarter', + limit: 8, + }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/income-statement?symbol=MSFT&period=quarter&limit=8', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getBalanceSheet', () => { - 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, - ); + it('should get balance sheet with default period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', totalAssets: 1000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getBalanceSheet({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/balance-sheet-statement?symbol=AAPL&period=annual&limit=5', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getCashFlowStatement', () => { - 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, - ); + it('should get cash flow statement with default period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', operatingCashFlow: 1000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getCashFlowStatement({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/cash-flow-statement?symbol=AAPL&period=annual&limit=5', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getKeyMetrics', () => { - 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, - ); + it('should get key metrics with default period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', peRatio: 25 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getKeyMetrics({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/key-metrics?symbol=AAPL&period=annual&limit=5', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getFinancialRatios', () => { - 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, - ); + it('should get financial ratios with default period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', priceToBookRatio: 5 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getFinancialRatios({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/ratios?symbol=AAPL&period=annual&limit=5', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getEnterpriseValue', () => { - 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, - ); + it('should get enterprise value with default period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', enterpriseValue: 1000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getEnterpriseValue({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/enterprise-values?symbol=AAPL&period=annual&limit=5', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getCashflowGrowth', () => { - 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, - ); + it('should get cash flow growth with default period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', operatingCashFlowGrowth: 0.1 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getCashflowGrowth({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/cash-flow-statement-growth?symbol=AAPL&period=annual&limit=5', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getIncomeGrowth', () => { - 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, - ); + it('should get income growth with default period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', revenueGrowth: 0.1 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getIncomeGrowth({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/income-statement-growth?symbol=AAPL&period=annual&limit=5', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getBalanceSheetGrowth', () => { - 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, - ); + it('should get balance sheet growth with default period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', totalAssetsGrowth: 0.1 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getBalanceSheetGrowth({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/balance-sheet-statement-growth?symbol=AAPL&period=annual&limit=5', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getFinancialGrowth', () => { - 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, - ); + it('should get financial growth with default period and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', epsGrowth: 0.1 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getFinancialGrowth({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith( + '/financial-growth?symbol=AAPL&period=annual&limit=5', + 'stable', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getEarningsHistorical', () => { - 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, - ); + it('should get earnings historical with default limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', epsActual: 1.5 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getEarningsHistorical({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith('/earnings?symbol=AAPL&limit=5', 'stable'); + expect(result).toEqual(mockResponse); + }); + + it('should get earnings historical with explicit limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'MSFT', epsActual: 2.5 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getEarningsHistorical({ symbol: 'MSFT', limit: 10 }); + + expect(mockClient.get).toHaveBeenCalledWith('/earnings?symbol=MSFT&limit=10', 'stable'); + expect(result).toEqual(mockResponse); + }); }); describe('getEarningsSurprises', () => { - it( - 'should fetch earnings surprises for AAPL', - async () => { - if (shouldSkipTests()) { - console.log('Skipping earnings surprises test - no API key available'); - return; - } - - const result = - testDataCache.earningsSurprises || (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'); - } - }, - API_TIMEOUT, - ); - }); + it('should get earnings surprises using /earnings-surprises/{symbol} endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', actualEarningResult: 1.5 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await financialEndpoints.getEarningsSurprises('AAPL'); - 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); - } - }, - API_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/earnings-surprises/AAPL', 'v3'); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/insider.test.ts b/packages/api/src/__tests__/endpoints/insider.test.ts index 2255660..c863253 100644 --- a/packages/api/src/__tests__/endpoints/insider.test.ts +++ b/packages/api/src/__tests__/endpoints/insider.test.ts @@ -1,562 +1,344 @@ -import { FMP } from '../../fmp'; -import { TransactionType } from 'fmp-node-types'; -import { shouldSkipTests, createTestClient, API_TIMEOUT, FAST_TIMEOUT } from '../utils/test-setup'; +import { InsiderEndpoints } from '../../endpoints/insider'; +import { FMPClient } from '../../client'; + +// Mock the FMPClient +jest.mock('../../client'); describe('InsiderEndpoints', () => { - let fmp: FMP; + let endpoints: InsiderEndpoints; + let mockClient: jest.Mocked; beforeEach(() => { - // Create a new FMP instance for each test - fmp = createTestClient(); + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + endpoints = new InsiderEndpoints(mockClient); }); describe('getInsiderTradingRSS', () => { - it( - 'should return insider trading RSS data with pagination', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insider trading RSS test - no API key available'); - return; - } - - const result = await fmp.insider.getInsiderTradingRSS({ page: 0, limit: 5 }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeLessThanOrEqual(5); - - if (result.data && result.data.length > 0) { - const firstTrade = result.data[0]; - expect(firstTrade).toHaveProperty('symbol'); - expect(firstTrade).toHaveProperty('filingDate'); - expect(firstTrade).toHaveProperty('transactionDate'); - expect(firstTrade).toHaveProperty('reportingCik'); - expect(firstTrade).toHaveProperty('companyCik'); - expect(firstTrade).toHaveProperty('transactionType'); - expect(firstTrade).toHaveProperty('reportingName'); - } - }, - API_TIMEOUT, - ); + it('should get RSS feed with default page and limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', reportingName: 'John Doe' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getInsiderTradingRSS({}); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading/latest', 'stable', { + page: 0, + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); + + it('should pass through explicit page and limit', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getInsiderTradingRSS({ page: 2, limit: 5 }); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading/latest', 'stable', { + page: 2, + limit: 5, + }); + expect(result).toEqual(mockResponse); + }); }); describe('searchInsiderTrading', () => { - it( - 'should return insider trading data for a specific symbol', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insider trading search test - no API key available'); - return; - } - - const result = await fmp.insider.searchInsiderTrading({ - symbol: 'AAPL', - page: 0, - limit: 3, - }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeLessThanOrEqual(3); - - if (result.data && result.data.length > 0) { - const firstTrade = result.data[0]; - expect(firstTrade).toHaveProperty('symbol'); - expect(firstTrade).toHaveProperty('filingDate'); - expect(firstTrade).toHaveProperty('transactionDate'); - expect(firstTrade).toHaveProperty('reportingCik'); - expect(firstTrade).toHaveProperty('companyCik'); - expect(firstTrade).toHaveProperty('transactionType'); - expect(firstTrade).toHaveProperty('reportingName'); - expect(firstTrade.symbol).toBe('AAPL'); - } - }, - API_TIMEOUT, - ); - - it( - 'should return insider trading data with pagination only', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insider trading search test - no API key available'); - return; - } - - const result = await fmp.insider.searchInsiderTrading({ - page: 0, - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeLessThanOrEqual(2); - }, - API_TIMEOUT, - ); - - it( - 'should return insider trading data filtered by reporting CIK', - async () => { - if (shouldSkipTests()) { - console.log( - 'Skipping insider trading search by reporting CIK test - no API key available', - ); - return; - } - - const result = await fmp.insider.searchInsiderTrading({ - reportingCik: '0000320193', - page: 0, - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeLessThanOrEqual(2); - }, - API_TIMEOUT, - ); - - it( - 'should return insider trading data filtered by company CIK', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insider trading search by company CIK test - no API key available'); - return; - } - - const result = await fmp.insider.searchInsiderTrading({ - companyCik: '0000320193', - page: 0, - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeLessThanOrEqual(2); - }, - API_TIMEOUT, - ); - - it( - 'should return insider trading data filtered by transaction type', - async () => { - if (shouldSkipTests()) { - console.log( - 'Skipping insider trading search by transaction type test - no API key available', - ); - return; - } - - const result = await fmp.insider.searchInsiderTrading({ - transactionType: 'P-Purchase', - page: 0, - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeLessThanOrEqual(2); - }, - API_TIMEOUT, - ); + it('should search with only defaults when no filters provided', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.searchInsiderTrading({}); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading/search', 'stable', { + page: 0, + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); + + it('should include all provided filters in query params', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.searchInsiderTrading({ + symbol: 'AAPL', + reportingCik: '0000111111', + companyCik: '0000320193', + transactionType: 'P-Purchase', + page: 1, + limit: 25, + }); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading/search', 'stable', { + page: 1, + limit: 25, + symbol: 'AAPL', + reportingCik: '0000111111', + companyCik: '0000320193', + transactionType: 'P-Purchase', + }); + expect(result).toEqual(mockResponse); + }); }); describe('getTransactionTypes', () => { - it( - 'should return array of transaction types', - async () => { - if (shouldSkipTests()) { - console.log('Skipping transaction types test - no API key available'); - return; - } - - const result = await fmp.insider.getTransactionTypes(); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - // Check that we have some expected transaction types - const transactionTypes = result.data!; - const typeStrings = transactionTypes.map((t: any) => t.transactionType); - expect(typeStrings).toContain('P-Purchase'); - expect(typeStrings).toContain('S-Sale'); - }, - FAST_TIMEOUT, - ); + it('should get transaction types', async () => { + const mockResponse = { + success: true, + data: ['P-Purchase', 'S-Sale'], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getTransactionTypes(); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading-transaction-type', 'stable'); + expect(result).toEqual(mockResponse); + }); }); - describe('getInsidersBySymbol (DEPRECATED)', () => { - it( - 'should return insiders data for a symbol (v4 endpoint)', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insiders by symbol test - no API key available'); - return; - } - - const result = await fmp.insider.getInsidersBySymbol({ symbol: 'AAPL' }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const firstInsider = result.data[0]; - expect(firstInsider).toHaveProperty('typeOfOwner'); - expect(firstInsider).toHaveProperty('transactionDate'); - expect(firstInsider).toHaveProperty('owner'); - } - }, - API_TIMEOUT, - ); + describe('getInsidersBySymbol', () => { + it('should get insiders roster by symbol (v4)', async () => { + const mockResponse = { + success: true, + data: [{ owner: 'John Doe', typeOfOwner: 'officer' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getInsidersBySymbol({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-roaster', 'v4', { symbol: 'AAPL' }); + expect(result).toEqual(mockResponse); + }); }); describe('getInsiderTradeStatistics', () => { - it( - 'should return insider trade statistics for a symbol', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insider trade statistics test - no API key available'); - return; - } - - const result = await fmp.insider.getInsiderTradeStatistics({ symbol: 'AAPL' }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const firstStat = result.data[0]; - expect(firstStat).toHaveProperty('symbol'); - expect(firstStat).toHaveProperty('cik'); - expect(firstStat).toHaveProperty('year'); - expect(firstStat).toHaveProperty('quarter'); - expect(firstStat).toHaveProperty('totalPurchases'); - expect(firstStat).toHaveProperty('totalSales'); - expect(firstStat.symbol).toBe('AAPL'); - } - }, - API_TIMEOUT, - ); + it('should get insider trade statistics by symbol', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', totalPurchases: 10 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getInsiderTradeStatistics({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading/statistics', 'stable', { + symbol: 'AAPL', + }); + expect(result).toEqual(mockResponse); + }); }); - describe('getCikMapper (DEPRECATED)', () => { - it( - 'should return CIK mapper data with pagination (v4 endpoint)', - async () => { - if (shouldSkipTests()) { - console.log('Skipping CIK mapper test - no API key available'); - return; - } - - const result = await fmp.insider.getCikMapper({ page: 0 }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const firstMapping = result.data[0]; - expect(firstMapping).toHaveProperty('reportingCik'); - expect(firstMapping).toHaveProperty('reportingName'); - } - }, - API_TIMEOUT, - ); + describe('getCikMapper', () => { + it('should get CIK mapper with default page (v4)', async () => { + const mockResponse = { + success: true, + data: [{ reportingCik: '0000320193', reportingName: 'Apple Inc.' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getCikMapper({}); + + expect(mockClient.get).toHaveBeenCalledWith('/mapper-cik-name', 'v4', { page: 0 }); + expect(result).toEqual(mockResponse); + }); + + it('should pass through explicit page', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getCikMapper({ page: 3 }); + + expect(mockClient.get).toHaveBeenCalledWith('/mapper-cik-name', 'v4', { page: 3 }); + expect(result).toEqual(mockResponse); + }); }); - describe('getCikMapperByName (DEPRECATED)', () => { - it( - 'should return CIK mapper data by name search (v4 endpoint)', - async () => { - if (shouldSkipTests()) { - console.log('Skipping CIK mapper by name test - no API key available'); - return; - } - - const result = await fmp.insider.getCikMapperByName({ name: 'apple', page: 0 }); - - // This endpoint might fail, so we'll check for success or handle the failure gracefully - if (result.success) { - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const firstMapping = result.data[0]; - expect(firstMapping).toHaveProperty('reportingCik'); - expect(firstMapping).toHaveProperty('reportingName'); - expect(firstMapping.reportingName.toLowerCase()).toContain('apple'); - } - } else { - // If the API call fails, just log it but don't fail the test - console.log( - 'CIK mapper by name API call failed, which is expected for deprecated endpoints', - ); - expect(result.success).toBe(false); - } - }, - API_TIMEOUT, - ); + describe('getCikMapperByName', () => { + it('should get CIK mapper by name with default page (v4)', async () => { + const mockResponse = { + success: true, + data: [{ reportingCik: '0000320193', reportingName: 'Apple Inc.' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getCikMapperByName({ name: 'Apple' }); + + expect(mockClient.get).toHaveBeenCalledWith('/mapper-cik-name', 'v4', { + name: 'Apple', + page: 0, + }); + expect(result).toEqual(mockResponse); + }); }); - describe('getCikMapperBySymbol (DEPRECATED)', () => { - it( - 'should return CIK mapper data by symbol (v4 endpoint)', - async () => { - if (shouldSkipTests()) { - console.log('Skipping CIK mapper by symbol test - no API key available'); - return; - } - - const result = await fmp.insider.getCikMapperBySymbol({ symbol: 'AAPL' }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(result.data).toHaveProperty('symbol'); - expect(result.data).toHaveProperty('companyCik'); - expect(result.data!.symbol).toBe('AAPL'); - }, - FAST_TIMEOUT, - ); + describe('getCikMapperBySymbol', () => { + it('should get CIK mapper by symbol via getSingle (v4)', async () => { + const mockResponse = { + success: true, + data: { symbol: 'AAPL', companyCik: '0000320193' }, + error: null, + status: 200, + }; + mockClient.getSingle.mockResolvedValue(mockResponse); + + const result = await endpoints.getCikMapperBySymbol({ symbol: 'AAPL' }); + + expect(mockClient.getSingle).toHaveBeenCalledWith('/mapper-cik-company/AAPL', 'v4'); + expect(result).toEqual(mockResponse); + }); }); describe('getBeneficialOwnership', () => { - it( - 'should return beneficial ownership data for a symbol', - async () => { - if (shouldSkipTests()) { - console.log('Skipping beneficial ownership test - no API key available'); - return; - } - - const result = await fmp.insider.getBeneficialOwnership({ symbol: 'AAPL' }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const firstOwnership = result.data[0]; - expect(firstOwnership).toHaveProperty('cik'); - expect(firstOwnership).toHaveProperty('symbol'); - expect(firstOwnership).toHaveProperty('filingDate'); - expect(firstOwnership).toHaveProperty('nameOfReportingPerson'); - expect(firstOwnership.symbol).toBe('AAPL'); - } - }, - API_TIMEOUT, - ); + it('should get beneficial ownership with default limit', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', cik: '0000320193' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getBeneficialOwnership({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith('/acquisition-of-beneficial-ownership', 'stable', { + symbol: 'AAPL', + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); + + it('should pass through explicit limit', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getBeneficialOwnership({ symbol: 'MSFT', limit: 10 }); + + expect(mockClient.get).toHaveBeenCalledWith('/acquisition-of-beneficial-ownership', 'stable', { + symbol: 'MSFT', + limit: 10, + }); + expect(result).toEqual(mockResponse); + }); }); - describe('getFailToDeliver (DEPRECATED)', () => { - it( - 'should return fail to deliver data for a symbol (v4 endpoint)', - async () => { - if (shouldSkipTests()) { - console.log('Skipping fail to deliver test - no API key available'); - return; - } - - const result = await fmp.insider.getFailToDeliver({ symbol: 'AAPL', page: 0 }); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const firstEntry = result.data[0]; - expect(firstEntry).toHaveProperty('symbol'); - expect(firstEntry).toHaveProperty('date'); - expect(firstEntry).toHaveProperty('price'); - expect(firstEntry).toHaveProperty('quantity'); - expect(firstEntry.symbol).toBe('AAPL'); - } - }, - API_TIMEOUT, - ); + describe('getFailToDeliver', () => { + it('should get fail to deliver data with default page (v4)', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', quantity: 500 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getFailToDeliver({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith('/fail_to_deliver', 'v4', { + symbol: 'AAPL', + page: 0, + }); + expect(result).toEqual(mockResponse); + }); }); - describe('convenience methods', () => { - describe('getInsiderTradesBySymbol', () => { - it( - 'should return insider trades for a specific symbol', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insider trades by symbol test - no API key available'); - return; - } - - const result = await fmp.insider.getInsiderTradesBySymbol('AAPL', 0); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const firstTrade = result.data[0]; - expect(firstTrade).toHaveProperty('symbol'); - expect(firstTrade.symbol).toBe('AAPL'); - } - }, - API_TIMEOUT, - ); - - it( - 'should use default page value when not provided', - async () => { - if (shouldSkipTests()) { - console.log( - 'Skipping insider trades by symbol default page test - no API key available', - ); - return; - } - - const result = await fmp.insider.getInsiderTradesBySymbol('AAPL'); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - }, - API_TIMEOUT, - ); + describe('getInsiderTradesBySymbol', () => { + it('should delegate to searchInsiderTrading with symbol and explicit page', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getInsiderTradesBySymbol('AAPL', 1); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading/search', 'stable', { + page: 1, + limit: 100, + symbol: 'AAPL', + }); + expect(result).toEqual(mockResponse); + }); + + it('should default page to 0', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + await endpoints.getInsiderTradesBySymbol('AAPL'); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading/search', 'stable', { + page: 0, + limit: 100, + symbol: 'AAPL', + }); }); + }); - describe('getInsiderTradesByType', () => { - it( - 'should return insider trades filtered by transaction type', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insider trades by type test - no API key available'); - return; - } - - const result = await fmp.insider.getInsiderTradesByType(TransactionType.SALE, 0); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const firstTrade = result.data[0]; - expect(firstTrade).toHaveProperty('transactionType'); - expect(firstTrade.transactionType).toBe(TransactionType.SALE); - } - }, - API_TIMEOUT, - ); - - it( - 'should use default page value when not provided', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insider trades by type default page test - no API key available'); - return; - } - - const result = await fmp.insider.getInsiderTradesByType(TransactionType.SALE); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - }, - API_TIMEOUT, - ); + describe('getInsiderTradesByType', () => { + it('should delegate to searchInsiderTrading with transactionType and page', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getInsiderTradesByType('P-Purchase', 2); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading/search', 'stable', { + page: 2, + limit: 100, + transactionType: 'P-Purchase', + }); + expect(result).toEqual(mockResponse); }); + }); - describe('getInsiderTradesByReportingCik', () => { - it( - 'should return insider trades filtered by reporting CIK', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insider trades by reporting CIK test - no API key available'); - return; - } - - const result = await fmp.insider.getInsiderTradesByReportingCik('0000320193', 0); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const firstTrade = result.data[0]; - expect(firstTrade).toHaveProperty('reportingCik'); - } - }, - API_TIMEOUT, - ); - - it( - 'should use default page value when not provided', - async () => { - if (shouldSkipTests()) { - console.log( - 'Skipping insider trades by reporting CIK default page test - no API key available', - ); - return; - } - - const result = await fmp.insider.getInsiderTradesByReportingCik('0000320193'); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - }, - API_TIMEOUT, - ); + describe('getInsiderTradesByReportingCik', () => { + it('should delegate to searchInsiderTrading with reportingCik and page', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getInsiderTradesByReportingCik('0000111111', 1); + + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading/search', 'stable', { + page: 1, + limit: 100, + reportingCik: '0000111111', + }); + expect(result).toEqual(mockResponse); }); + }); + + describe('getInsiderTradesByCompanyCik', () => { + it('should delegate to searchInsiderTrading with companyCik and page', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getInsiderTradesByCompanyCik('0000320193', 0); - describe('getInsiderTradesByCompanyCik', () => { - it( - 'should return insider trades filtered by company CIK', - async () => { - if (shouldSkipTests()) { - console.log('Skipping insider trades by company CIK test - no API key available'); - return; - } - - const result = await fmp.insider.getInsiderTradesByCompanyCik('0000320193', 0); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const firstTrade = result.data[0]; - expect(firstTrade).toHaveProperty('companyCik'); - } - }, - API_TIMEOUT, - ); - - it( - 'should use default page value when not provided', - async () => { - if (shouldSkipTests()) { - console.log( - 'Skipping insider trades by company CIK default page test - no API key available', - ); - return; - } - - const result = await fmp.insider.getInsiderTradesByCompanyCik('0000320193'); - - expect(result.success).toBe(true); - expect(result.data).not.toBeNull(); - expect(Array.isArray(result.data)).toBe(true); - }, - API_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/insider-trading/search', 'stable', { + page: 0, + limit: 100, + companyCik: '0000320193', + }); + expect(result).toEqual(mockResponse); }); }); }); diff --git a/packages/api/src/__tests__/endpoints/institutional.test.ts b/packages/api/src/__tests__/endpoints/institutional.test.ts index 192d2f6..6668223 100644 --- a/packages/api/src/__tests__/endpoints/institutional.test.ts +++ b/packages/api/src/__tests__/endpoints/institutional.test.ts @@ -1,77 +1,78 @@ -import { FMP } from '../../fmp'; +import { InstitutionalEndpoints } from '../../endpoints/institutional'; +import { FMPClient } from '../../client'; -const API_KEY = process.env.FMP_API_KEY || ''; +// Mock the FMPClient +jest.mock('../../client'); -describe('InstitutionalEndpoints (integration)', () => { - let fmp: FMP; +describe('InstitutionalEndpoints', () => { + let endpoints: InstitutionalEndpoints; + let mockClient: jest.Mocked; - beforeAll(() => { - if (!API_KEY) { - throw new Error('FMP_API_KEY must be set in environment for integration tests'); - } - fmp = new FMP({ apiKey: API_KEY }); + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + endpoints = new InstitutionalEndpoints(mockClient); }); describe('getForm13F', () => { - it('should return non-empty data array with expected fields for a valid CIK and date', async () => { - const result = await fmp.institutional.getForm13F({ - cik: '0001388838', + it('should get Form 13F data without a date (empty params)', async () => { + const mockResponse = { + success: true, + data: [{ nameOfIssuer: 'Apple Inc.', cusip: '037833100' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getForm13F({ cik: '0001067983' }); + + expect(mockClient.get).toHaveBeenCalledWith('/form-thirteen/0001067983', 'v3', {}); + expect(result).toEqual(mockResponse); + }); + + it('should include date in query params when provided', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getForm13F({ cik: '0001388838', date: '2021-09-30' }); + + expect(mockClient.get).toHaveBeenCalledWith('/form-thirteen/0001388838', 'v3', { date: '2021-09-30', }); - expect(result).toBeDefined(); - expect(result.success).toBe(true); - expect(Array.isArray(result.data)).toBe(true); - if (result.data && result.data.length > 0) { - const first = result.data[0]; - expect(first).toHaveProperty('nameOfIssuer'); - expect(first).toHaveProperty('cusip'); - expect(first).toHaveProperty('value'); - expect(first).toHaveProperty('shares'); - expect(first).toHaveProperty('titleOfClass'); - expect(first).toHaveProperty('tickercusip'); - expect(first).toHaveProperty('acceptedDate'); - expect(first).toHaveProperty('fillingDate'); - expect(first).toHaveProperty('link'); - expect(first).toHaveProperty('finalLink'); - } else { - console.warn( - '⚠️ getForm13F returned an empty array for CIK 0001388838 and date 2021-09-30', - ); - } + expect(result).toEqual(mockResponse); }); }); describe('getForm13FDates', () => { - it('should return a non-empty array of date strings for a valid CIK', async () => { - const result = await fmp.institutional.getForm13FDates({ cik: '0001067983' }); - expect(result).toBeDefined(); - expect(result.success).toBe(true); - expect(Array.isArray(result.data)).toBe(true); - if (result.data && result.data.length > 0) { - expect(typeof result.data[0]).toBe('string'); - // Optionally, check if it's a valid date string - expect(result.data[0]).toMatch(/^\d{4}-\d{2}-\d{2}$/); - } else { - console.warn('⚠️ getForm13FDates returned an empty array for CIK 0001067983'); - } + it('should get Form 13F filing dates for a CIK', async () => { + const mockResponse = { + success: true, + data: ['2023-12-31', '2023-09-30'], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getForm13FDates({ cik: '0001067983' }); + + expect(mockClient.get).toHaveBeenCalledWith('/form-thirteen-date/0001067983', 'v3'); + expect(result).toEqual(mockResponse); }); }); describe('getInstitutionalHolders', () => { - it('should return non-empty data array with expected fields for a valid symbol', async () => { - const result = await fmp.institutional.getInstitutionalHolders({ symbol: 'AAPL' }); - expect(result).toBeDefined(); - expect(result.success).toBe(true); - expect(Array.isArray(result.data)).toBe(true); - if (result.data && result.data.length > 0) { - const first = result.data[0]; - expect(first).toHaveProperty('holder'); - expect(first).toHaveProperty('shares'); - expect(first).toHaveProperty('dateReported'); - expect(first).toHaveProperty('change'); - } else { - console.warn('⚠️ getInstitutionalHolders returned an empty array for symbol AAPL'); - } + it('should get institutional holders for a symbol', async () => { + const mockResponse = { + success: true, + data: [{ holder: 'Vanguard', shares: 1000000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getInstitutionalHolders({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith('/institutional-holder/AAPL', 'v3'); + expect(result).toEqual(mockResponse); }); }); }); diff --git a/packages/api/src/__tests__/endpoints/list.test.ts b/packages/api/src/__tests__/endpoints/list.test.ts index b56c3fc..e1b87e5 100644 --- a/packages/api/src/__tests__/endpoints/list.test.ts +++ b/packages/api/src/__tests__/endpoints/list.test.ts @@ -1,329 +1,100 @@ -import { FMP } from '../../fmp'; -import { shouldSkipTests, createTestClient, API_TIMEOUT } from '../utils/test-setup'; -import { StockList, ETFList, CryptoList, ForexList, AvailableIndexesList } from 'fmp-node-types'; +import { ListEndpoints } from '../../endpoints/list'; +import { FMPClient } from '../../client'; -// Test data cache to avoid duplicate API calls -interface ListTestDataCache { - stocks?: any; - etfs?: any; - crypto?: any; - forex?: any; - indexes?: any; -} +// Mock the FMPClient +jest.mock('../../client'); -describe('List Endpoints', () => { - let fmp: FMP; - let testDataCache: ListTestDataCache = {}; +describe('ListEndpoints', () => { + let listEndpoints: ListEndpoints; + let mockClient: jest.Mocked; - beforeAll(async () => { - if (shouldSkipTests()) { - console.log('Skipping list tests - no API key available'); - return; - } - fmp = createTestClient(); - - try { - // Fetch all list data in parallel - const [stocks, etfs, crypto, forex, indexes] = await Promise.all([ - fmp.list.getStockList(), - fmp.list.getETFList(), - fmp.list.getCryptoList(), - fmp.list.getForexList(), - fmp.list.getAvailableIndexes(), - ]); - - testDataCache = { - stocks, - etfs, - crypto, - forex, - indexes, - }; - } catch (error) { - console.warn('Failed to pre-fetch test data:', error); - // Continue with tests - they will fetch data individually if needed - } - }, API_TIMEOUT); // Add timeout to beforeAll hook + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + listEndpoints = new ListEndpoints(mockClient); + }); describe('getStockList', () => { - it( - 'should fetch stock list with valid data structure and return non-empty array', - async () => { - if (shouldSkipTests()) { - console.log('Skipping stock list test - no API key available'); - return; - } - - const result = testDataCache.stocks || (await fmp.list.getStockList()); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - if (result.data && result.data.length > 0) { - const stock = result.data[0] as StockList; - - // Validate required fields - expect(stock.symbol).toBeDefined(); - expect(typeof stock.symbol).toBe('string'); - expect(stock.symbol.length).toBeGreaterThan(0); - - expect(stock.exchange).toBeDefined(); - expect(typeof stock.exchange).toBe('string'); - - expect(stock.exchangeShortName).toBeDefined(); - expect(typeof stock.exchangeShortName).toBe('string'); - - expect(stock.price).toBeDefined(); - expect(typeof stock.price).toBe('number'); - expect(stock.price).toBeGreaterThan(0); + it('should get stock list using /stock/list endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', name: 'Apple Inc.', exchange: 'NASDAQ' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - expect(stock.type).toBeDefined(); - expect(typeof stock.type).toBe('string'); + const result = await listEndpoints.getStockList(); - expect(stock.name).toBeDefined(); - expect(typeof stock.name).toBe('string'); - expect(stock.name.length).toBeGreaterThan(0); - } - }, - API_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/stock/list'); + expect(result).toEqual(mockResponse); + }); }); describe('getETFList', () => { - it( - 'should fetch ETF list with valid data structure and return non-empty array', - async () => { - if (shouldSkipTests()) { - console.log('Skipping ETF list test - no API key available'); - return; - } - - const result = testDataCache.etfs || (await fmp.list.getETFList()); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - if (result.data && result.data.length > 0) { - const etf = result.data[0] as ETFList; - - // Validate required fields - expect(etf.symbol).toBeDefined(); - expect(typeof etf.symbol).toBe('string'); - expect(etf.symbol.length).toBeGreaterThan(0); - - expect(etf.exchange).toBeDefined(); - expect(typeof etf.exchange).toBe('string'); - - expect(etf.exchangeShortName).toBeDefined(); - expect(typeof etf.exchangeShortName).toBe('string'); + it('should get ETF list using /etf/list endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'SPY', name: 'SPDR S&P 500 ETF Trust', exchange: 'NYSE' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - expect(etf.price).toBeDefined(); - expect(typeof etf.price).toBe('number'); - expect(etf.price).toBeGreaterThan(0); + const result = await listEndpoints.getETFList(); - expect(etf.name).toBeDefined(); - expect(typeof etf.name).toBe('string'); - expect(etf.name.length).toBeGreaterThan(0); - } - }, - API_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/etf/list'); + expect(result).toEqual(mockResponse); + }); }); describe('getCryptoList', () => { - it( - 'should fetch crypto list with valid data structure, return non-empty array, and contain major cryptocurrencies', - async () => { - if (shouldSkipTests()) { - console.log('Skipping crypto list test - no API key available'); - return; - } - - const result = testDataCache.crypto || (await fmp.list.getCryptoList()); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - if (result.data && result.data.length > 0) { - const crypto = result.data[0] as CryptoList; - - // Validate required fields - expect(crypto.symbol).toBeDefined(); - expect(typeof crypto.symbol).toBe('string'); - expect(crypto.symbol.length).toBeGreaterThan(0); - - expect(crypto.name).toBeDefined(); - expect(typeof crypto.name).toBe('string'); - expect(crypto.name.length).toBeGreaterThan(0); - - expect(crypto.currency).toBeDefined(); - expect(typeof crypto.currency).toBe('string'); - - expect(crypto.stockExchange).toBeDefined(); - expect(typeof crypto.stockExchange).toBe('string'); - - expect(crypto.exchangeShortName).toBeDefined(); - expect(typeof crypto.exchangeShortName).toBe('string'); + it('should get crypto list using /symbol/available-cryptocurrencies endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'BTCUSD', name: 'Bitcoin', currency: 'USD' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - // Check for major cryptocurrencies - const symbols = result.data.map((crypto: CryptoList) => crypto.symbol); - const majorCryptos = ['BTCUSD', 'ETHUSD', 'USDTUSD', 'BNBUSD', 'ADAUSD']; - const foundMajorCryptos = majorCryptos.filter(symbol => - symbols.some((s: string) => s.includes(symbol.replace('USD', '')) || s === symbol), - ); + const result = await listEndpoints.getCryptoList(); - // Should find at least some major cryptocurrencies - expect(foundMajorCryptos.length).toBeGreaterThan(0); - } - }, - API_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/symbol/available-cryptocurrencies'); + expect(result).toEqual(mockResponse); + }); }); describe('getForexList', () => { - it( - 'should fetch forex list with valid data structure, return non-empty array, and contain major forex pairs', - async () => { - if (shouldSkipTests()) { - console.log('Skipping forex list test - no API key available'); - return; - } - - const result = testDataCache.forex || (await fmp.list.getForexList()); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - if (result.data && result.data.length > 0) { - const forex = result.data[0] as ForexList; - - // Validate required fields - expect(forex.symbol).toBeDefined(); - expect(typeof forex.symbol).toBe('string'); - expect(forex.symbol.length).toBeGreaterThan(0); - - expect(forex.name).toBeDefined(); - expect(typeof forex.name).toBe('string'); - expect(forex.name.length).toBeGreaterThan(0); - - expect(forex.currency).toBeDefined(); - expect(typeof forex.currency).toBe('string'); - - expect(forex.stockExchange).toBeDefined(); - expect(typeof forex.stockExchange).toBe('string'); - - expect(forex.exchangeShortName).toBeDefined(); - expect(typeof forex.exchangeShortName).toBe('string'); + it('should get forex list using /symbol/available-forex-currency-pairs endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'EURUSD', name: 'EUR/USD', currency: 'USD' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - // Check for major forex pairs - const symbols = result.data.map((forex: ForexList) => forex.symbol); - const majorPairs = ['EURUSD', 'GBPUSD', 'USDJPY', 'USDCHF', 'AUDUSD', 'USDCAD']; - const foundMajorPairs = majorPairs.filter(symbol => symbols.includes(symbol)); + const result = await listEndpoints.getForexList(); - // Should find at least some major forex pairs - expect(foundMajorPairs.length).toBeGreaterThan(0); - } - }, - API_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/symbol/available-forex-currency-pairs'); + expect(result).toEqual(mockResponse); + }); }); describe('getAvailableIndexes', () => { - it( - 'should fetch available indexes with valid data structure, return non-empty array, and contain major market indexes', - async () => { - if (shouldSkipTests()) { - console.log('Skipping available indexes test - no API key available'); - return; - } - - const result = testDataCache.indexes || (await fmp.list.getAvailableIndexes()); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - if (result.data && result.data.length > 0) { - const index = result.data[0] as AvailableIndexesList; - - // Validate required fields - expect(index.symbol).toBeDefined(); - expect(typeof index.symbol).toBe('string'); - expect(index.symbol.length).toBeGreaterThan(0); - - expect(index.name).toBeDefined(); - expect(typeof index.name).toBe('string'); - expect(index.name.length).toBeGreaterThan(0); - - expect(index.currency).toBeDefined(); - expect(typeof index.currency).toBe('string'); - - expect(index.stockExchange).toBeDefined(); - expect(typeof index.stockExchange).toBe('string'); - - expect(index.exchangeShortName).toBeDefined(); - expect(typeof index.exchangeShortName).toBe('string'); - - // Check for major market indexes - const symbols = result.data.map((index: AvailableIndexesList) => index.symbol); - const majorIndexes = ['^GSPC', '^DJI', '^IXIC', '^RUT', '^VIX']; - const foundMajorIndexes = majorIndexes.filter(symbol => symbols.includes(symbol)); - - // Should find at least some major indexes - expect(foundMajorIndexes.length).toBeGreaterThan(0); - } - }, - API_TIMEOUT, - ); - }); - - describe('Data Consistency', () => { - it( - 'should have consistent data types across all list endpoints', - async () => { - if (shouldSkipTests()) { - console.log('Skipping data consistency test - no API key available'); - return; - } - - // Use cached data if available, otherwise make minimal API calls - const stockResult = testDataCache.stocks || (await fmp.list.getStockList()); - const etfResult = testDataCache.etfs || (await fmp.list.getETFList()); - const cryptoResult = testDataCache.crypto || (await fmp.list.getCryptoList()); - const forexResult = testDataCache.forex || (await fmp.list.getForexList()); - const indexesResult = testDataCache.indexes || (await fmp.list.getAvailableIndexes()); - - // All should be successful - expect(stockResult.success).toBe(true); - expect(etfResult.success).toBe(true); - expect(cryptoResult.success).toBe(true); - expect(forexResult.success).toBe(true); - expect(indexesResult.success).toBe(true); + it('should get available indexes using /symbol/available-indexes endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: '^GSPC', name: 'S&P 500', currency: 'USD' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - // All should return arrays - expect(Array.isArray(stockResult.data)).toBe(true); - expect(Array.isArray(etfResult.data)).toBe(true); - expect(Array.isArray(cryptoResult.data)).toBe(true); - expect(Array.isArray(forexResult.data)).toBe(true); - expect(Array.isArray(indexesResult.data)).toBe(true); + const result = await listEndpoints.getAvailableIndexes(); - // All arrays should be non-empty - expect(stockResult.data!.length).toBeGreaterThan(0); - expect(etfResult.data!.length).toBeGreaterThan(0); - expect(cryptoResult.data!.length).toBeGreaterThan(0); - expect(forexResult.data!.length).toBeGreaterThan(0); - expect(indexesResult.data!.length).toBeGreaterThan(0); - }, - API_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/symbol/available-indexes'); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/market.test.ts b/packages/api/src/__tests__/endpoints/market.test.ts index 83963eb..e5b43be 100644 --- a/packages/api/src/__tests__/endpoints/market.test.ts +++ b/packages/api/src/__tests__/endpoints/market.test.ts @@ -1,154 +1,134 @@ -import { FMP } from '../../fmp'; -import { API_KEY, isCI } from '../utils/test-setup'; - -// Helper function to safely access data that could be an array or single object -function getFirstItem(data: T | T[]): T { - return Array.isArray(data) ? data[0] : data; -} - -describe('Market Endpoints', () => { - if (!API_KEY || isCI) { - it('should skip tests when no API key is provided or running in CI', () => { - expect(true).toBe(true); - }); - return; - } +import { MarketEndpoints } from '../../endpoints/market'; +import { FMPClient } from '../../client'; + +// Mock the FMPClient +jest.mock('../../client'); - let fmp: FMP; +describe('MarketEndpoints', () => { + let marketEndpoints: MarketEndpoints; + let mockClient: jest.Mocked; - beforeAll(() => { - if (!API_KEY) { - throw new Error('FMP_API_KEY is required for testing'); - } - fmp = new FMP({ apiKey: API_KEY }); + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + marketEndpoints = new MarketEndpoints(mockClient); }); describe('getMarketHours', () => { - it('should fetch market hours', async () => { - const result = await fmp.market.getMarketHours(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(result.data?.isTheStockMarketOpen).toBeDefined(); - expect(result.data?.isTheForexMarketOpen).toBeDefined(); - expect(result.data?.isTheCryptoMarketOpen).toBeDefined(); - expect(result.data?.stockExchangeName).toBeDefined(); - }, 10000); + it('should get market hours using /market-hours endpoint', async () => { + const mockResponse = { + success: true, + data: [{ isTheStockMarketOpen: true }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await marketEndpoints.getMarketHours(); + + expect(mockClient.get).toHaveBeenCalledWith('/market-hours', 'v3'); + expect(result).toEqual(mockResponse); + }); }); describe('getMarketPerformance', () => { - it('should fetch market performance', async () => { - const result = await fmp.market.getMarketPerformance(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const performance = getFirstItem(result.data); - expect(performance.symbol).toBeDefined(); - expect(performance.name).toBeDefined(); - expect(Number(performance.price)).toBeGreaterThan(0); - expect(typeof performance.change).toBe('number'); - expect(typeof performance.changesPercentage).toBe('number'); - } - }, 10000); + it('should get market performance using /quotes/index endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: '^GSPC', price: 4500 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await marketEndpoints.getMarketPerformance(); + + expect(mockClient.get).toHaveBeenCalledWith('/quotes/index', 'v3'); + expect(result).toEqual(mockResponse); + }); }); describe('getGainers', () => { - it('should fetch market gainers', async () => { - const result = await fmp.market.getGainers(); - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const gainer = getFirstItem(result.data); - expect(gainer.symbol).toBeDefined(); - expect(gainer.name).toBeDefined(); - expect(Number(gainer.price)).toBeGreaterThan(0); - expect(Number(gainer.changesPercentage)).toBeGreaterThan(0); - expect(typeof gainer.change).toBe('number'); - } - }, 10000); + it('should get gainers using /biggest-gainers stable endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', changesPercentage: 5 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await marketEndpoints.getGainers(); + + expect(mockClient.get).toHaveBeenCalledWith('/biggest-gainers', 'stable'); + expect(result).toEqual(mockResponse); + }); }); describe('getLosers', () => { - it('should fetch market losers', async () => { - const result = await fmp.market.getLosers(); - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const loser = getFirstItem(result.data); - expect(loser.symbol).toBeDefined(); - expect(loser.name).toBeDefined(); - expect(Number(loser.price)).toBeGreaterThan(0); - expect(Number(loser.changesPercentage)).toBeLessThan(0); - expect(typeof loser.change).toBe('number'); - } - }, 10000); + it('should get losers using /biggest-losers stable endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'XYZ', changesPercentage: -5 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await marketEndpoints.getLosers(); + + expect(mockClient.get).toHaveBeenCalledWith('/biggest-losers', 'stable'); + expect(result).toEqual(mockResponse); + }); }); describe('getMostActive', () => { - it('should fetch most active stocks', async () => { - const result = await fmp.market.getMostActive(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const active = getFirstItem(result.data); - expect(active.symbol).toBeDefined(); - expect(active.name).toBeDefined(); - expect(Number(active.price)).toBeGreaterThan(0); - expect(typeof active.change).toBe('number'); - expect(typeof active.changesPercentage).toBe('number'); - } - }, 10000); + it('should get most active using /most-actives stable endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', volume: 1000000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await marketEndpoints.getMostActive(); + + expect(mockClient.get).toHaveBeenCalledWith('/most-actives', 'stable'); + expect(result).toEqual(mockResponse); + }); }); describe('getSectorPerformance', () => { - it('should fetch sector performance', async () => { - const result = await fmp.market.getSectorPerformance(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const sector = getFirstItem(result.data); - expect(sector.sector).toBeDefined(); - expect(sector.changesPercentage).toBeDefined(); - // changesPercentage can be a string or number from the API - expect(['string', 'number']).toContain(typeof sector.changesPercentage); - } - }, 10000); + it('should get sector performance using /sector-performance endpoint', async () => { + const mockResponse = { + success: true, + data: [{ sector: 'Technology', changesPercentage: 1.5 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await marketEndpoints.getSectorPerformance(); + + expect(mockClient.get).toHaveBeenCalledWith('/sector-performance', 'v3'); + expect(result).toEqual(mockResponse); + }); }); describe('getMarketIndex', () => { - it('should fetch market index data', async () => { - const result = await fmp.market.getMarketIndex(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const index = getFirstItem(result.data); - expect(index.symbol).toBeDefined(); - expect(index.name).toBeDefined(); - expect(Number(index.price)).toBeGreaterThan(0); - // Check for optional properties that may or may not be present - if (index.type !== undefined) { - expect(typeof index.type).toBe('string'); - } - if (index.volume !== undefined) { - expect(typeof index.volume).toBe('number'); - } - } - }, 15000); + it('should get market index using /quotes/index endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: '^GSPC', price: 4500 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await marketEndpoints.getMarketIndex(); + + expect(mockClient.get).toHaveBeenCalledWith('/quotes/index', 'v3'); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/mutual-fund.test.ts b/packages/api/src/__tests__/endpoints/mutual-fund.test.ts index 886ab71..cb2f1a5 100644 --- a/packages/api/src/__tests__/endpoints/mutual-fund.test.ts +++ b/packages/api/src/__tests__/endpoints/mutual-fund.test.ts @@ -1,40 +1,40 @@ -import { FMP } from '../../fmp'; -import { createTestClient, shouldSkipTests } from '../utils/test-setup'; +import { MutualFundEndpoints } from '../../endpoints/mutual-fund'; +import { FMPClient } from '../../client'; -describe('Mutual Fund Endpoints', () => { - let fmp: FMP; +// Mock the FMPClient +jest.mock('../../client'); - beforeAll(() => { - if (shouldSkipTests()) { - console.log('Skipping mutual fund tests - no API key available'); - return; - } - fmp = createTestClient(); +describe('MutualFundEndpoints', () => { + let mutualFundEndpoints: MutualFundEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + mutualFundEndpoints = new MutualFundEndpoints(mockClient); }); describe('getHolders', () => { - it('should fetch mutual fund holders', async () => { - if (shouldSkipTests()) { - console.log('Skipping mutual fund holders test - no API key available'); - return; - } - - const result = await fmp.mutualFund.getHolders('VFINX'); + it('should get mutual fund holders using /mutual-fund-holder/{symbol} endpoint', async () => { + const mockResponse = { + success: true, + data: [ + { + holder: 'Vanguard Total Stock Market Index Fund', + shares: 1000000, + dateReported: '2024-01-15', + change: 5000, + weightPercent: 5.2, + }, + ], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); + const result = await mutualFundEndpoints.getHolders('AAPL'); - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const holding = result.data[0]; - expect(holding.holder).toBeDefined(); - expect(holding.shares).toBeDefined(); - expect(holding.dateReported).toBeDefined(); - expect(holding.change).toBeDefined(); - expect(holding.weightPercent).toBeDefined(); - } else { - // Accept empty result as valid for this test - expect(Array.isArray(result.data)).toBe(true); - } - }, 10000); + expect(mockClient.get).toHaveBeenCalledWith('/mutual-fund-holder/AAPL', 'v3'); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/news.test.ts b/packages/api/src/__tests__/endpoints/news.test.ts index c2da2e3..fe78fcc 100644 --- a/packages/api/src/__tests__/endpoints/news.test.ts +++ b/packages/api/src/__tests__/endpoints/news.test.ts @@ -1,446 +1,305 @@ -import { FMP } from '../../fmp'; -import { shouldSkipTests, createTestClient, API_TIMEOUT, FAST_TIMEOUT } from '../utils/test-setup'; -import type { News, Article } from 'fmp-node-types'; - -// Helper function to validate news article structure -function validateNewsArticle(news: News): void { - expect(news.symbol).toBeDefined(); - expect(typeof news.symbol).toBe('string'); - expect(news.publishedDate).toBeDefined(); - expect(typeof news.publishedDate).toBe('string'); - expect(news.publisher).toBeDefined(); - expect(typeof news.publisher).toBe('string'); - expect(news.title).toBeDefined(); - expect(typeof news.title).toBe('string'); - expect(news.text).toBeDefined(); - expect(typeof news.text).toBe('string'); - expect(news.url).toBeDefined(); - expect(typeof news.url).toBe('string'); - expect(news.site).toBeDefined(); - expect(typeof news.site).toBe('string'); - expect(news.image).toBeDefined(); - // Image can be string or object depending on API response - expect(typeof news.image === 'string' || typeof news.image === 'object').toBe(true); -} - -// Helper function to validate FMP article structure -function validateFMPArticle(article: Article): void { - expect(article.title).toBeDefined(); - expect(typeof article.title).toBe('string'); - expect(article.date).toBeDefined(); - expect(typeof article.date).toBe('string'); - expect(article.content).toBeDefined(); - expect(typeof article.content).toBe('string'); - expect(article.link).toBeDefined(); - expect(typeof article.link).toBe('string'); - expect(article.author).toBeDefined(); - expect(typeof article.author).toBe('string'); - expect(article.site).toBeDefined(); - expect(typeof article.site).toBe('string'); - expect(article.tickers).toBeDefined(); - expect(typeof article.tickers).toBe('string'); - expect(article.image).toBeDefined(); - expect(typeof article.image).toBe('string'); -} - -describe('NewsEndpoints Integration Tests', () => { - let fmp: FMP; - - beforeAll(async () => { - if (shouldSkipTests()) { - console.log('Skipping news integration tests - no API key available'); - return; - } - fmp = createTestClient(); +import { NewsEndpoints } from '../../endpoints/news'; +import { FMPClient } from '../../client'; + +// Mock the FMPClient +jest.mock('../../client'); + +describe('NewsEndpoints', () => { + let newsEndpoints: NewsEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + newsEndpoints = new NewsEndpoints(mockClient); }); describe('getArticles', () => { - it( - 'should fetch FMP articles successfully', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getArticles({ page: 1, limit: 5 }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const article = result.data[0]; - validateFMPArticle(article); - } - }, - API_TIMEOUT, - ); - - it( - 'should fetch FMP articles with pagination', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getArticles({ page: 2, limit: 3 }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - }, - API_TIMEOUT, - ); - - it( - 'should handle pagination correctly', - async () => { - if (shouldSkipTests()) return; - - // Test with a reasonable page number - const result = await fmp.news.getArticles({ page: 5, limit: 2 }); - - expect(result).toBeDefined(); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - // Should return data or empty array, but not error - if (result.data) { - expect(result.data.length).toBeLessThanOrEqual(2); - } - }, - FAST_TIMEOUT, - ); + it('should get articles using /fmp-articles endpoint with provided page and limit', async () => { + const mockResponse = { + success: true, + data: [{ title: 'Market Update', content: 'Lorem ipsum' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getArticles({ page: 2, limit: 10 }); + + expect(mockClient.get).toHaveBeenCalledWith('/fmp-articles', 'stable', { + page: 2, + limit: 10, + }); + expect(result).toEqual(mockResponse); + }); + + it('should default page and limit when called with no args', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getArticles(); + + expect(mockClient.get).toHaveBeenCalledWith('/fmp-articles', 'stable', { + page: 1, + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); }); describe('getStockNews', () => { - it( - 'should fetch stock news successfully', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getStockNews({ limit: 5 }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const news = result.data[0]; - validateNewsArticle(news); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch stock news with date range', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getStockNews({ - from: '2024-01-01', - to: '2024-01-15', - limit: 3, - }); - - // API might return success: false if no data for date range - expect(result).toBeDefined(); - expect(result.data).toBeDefined(); - // Data might be null/undefined or array depending on API response - if (result.data !== null && result.data !== undefined) { - expect(Array.isArray(result.data)).toBe(true); - } - }, - API_TIMEOUT, - ); - - it( - 'should fetch stock news with pagination', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getStockNews({ - page: 1, - limit: 10, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - }, - API_TIMEOUT, - ); + it('should get stock news with from and to using /news/stock-latest endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', title: 'Apple news' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getStockNews({ + from: '2024-01-01', + to: '2024-01-15', + limit: 50, + }); + + expect(mockClient.get).toHaveBeenCalledWith('/news/stock-latest', 'stable', { + page: 1, + limit: 50, + from: '2024-01-01', + to: '2024-01-15', + }); + expect(result).toEqual(mockResponse); + }); + + it('should default page and limit when called with no args', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getStockNews(); + + expect(mockClient.get).toHaveBeenCalledWith('/news/stock-latest', 'stable', { + page: 1, + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); }); describe('getCryptoNews', () => { - it( - 'should fetch crypto news successfully', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getCryptoNews({ limit: 5 }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const news = result.data[0]; - validateNewsArticle(news); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch crypto news with date range', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getCryptoNews({ - from: '2024-01-01', - to: '2024-01-15', - limit: 3, - }); - - // API might return success: false if no data for date range - expect(result).toBeDefined(); - expect(result.data).toBeDefined(); - // Data might be null/undefined or array depending on API response - if (result.data !== null && result.data !== undefined) { - expect(Array.isArray(result.data)).toBe(true); - } - }, - API_TIMEOUT, - ); + it('should get crypto news with date range using /news/crypto-latest endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'BTCUSD', title: 'Bitcoin news' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getCryptoNews({ from: '2024-01-08', to: '2024-01-15' }); + + expect(mockClient.get).toHaveBeenCalledWith('/news/crypto-latest', 'stable', { + page: 1, + limit: 100, + from: '2024-01-08', + to: '2024-01-15', + }); + expect(result).toEqual(mockResponse); + }); + + it('should default page and limit when called with no args', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getCryptoNews(); + + expect(mockClient.get).toHaveBeenCalledWith('/news/crypto-latest', 'stable', { + page: 1, + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); }); describe('getForexNews', () => { - it( - 'should fetch forex news successfully', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getForexNews({ limit: 5 }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const news = result.data[0]; - validateNewsArticle(news); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch forex news with date range', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getForexNews({ - from: '2024-01-01', - to: '2024-01-15', - limit: 3, - }); - - // API might return success: false if no data for date range - expect(result).toBeDefined(); - expect(result.data).toBeDefined(); - // Data might be null/undefined or array depending on API response - if (result.data !== null && result.data !== undefined) { - expect(Array.isArray(result.data)).toBe(true); - } - }, - API_TIMEOUT, - ); + it('should get forex news using /news/forex-latest endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'EURUSD', title: 'Forex news' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getForexNews({ from: '2024-01-10', limit: 30 }); + + expect(mockClient.get).toHaveBeenCalledWith('/news/forex-latest', 'stable', { + page: 1, + limit: 30, + from: '2024-01-10', + }); + expect(result).toEqual(mockResponse); + }); + + it('should default page and limit when called with no args', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getForexNews(); + + expect(mockClient.get).toHaveBeenCalledWith('/news/forex-latest', 'stable', { + page: 1, + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); }); describe('getStockNewsBySymbol', () => { - it( - 'should fetch stock news for specific symbols', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getStockNewsBySymbol({ - symbols: ['AAPL'], - limit: 3, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const news = result.data[0]; - validateNewsArticle(news); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch stock news for multiple symbols', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getStockNewsBySymbol({ - symbols: ['AAPL', 'MSFT'], - limit: 5, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - }, - API_TIMEOUT, - ); - - it( - 'should handle invalid symbols gracefully', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getStockNewsBySymbol({ - symbols: ['INVALID_SYMBOL_12345'], - limit: 3, - }); - - expect(result).toBeDefined(); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - // Should return empty array for invalid symbols - if (result.data) { - expect(result.data.length).toBe(0); - } - }, - FAST_TIMEOUT, - ); + it('should get stock news by symbol using /news/stock?symbols= endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', title: 'Apple news' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getStockNewsBySymbol({ + symbols: ['AAPL', 'MSFT'], + from: '2024-01-01', + limit: 20, + }); + + expect(mockClient.get).toHaveBeenCalledWith('/news/stock?symbols=AAPL,MSFT', 'stable', { + page: 1, + limit: 20, + from: '2024-01-01', + }); + expect(result).toEqual(mockResponse); + }); + + it('should default page and limit when only symbols provided', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getStockNewsBySymbol({ symbols: ['AAPL'] }); + + expect(mockClient.get).toHaveBeenCalledWith('/news/stock?symbols=AAPL', 'stable', { + page: 1, + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); }); describe('getCryptoNewsBySymbol', () => { - it( - 'should fetch crypto news for specific symbols', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getCryptoNewsBySymbol({ - symbols: ['BTCUSD'], - limit: 3, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const news = result.data[0]; - validateNewsArticle(news); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch crypto news for multiple symbols', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getCryptoNewsBySymbol({ - symbols: ['BTCUSD', 'ETHUSD'], - limit: 5, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - }, - API_TIMEOUT, - ); - - it( - 'should fetch crypto news with date range', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getCryptoNewsBySymbol({ - symbols: ['BTCUSD'], - from: '2024-01-01', - to: '2024-01-15', - limit: 3, - }); - - // API might return success: false if no data for date range - expect(result).toBeDefined(); - expect(result.data).toBeDefined(); - // Data might be null/undefined or array depending on API response - if (result.data !== null && result.data !== undefined) { - expect(Array.isArray(result.data)).toBe(true); - } - }, - API_TIMEOUT, - ); + it('should get crypto news by symbol using /news/crypto?symbols= endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'BTCUSD', title: 'Bitcoin news' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getCryptoNewsBySymbol({ + symbols: ['BTCUSD', 'ETHUSD'], + from: '2024-01-01', + limit: 25, + }); + + expect(mockClient.get).toHaveBeenCalledWith('/news/crypto?symbols=BTCUSD,ETHUSD', 'stable', { + page: 1, + limit: 25, + from: '2024-01-01', + }); + expect(result).toEqual(mockResponse); + }); + + it('should default page and limit when only symbols provided', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getCryptoNewsBySymbol({ symbols: ['BTCUSD'] }); + + expect(mockClient.get).toHaveBeenCalledWith('/news/crypto?symbols=BTCUSD', 'stable', { + page: 1, + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); }); describe('getForexNewsBySymbol', () => { - it( - 'should fetch forex news for specific symbols', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getForexNewsBySymbol({ - symbols: ['EURUSD'], - limit: 3, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - const news = result.data[0]; - validateNewsArticle(news); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch forex news for multiple symbols', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getForexNewsBySymbol({ - symbols: ['EURUSD', 'GBPUSD'], - limit: 5, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - }, - API_TIMEOUT, - ); - - it( - 'should fetch forex news with date range', - async () => { - if (shouldSkipTests()) return; - - const result = await fmp.news.getForexNewsBySymbol({ - symbols: ['EURUSD'], - from: '2024-01-01', - to: '2024-01-15', - limit: 3, - }); - - // API might return success: false if no data for date range - expect(result).toBeDefined(); - expect(result.data).toBeDefined(); - // Data might be null/undefined or array depending on API response - if (result.data !== null && result.data !== undefined) { - expect(Array.isArray(result.data)).toBe(true); - } - }, - API_TIMEOUT, - ); + it('should get forex news by symbol using /news/forex?symbols= endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'EURUSD', title: 'Forex news' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getForexNewsBySymbol({ + symbols: ['EURUSD', 'GBPUSD'], + from: '2024-01-01', + limit: 20, + }); + + expect(mockClient.get).toHaveBeenCalledWith('/news/forex?symbols=EURUSD,GBPUSD', 'stable', { + page: 1, + limit: 20, + from: '2024-01-01', + }); + expect(result).toEqual(mockResponse); + }); + + it('should default page and limit when only symbols provided', async () => { + const mockResponse = { + success: true, + data: [], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await newsEndpoints.getForexNewsBySymbol({ symbols: ['EURUSD'] }); + + expect(mockClient.get).toHaveBeenCalledWith('/news/forex?symbols=EURUSD', 'stable', { + page: 1, + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/screener.test.ts b/packages/api/src/__tests__/endpoints/screener.test.ts index 9be36cf..8bcc8d0 100644 --- a/packages/api/src/__tests__/endpoints/screener.test.ts +++ b/packages/api/src/__tests__/endpoints/screener.test.ts @@ -1,267 +1,106 @@ -import { FMP } from '../../fmp'; -import { shouldSkipTests, createTestClient, API_TIMEOUT, FAST_TIMEOUT } from '../utils/test-setup'; +import { ScreenerEndpoints } from '../../endpoints/screener'; +import { FMPClient } from '../../client'; -// Test data cache to avoid duplicate API calls -interface TestDataCache { - screener?: any; - availableExchanges?: any; - availableSectors?: any; - availableIndustries?: any; - availableCountries?: any; -} +// Mock the FMPClient +jest.mock('../../client'); -describe('Screener Endpoints', () => { - let fmp: FMP; - let testDataCache: TestDataCache = {}; +describe('ScreenerEndpoints', () => { + let endpoints: ScreenerEndpoints; + let mockClient: jest.Mocked; - 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); + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + endpoints = new ScreenerEndpoints(mockClient); + }); 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(); + it('should screen companies passing params through directly', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', companyName: 'Apple Inc.', marketCap: 3000000000000 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - if (result.data && Array.isArray(result.data)) { - expect(result.data.length).toBeGreaterThan(0); - expect(result.data.length).toBeLessThanOrEqual(10); + const params = { + marketCapMoreThan: 1000000000, + isActivelyTrading: true, + sector: 'Technology', + limit: 10, + }; + const result = await endpoints.getScreener(params); - 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, - ); + expect(mockClient.get).toHaveBeenCalledWith('/company-screener', 'stable', params); + expect(result).toEqual(mockResponse); + }); }); 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); + it('should get available exchanges', async () => { + const mockResponse = { + success: true, + data: [{ exchange: 'NASDAQ', name: 'Nasdaq' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - const exchange = result.data[0]; - expect(exchange.exchange).toBeDefined(); - expect(exchange.name).toBeDefined(); - expect(exchange.countryName).toBeDefined(); - expect(exchange.countryCode).toBeDefined(); + const result = await endpoints.getAvailableExchanges(); - // 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, - ); + expect(mockClient.get).toHaveBeenCalledWith('/available-exchanges', 'stable'); + expect(result).toEqual(mockResponse); + }); }); 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); + it('should get available sectors', async () => { + const mockResponse = { + success: true, + data: [{ sector: 'Technology' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - const sector = result.data[0]; - expect(sector.sector).toBeDefined(); + const result = await endpoints.getAvailableSectors(); - // 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, - ); + expect(mockClient.get).toHaveBeenCalledWith('/available-sectors', 'stable'); + expect(result).toEqual(mockResponse); + }); }); 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); + it('should get available industries', async () => { + const mockResponse = { + success: true, + data: [{ industry: 'Software—Application' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - const industry = result.data[0]; - expect(industry.industry).toBeDefined(); + const result = await endpoints.getAvailableIndustries(); - // 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, - ); + expect(mockClient.get).toHaveBeenCalledWith('/available-industries', 'stable'); + expect(result).toEqual(mockResponse); + }); }); 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, - }); + it('should get available countries', async () => { + const mockResponse = { + success: true, + data: [{ country: 'US' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); - expect(screenerResult.success).toBe(true); - expect(screenerResult.data).toBeDefined(); + const result = await endpoints.getAvailableCountries(); - if (screenerResult.data && Array.isArray(screenerResult.data)) { - screenerResult.data.forEach(company => { - expect(company.sector).toBe(firstSector); - }); - } - } - }, - API_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/available-countries', 'stable'); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/sec.test.ts b/packages/api/src/__tests__/endpoints/sec.test.ts index 88dda50..527a71a 100644 --- a/packages/api/src/__tests__/endpoints/sec.test.ts +++ b/packages/api/src/__tests__/endpoints/sec.test.ts @@ -1,676 +1,221 @@ -import { FMP } from '../../fmp'; -import { - shouldSkipTests, - createTestClient, - API_TIMEOUT, - FAST_TIMEOUT, - TEST_SYMBOLS, -} from '../utils/test-setup'; - -describe('SEC Endpoints', () => { - let fmp: FMP; - - beforeAll(() => { - if (shouldSkipTests()) { - console.log('Skipping SEC tests - no API key available'); - return; - } - fmp = createTestClient(); +import { SECEndpoints } from '../../endpoints/sec'; +import { FMPClient } from '../../client'; + +// Mock the FMPClient +jest.mock('../../client'); + +describe('SECEndpoints', () => { + let endpoints: SECEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + endpoints = new SECEndpoints(mockClient); }); describe('getRSSFeed', () => { - it( - 'should fetch RSS feed with default parameters and validate data', - async () => { - if (shouldSkipTests()) { - console.log('Skipping RSS feed test - no API key available'); - return; - } - const result = await fmp.sec.getRSSFeed(); - - if (!result.success) { - console.warn('⚠️ RSS feed API did not return success:', result.error); - return; - } - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - // Ensure we're getting actual data, not just an empty array - expect(result.data.length).toBeGreaterThan(0); - - // Validate first record structure - const firstRecord = result.data[0]; - expect(firstRecord.title).toBeDefined(); - expect(firstRecord.date).toBeDefined(); - expect(firstRecord.link).toBeDefined(); - expect(firstRecord.cik).toBeDefined(); - expect(firstRecord.form_type).toBeDefined(); - expect(firstRecord.ticker).toBeDefined(); - expect(typeof firstRecord.done).toBe('boolean'); - - // Validate data types and content - expect(typeof firstRecord.title).toBe('string'); - expect(firstRecord.title.length).toBeGreaterThan(0); - expect(typeof firstRecord.date).toBe('string'); - expect(firstRecord.date.length).toBeGreaterThan(0); - expect(typeof firstRecord.link).toBe('string'); - expect(firstRecord.link.length).toBeGreaterThan(0); - expect(firstRecord.link).toContain('http'); - expect(typeof firstRecord.cik).toBe('string'); - expect(firstRecord.cik.length).toBeGreaterThan(0); - expect(typeof firstRecord.form_type).toBe('string'); - expect(firstRecord.form_type.length).toBeGreaterThan(0); - expect(typeof firstRecord.ticker).toBe('string'); - expect(firstRecord.ticker.length).toBeGreaterThan(0); - - // Validate date format (should be a valid date string) - expect(new Date(firstRecord.date).toString()).not.toBe('Invalid Date'); - } else { - // If no data returned, this should be considered a test failure - // as we expect the RSS feed to always have recent filings - fail('RSS feed should return an array with recent filings'); - } - }, - API_TIMEOUT, - ); - - it( - 'should fetch RSS feed with custom parameters and validate filtered results', - async () => { - if (shouldSkipTests()) { - console.log('Skipping RSS feed with params test - no API key available'); - return; - } - const result = await fmp.sec.getRSSFeed({ - limit: 5, - type: '10-K', - from: '2024-01-01', - to: '2024-12-31', - isDone: true, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - // Should return limited results - expect(result.data.length).toBeLessThanOrEqual(5); - - if (result.data.length > 0) { - const firstRecord = result.data[0]; - // The API may return "NT 10-K" (Notice of Late Filing) or other variations - expect(firstRecord.form_type).toContain('10-K'); - expect(firstRecord.done).toBe(true); - - // Validate date is within a reasonable range (allow for some flexibility) - const filingDate = new Date(firstRecord.date); - const fromDate = new Date('2023-01-01'); // Allow filings from 2023 onwards - const toDate = new Date('2025-12-31'); // Allow filings up to 2025 - expect(filingDate.getTime()).toBeGreaterThanOrEqual(fromDate.getTime()); - expect(filingDate.getTime()).toBeLessThanOrEqual(toDate.getTime()); - } else { - // If no 10-K filings found in 2024, that's acceptable - console.log('⚠️ No 10-K filings found for 2024 - this may be expected'); - } - } - }, - API_TIMEOUT, - ); + it('should get RSS feed with no params (undefined passed through)', async () => { + const mockResponse = { + success: true, + data: [{ title: 'Filing', form_type: '4' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getRSSFeed(); + + expect(mockClient.get).toHaveBeenCalledWith('/rss_feed', 'v4', undefined); + expect(result).toEqual(mockResponse); + }); + + it('should pass custom params through (v4)', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const params = { + limit: 5, + type: '10-K', + from: '2024-01-01', + to: '2024-12-31', + isDone: true, + }; + const result = await endpoints.getRSSFeed(params); + + expect(mockClient.get).toHaveBeenCalledWith('/rss_feed', 'v4', params); + expect(result).toEqual(mockResponse); + }); }); - describe('getRSSFeedV3', () => { - it( - 'should fetch RSS feed V3 with default parameters and validate data', - async () => { - if (shouldSkipTests()) { - console.log('Skipping RSS feed V3 test - no API key available'); - return; - } - const result = await fmp.sec.getRSSFeedV3(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - // Ensure we're getting actual data - expect(result.data.length).toBeGreaterThan(0); - - // Validate first record structure - const firstRecord = result.data[0]; - expect(firstRecord.title).toBeDefined(); - expect(firstRecord.date).toBeDefined(); - expect(firstRecord.link).toBeDefined(); - expect(firstRecord.cik).toBeDefined(); - expect(firstRecord.form_type).toBeDefined(); - expect(firstRecord.ticker).toBeDefined(); - expect(typeof firstRecord.done).toBe('boolean'); - - // Validate data content - expect(firstRecord.title.length).toBeGreaterThan(0); - expect(firstRecord.date.length).toBeGreaterThan(0); - expect(firstRecord.link.length).toBeGreaterThan(0); - expect(firstRecord.cik.length).toBeGreaterThan(0); - expect(firstRecord.form_type.length).toBeGreaterThan(0); - expect(firstRecord.ticker.length).toBeGreaterThan(0); - } else { - fail('RSS feed V3 should return an array with recent filings'); - } - }, - API_TIMEOUT, - ); - - it( - 'should fetch RSS feed V3 with custom parameters and validate response', - async () => { - if (shouldSkipTests()) { - console.log('Skipping RSS feed V3 with params test - no API key available'); - return; - } - const result = await fmp.sec.getRSSFeedV3({ - page: 0, - datatype: 'csv', - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - // CSV response should be a string or array - const data = result.data as string | any[]; - if (typeof data === 'string') { - expect(data.length).toBeGreaterThan(0); - expect(data).toContain(','); - } else if (Array.isArray(data)) { - expect(data.length).toBeGreaterThan(0); - } else { - // If neither string nor array, should still have some data - expect(data).toBeDefined(); - } - }, - API_TIMEOUT, - ); + describe('getRSSFeedAll', () => { + it('should get RSS feed all with no params (undefined passed through)', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getRSSFeedAll(); + + expect(mockClient.get).toHaveBeenCalledWith('/rss_feed_all', 'v4', undefined); + expect(result).toEqual(mockResponse); + }); + + it('should pass custom params through (v4)', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const params = { page: 1, datatype: 'csv' }; + const result = await endpoints.getRSSFeedAll(params); + + expect(mockClient.get).toHaveBeenCalledWith('/rss_feed_all', 'v4', params); + expect(result).toEqual(mockResponse); + }); }); - describe('getRSSFeedAll', () => { - it( - 'should fetch RSS feed all with default parameters and validate data', - async () => { - if (shouldSkipTests()) { - console.log('Skipping RSS feed all test - no API key available'); - return; - } - const result = await fmp.sec.getRSSFeedAll(); - - if (!result.success) { - console.warn('⚠️ RSS feed all API did not return success:', result.error); - return; - } - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - // Ensure we're getting actual data - expect(result.data.length).toBeGreaterThan(0); - - // Validate first record structure - const firstRecord = result.data[0]; - expect(firstRecord.symbol).toBeDefined(); - expect(firstRecord.fillingDate).toBeDefined(); - expect(firstRecord.acceptedDate).toBeDefined(); - expect(firstRecord.cik).toBeDefined(); - expect(firstRecord.type).toBeDefined(); - expect(firstRecord.link).toBeDefined(); - expect(firstRecord.finalLink).toBeDefined(); - - // Validate data types and content - expect(typeof firstRecord.symbol).toBe('string'); - expect(firstRecord.symbol.length).toBeGreaterThan(0); - expect(typeof firstRecord.fillingDate).toBe('string'); - expect(firstRecord.fillingDate.length).toBeGreaterThan(0); - expect(typeof firstRecord.acceptedDate).toBe('string'); - expect(firstRecord.acceptedDate.length).toBeGreaterThan(0); - expect(typeof firstRecord.cik).toBe('string'); - expect(firstRecord.cik.length).toBeGreaterThan(0); - expect(typeof firstRecord.type).toBe('string'); - expect(firstRecord.type.length).toBeGreaterThan(0); - expect(typeof firstRecord.link).toBe('string'); - expect(firstRecord.link.length).toBeGreaterThan(0); - expect(typeof firstRecord.finalLink).toBe('string'); - expect(firstRecord.finalLink.length).toBeGreaterThan(0); - - // Validate date formats - expect(new Date(firstRecord.fillingDate).toString()).not.toBe('Invalid Date'); - expect(new Date(firstRecord.acceptedDate).toString()).not.toBe('Invalid Date'); - } else { - fail('RSS feed all should return an array with recent filings'); - } - }, - API_TIMEOUT, - ); - - it( - 'should fetch RSS feed all with custom parameters and validate response', - async () => { - if (shouldSkipTests()) { - console.log('Skipping RSS feed all with params test - no API key available'); - return; - } - const result = await fmp.sec.getRSSFeedAll({ - page: 0, - datatype: 'csv', - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - // CSV response should be a string or array - const data = result.data as string | any[]; - if (typeof data === 'string') { - expect(data.length).toBeGreaterThan(0); - expect(data).toContain(','); - } else if (Array.isArray(data)) { - expect(data.length).toBeGreaterThan(0); - } else { - // If neither string nor array, should still have some data - expect(data).toBeDefined(); - } - }, - API_TIMEOUT, - ); + describe('getRSSFeedV3', () => { + it('should get RSS feed V3 with no params (v3)', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getRSSFeedV3(); + + expect(mockClient.get).toHaveBeenCalledWith('/rss_feed', 'v3', undefined); + expect(result).toEqual(mockResponse); + }); + + it('should pass custom params through (v3)', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const params = { page: 0, datatype: 'csv' }; + const result = await endpoints.getRSSFeedV3(params); + + expect(mockClient.get).toHaveBeenCalledWith('/rss_feed', 'v3', params); + expect(result).toEqual(mockResponse); + }); }); describe('getRSSFeed8K', () => { - it( - 'should fetch RSS feed 8-K with default parameters and validate data', - async () => { - if (shouldSkipTests()) { - console.log('Skipping RSS feed 8-K test - no API key available'); - return; - } - const result = await fmp.sec.getRSSFeed8K(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - // Ensure we're getting actual data - expect(result.data.length).toBeGreaterThan(0); - - // Validate first record structure - const firstRecord = result.data[0]; - expect(firstRecord.title).toBeDefined(); - expect(firstRecord.symbol).toBeDefined(); - expect(firstRecord.cik).toBeDefined(); - expect(firstRecord.link).toBeDefined(); - expect(firstRecord.finalLink).toBeDefined(); - expect(firstRecord.date).toBeDefined(); - expect(firstRecord.process).toBeDefined(); - expect(firstRecord.hasFinancials).toBeDefined(); - - // Validate data types and content - expect(typeof firstRecord.title).toBe('string'); - expect(firstRecord.title.length).toBeGreaterThan(0); - expect(typeof firstRecord.symbol).toBe('string'); - expect(firstRecord.symbol.length).toBeGreaterThan(0); - expect(typeof firstRecord.cik).toBe('string'); - expect(firstRecord.cik.length).toBeGreaterThan(0); - expect(typeof firstRecord.link).toBe('string'); - expect(firstRecord.link.length).toBeGreaterThan(0); - expect(firstRecord.link).toContain('http'); - expect(typeof firstRecord.finalLink).toBe('string'); - // finalLink can be empty, so we don't check length - expect(typeof firstRecord.date).toBe('string'); - expect(firstRecord.date.length).toBeGreaterThan(0); - expect(typeof firstRecord.process).toBe('string'); - expect(typeof firstRecord.hasFinancials).toBe('string'); - - // Validate date format - expect(new Date(firstRecord.date).toString()).not.toBe('Invalid Date'); - } else { - fail('RSS feed 8-K should return an array with recent 8-K filings'); - } - }, - API_TIMEOUT, - ); - - it( - 'should fetch RSS feed 8-K with custom parameters and validate filtered results', - async () => { - if (shouldSkipTests()) { - console.log('Skipping RSS feed 8-K with params test - no API key available'); - return; - } - const result = await fmp.sec.getRSSFeed8K({ - page: 0, - from: '2024-01-01', - to: '2024-12-31', - hasFinancial: true, - limit: 5, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - // Should return limited results - expect(result.data.length).toBeLessThanOrEqual(5); - - if (result.data.length > 0) { - const firstRecord = result.data[0]; - // Validate date is within a reasonable range (allow for some flexibility) - const filingDate = new Date(firstRecord.date); - const fromDate = new Date('2023-01-01'); // Allow filings from 2023 onwards - const toDate = new Date('2025-12-31'); // Allow filings up to 2025 - expect(filingDate.getTime()).toBeGreaterThanOrEqual(fromDate.getTime()); - expect(filingDate.getTime()).toBeLessThanOrEqual(toDate.getTime()); - - // Validate required fields - expect(firstRecord.title.length).toBeGreaterThan(0); - expect(firstRecord.symbol.length).toBeGreaterThan(0); - expect(firstRecord.cik.length).toBeGreaterThan(0); - expect(firstRecord.link.length).toBeGreaterThan(0); - // finalLink can be empty, so we don't check length - expect(typeof firstRecord.process).toBe('string'); - expect(typeof firstRecord.hasFinancials).toBe('string'); - } else { - // If no 8-K filings found with these parameters, the test should fail - fail('8-K RSS feed should return data with the given parameters'); - } - } - }, - API_TIMEOUT, - ); + it('should get RSS feed 8-K with no params (v4)', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getRSSFeed8K(); + + expect(mockClient.get).toHaveBeenCalledWith('/rss_feed_8k', 'v4', undefined); + expect(result).toEqual(mockResponse); + }); + + it('should pass custom params through (v4)', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const params = { + page: 0, + from: '2024-01-01', + to: '2024-12-31', + hasFinancial: true, + limit: 5, + }; + const result = await endpoints.getRSSFeed8K(params); + + expect(mockClient.get).toHaveBeenCalledWith('/rss_feed_8k', 'v4', params); + expect(result).toEqual(mockResponse); + }); }); describe('getSECFilings', () => { - it( - 'should fetch SEC filings by symbol and validate data', - async () => { - if (shouldSkipTests()) { - console.log('Skipping SEC filings test - no API key available'); - return; - } - const result = await fmp.sec.getSECFilings({ - symbol: TEST_SYMBOLS.STOCK, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - // Should return filings for the test symbol - expect(result.data.length).toBeGreaterThan(0); - - // Validate first record structure - const firstRecord = result.data[0]; - expect(firstRecord.fillingDate).toBeDefined(); - expect(firstRecord.acceptedDate).toBeDefined(); - expect(firstRecord.cik).toBeDefined(); - expect(firstRecord.type).toBeDefined(); - expect(firstRecord.link).toBeDefined(); - expect(firstRecord.finalLink).toBeDefined(); - expect(firstRecord.symbol).toBeDefined(); - - // Validate data content - expect(firstRecord.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(firstRecord.fillingDate.length).toBeGreaterThan(0); - expect(firstRecord.acceptedDate.length).toBeGreaterThan(0); - expect(firstRecord.cik.length).toBeGreaterThan(0); - expect(firstRecord.type.length).toBeGreaterThan(0); - expect(firstRecord.link.length).toBeGreaterThan(0); - expect(firstRecord.finalLink.length).toBeGreaterThan(0); - - // Validate date formats - expect(new Date(firstRecord.fillingDate).toString()).not.toBe('Invalid Date'); - expect(new Date(firstRecord.acceptedDate).toString()).not.toBe('Invalid Date'); - } else { - fail(`SEC filings should return an array with filings for ${TEST_SYMBOLS.STOCK}`); - } - }, - API_TIMEOUT, - ); - - it( - 'should fetch SEC filings with custom parameters and validate filtered results', - async () => { - if (shouldSkipTests()) { - console.log('Skipping SEC filings with params test - no API key available'); - return; - } - const result = await fmp.sec.getSECFilings({ - symbol: TEST_SYMBOLS.STOCK, - params: { - page: 0, - type: '10-K', - }, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - if (result.data.length > 0) { - const firstRecord = result.data[0]; - expect(firstRecord.type).toBe('10-K'); - expect(firstRecord.symbol).toBe(TEST_SYMBOLS.STOCK); - } else { - // If no 10-K filings found for this symbol, that's acceptable - console.log( - `⚠️ No 10-K filings found for ${TEST_SYMBOLS.STOCK} - this may be expected`, - ); - } - } - }, - API_TIMEOUT, - ); + it('should get SEC filings by symbol with no params (v3)', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', type: '10-K' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getSECFilings({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith('/sec_filings/AAPL', 'v3', undefined); + expect(result).toEqual(mockResponse); + }); + + it('should pass filing params through (v3)', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getSECFilings({ + symbol: 'MSFT', + params: { type: '10-K', page: 0 }, + }); + + expect(mockClient.get).toHaveBeenCalledWith('/sec_filings/MSFT', 'v3', { + type: '10-K', + page: 0, + }); + expect(result).toEqual(mockResponse); + }); }); describe('getIndividualIndustryClassification', () => { - it( - 'should fetch individual industry classification by symbol and validate data', - async () => { - if (shouldSkipTests()) { - console.log('Skipping individual industry classification test - no API key available'); - return; - } - const result = await fmp.sec.getIndividualIndustryClassification({ - symbol: TEST_SYMBOLS.STOCK, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data) { - // Validate record structure (now returns single object, not array) - const record = result.data; - expect(record.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(record.name).toBeDefined(); - expect(record.cik).toBeDefined(); - expect(record.sicCode).toBeDefined(); - expect(record.industryTitle).toBeDefined(); - expect(record.businessAdress).toBeDefined(); - expect(record.phoneNumber).toBeDefined(); - - // Validate data types and content - expect(typeof record.symbol).toBe('string'); - expect(record.symbol.length).toBeGreaterThan(0); - expect(typeof record.name).toBe('string'); - expect(record.name.length).toBeGreaterThan(0); - expect(typeof record.cik).toBe('string'); - expect(record.cik.length).toBeGreaterThan(0); - expect(typeof record.sicCode).toBe('string'); - expect(record.sicCode.length).toBeGreaterThan(0); - expect(typeof record.industryTitle).toBe('string'); - expect(record.industryTitle.length).toBeGreaterThan(0); - expect(typeof record.businessAdress).toBe('string'); // API returns as string, not array - expect(record.businessAdress.length).toBeGreaterThan(0); - expect(typeof record.phoneNumber).toBe('string'); - expect(record.phoneNumber.length).toBeGreaterThan(0); - } else { - fail(`Industry classification should return data for ${TEST_SYMBOLS.STOCK}`); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch individual industry classification by CIK and validate data', - async () => { - if (shouldSkipTests()) { - console.log( - 'Skipping individual industry classification by CIK test - no API key available', - ); - return; - } - const result = await fmp.sec.getIndividualIndustryClassification({ - cik: '0001708646', - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data) { - // Handle both single object and array responses (API inconsistency) - const record = Array.isArray(result.data) ? result.data[0] : result.data; - if (record && record.cik) { - expect(record.cik).toBe('0001708646'); // CIK should be padded with zeros - expect(record.name).toBeDefined(); - expect(record.name.length).toBeGreaterThan(0); - expect(record.sicCode).toBeDefined(); - expect(record.sicCode.length).toBeGreaterThan(0); - expect(record.industryTitle).toBeDefined(); - expect(record.industryTitle.length).toBeGreaterThan(0); - } - } else { - console.log('⚠️ No data returned for CIK 320193 - this may be expected'); - } - }, - FAST_TIMEOUT, - ); + it('should get individual industry classification via getSingle (v4)', async () => { + const mockResponse = { + success: true, + data: { symbol: 'AAPL', sicCode: '3571', industryTitle: 'Electronic Computers' }, + error: null, + status: 200, + }; + mockClient.getSingle.mockResolvedValue(mockResponse); + + const params = { symbol: 'AAPL' }; + const result = await endpoints.getIndividualIndustryClassification(params); + + expect(mockClient.getSingle).toHaveBeenCalledWith( + '/standard_industrial_classification', + 'v4', + params, + ); + expect(result).toEqual(mockResponse); + }); }); describe('getAllIndustryClassifications', () => { - it( - 'should fetch all industry classifications and validate data', - async () => { - if (shouldSkipTests()) { - console.log('Skipping all industry classifications test - no API key available'); - return; - } - const result = await fmp.sec.getAllIndustryClassifications(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - // Should return a substantial list of companies - expect(result.data.length).toBeGreaterThan(100); - - // Validate first record structure - const firstRecord = result.data[0]; - expect(firstRecord.symbol).toBeDefined(); - expect(firstRecord.name).toBeDefined(); - expect(firstRecord.cik).toBeDefined(); - expect(firstRecord.sicCode).toBeDefined(); - expect(firstRecord.industryTitle).toBeDefined(); - expect(firstRecord.businessAdress).toBeDefined(); - expect(firstRecord.phoneNumber).toBeDefined(); - - // Validate data types and content - expect(typeof firstRecord.symbol).toBe('string'); - expect(firstRecord.symbol.length).toBeGreaterThan(0); - expect(typeof firstRecord.name).toBe('string'); - expect(firstRecord.name.length).toBeGreaterThan(0); - expect(typeof firstRecord.cik).toBe('string'); - expect(firstRecord.cik.length).toBeGreaterThan(0); - expect(typeof firstRecord.sicCode).toBe('string'); - expect(firstRecord.sicCode.length).toBeGreaterThan(0); - expect(typeof firstRecord.industryTitle).toBe('string'); - expect(firstRecord.industryTitle.length).toBeGreaterThan(0); - expect(typeof firstRecord.businessAdress).toBe('string'); // API returns as string, not array - expect(firstRecord.businessAdress.length).toBeGreaterThan(0); - expect(typeof firstRecord.phoneNumber).toBe('string'); - expect(firstRecord.phoneNumber.length).toBeGreaterThan(0); - } else { - fail('All industry classifications should return a substantial array of companies'); - } - }, - API_TIMEOUT, - ); + it('should get all industry classifications (v4)', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', industryTitle: 'Electronic Computers' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getAllIndustryClassifications(); + + expect(mockClient.get).toHaveBeenCalledWith( + '/standard_industrial_classification/all', + 'v4', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getIndustryClassificationCodes', () => { - it( - 'should fetch industry classification codes by industry title and validate data', - async () => { - if (shouldSkipTests()) { - console.log( - 'Skipping industry classification codes by title test - no API key available', - ); - return; - } - const result = await fmp.sec.getIndustryClassificationCodes({ - industryTitle: 'services', - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - // Should return multiple SIC codes for services - expect(result.data.length).toBeGreaterThan(0); - - // Validate first record structure - const firstRecord = result.data[0]; - expect(firstRecord.office).toBeDefined(); - expect(firstRecord.sicCode).toBeDefined(); - expect(firstRecord.industryTitle).toBeDefined(); - - // Validate data types and content - expect(typeof firstRecord.office).toBe('string'); - expect(firstRecord.office.length).toBeGreaterThan(0); - expect(typeof firstRecord.sicCode).toBe('string'); - expect(firstRecord.sicCode.length).toBeGreaterThan(0); - expect(typeof firstRecord.industryTitle).toBe('string'); - expect(firstRecord.industryTitle.length).toBeGreaterThan(0); - expect(firstRecord.industryTitle.toLowerCase()).toContain('service'); - } else { - fail('Industry classification codes should return an array of SIC codes for services'); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch industry classification codes by SIC code and validate data', - async () => { - if (shouldSkipTests()) { - console.log('Skipping industry classification codes by SIC test - no API key available'); - return; - } - const result = await fmp.sec.getIndustryClassificationCodes({ - sicCode: 6321, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - expect(result.data.length).toBeGreaterThan(0); - - const firstRecord = result.data[0]; - expect(firstRecord.sicCode).toBe('6321'); - expect(firstRecord.office).toBeDefined(); - expect(firstRecord.office.length).toBeGreaterThan(0); - expect(firstRecord.industryTitle).toBeDefined(); - expect(firstRecord.industryTitle.length).toBeGreaterThan(0); - } else { - fail('Industry classification codes should return data for SIC code 6321'); - } - }, - FAST_TIMEOUT, - ); + it('should get industry classification codes passing params through (v4)', async () => { + const mockResponse = { + success: true, + data: [{ sicCode: '3571', industryTitle: 'Electronic Computers' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const params = { sicCode: 3571 }; + const result = await endpoints.getIndustryClassificationCodes(params); + + expect(mockClient.get).toHaveBeenCalledWith( + '/standard_industrial_classification_list', + 'v4', + params, + ); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/senate-house.test.ts b/packages/api/src/__tests__/endpoints/senate-house.test.ts index 6161ccc..37fc9aa 100644 --- a/packages/api/src/__tests__/endpoints/senate-house.test.ts +++ b/packages/api/src/__tests__/endpoints/senate-house.test.ts @@ -1,392 +1,143 @@ -import { FMP } from '../../fmp'; -import { FAST_TIMEOUT } from '../utils/test-setup'; +import { SenateHouseEndpoints } from '../../endpoints/senate-house'; +import { FMPClient } from '../../client'; + +// Mock the FMPClient +jest.mock('../../client'); describe('SenateHouseEndpoints', () => { - let fmp: FMP; + let endpoints: SenateHouseEndpoints; + let mockClient: jest.Mocked; beforeEach(() => { - fmp = new FMP(); + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + endpoints = new SenateHouseEndpoints(mockClient); }); describe('getSenateTrading', () => { - it( - 'should get senate trading data for a specific symbol', - async () => { - const result = await fmp.senateHouse.getSenateTrading({ - symbol: 'AAPL', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Type safety test - verify SenateTradingResponse structure - if (result.success && result.data) { - const senateData = result.data; - expect(Array.isArray(senateData)).toBe(true); - - if (senateData.length > 0) { - const firstItem = senateData[0]; - // Check for SenateTradingResponse specific fields - expect(firstItem).toHaveProperty('firstName'); - expect(firstItem).toHaveProperty('lastName'); - expect(firstItem).toHaveProperty('office'); - expect(firstItem).toHaveProperty('disclosureDate'); - expect(firstItem).toHaveProperty('transactionDate'); - expect(firstItem).toHaveProperty('owner'); - expect(firstItem).toHaveProperty('assetDescription'); - expect(firstItem).toHaveProperty('assetType'); - expect(firstItem).toHaveProperty('type'); - expect(firstItem).toHaveProperty('amount'); - expect(firstItem).toHaveProperty('comment'); - expect(firstItem).toHaveProperty('symbol'); - // Note: Both Senate and House now have similar fields in unified structure - } - } - }, - 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, - ); + it('should get senate trading by symbol (stable)', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', firstName: 'Jane', lastName: 'Doe' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getSenateTrading({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith('/senate-trades', 'stable', { symbol: 'AAPL' }); + expect(result).toEqual(mockResponse); + }); }); describe('getSenateTradingRSSFeed', () => { - it( - 'should get senate trading RSS feed for page 0', - async () => { - const result = await fmp.senateHouse.getSenateTradingRSSFeed({ - page: 0, - limit: 5, - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); + it('should get senate RSS feed with default limit of 100', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); - // Type safety test - verify SenateTradingResponse structure - if (result.success && result.data) { - const senateData = result.data; - expect(Array.isArray(senateData)).toBe(true); + const result = await endpoints.getSenateTradingRSSFeed({ page: 0 }); - 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 - } - } - }, - FAST_TIMEOUT, - ); - - it( - 'should get senate trading RSS feed for different pages', - async () => { - const result = await fmp.senateHouse.getSenateTradingRSSFeed({ - page: 1, - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - }, - FAST_TIMEOUT, - ); - - it( - 'should get senate trading RSS feed for page 2', - async () => { - const result = await fmp.senateHouse.getSenateTradingRSSFeed({ - page: 2, - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - }, - FAST_TIMEOUT, - ); - }); - - describe('getHouseTrading', () => { - it( - 'should get house trading data for a specific symbol', - async () => { - const result = await fmp.senateHouse.getHouseTrading({ - symbol: 'AAPL', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Type safety test - verify HouseTradingResponse structure - if (result.success && result.data) { - const houseData = result.data; - expect(Array.isArray(houseData)).toBe(true); - - if (houseData.length > 0) { - const firstItem = houseData[0]; - // Check for HouseTradingResponse specific fields - expect(firstItem).toHaveProperty('disclosureDate'); - expect(firstItem).toHaveProperty('transactionDate'); - expect(firstItem).toHaveProperty('owner'); - expect(firstItem).toHaveProperty('symbol'); - expect(firstItem).toHaveProperty('assetDescription'); - expect(firstItem).toHaveProperty('type'); - expect(firstItem).toHaveProperty('amount'); - expect(firstItem).toHaveProperty('firstName'); - expect(firstItem).toHaveProperty('lastName'); - expect(firstItem).toHaveProperty('district'); - expect(firstItem).toHaveProperty('link'); - expect(firstItem).toHaveProperty('capitalGainsOver200USD'); - // Note: Both Senate and House now have office field in unified structure - } - } - }, - 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({ + expect(mockClient.get).toHaveBeenCalledWith('/senate-latest', 'stable', { page: 0, - limit: 5, + limit: 100, }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Type safety test - this would catch the type mismatch - if (result.success && result.data) { - // This should be HouseTradingResponse[], not SenateTradingResponse[] - 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('firstName'); - expect(firstItem).toHaveProperty('lastName'); - expect(firstItem).toHaveProperty('district'); - expect(firstItem).toHaveProperty('capitalGainsOver200USD'); - // Note: Both Senate and House now have office field in unified structure - } - } + expect(result).toEqual(mockResponse); }); - 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 pass through explicit limit', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); - it( - 'should get house trading RSS feed for page 2', - async () => { - const result = await fmp.senateHouse.getHouseTradingRSSFeed({ - page: 2, - }); + const result = await endpoints.getSenateTradingRSSFeed({ page: 1, limit: 5 }); - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - }, - FAST_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/senate-latest', 'stable', { + page: 1, + limit: 5, + }); + expect(result).toEqual(mockResponse); + }); }); 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); + it('should get senate trading by name (stable)', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', firstName: 'Jerry' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getSenateTradingByName({ name: 'Jerry' }); + + expect(mockClient.get).toHaveBeenCalledWith('/senate-trades-by-name', 'stable', { + name: 'Jerry', + }); + expect(result).toEqual(mockResponse); + }); + }); - 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'); - } - } - }, - FAST_TIMEOUT, - ); + describe('getHouseTrading', () => { + it('should get house trading by symbol (stable)', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', district: 'CA-12' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getHouseTrading({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith('/house-trades', 'stable', { symbol: 'AAPL' }); + expect(result).toEqual(mockResponse); + }); + }); - it( - 'should get senate trading data by different names', - async () => { - const result = await fmp.senateHouse.getSenateTradingByName({ - name: 'John', - }); + describe('getHouseTradingRSSFeed', () => { + it('should get house RSS feed with default limit of 100', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); + const result = await endpoints.getHouseTradingRSSFeed({ page: 0 }); - if (result.success && result.data) { - expect(Array.isArray(result.data)).toBe(true); - if (result.data.length > 0) { - // Data found as expected - } else { - // No data found as expected - } - } - }, - FAST_TIMEOUT, - ); + expect(mockClient.get).toHaveBeenCalledWith('/house-latest', 'stable', { + page: 0, + limit: 100, + }); + expect(result).toEqual(mockResponse); + }); - it( - 'should handle empty results gracefully', - async () => { - const result = await fmp.senateHouse.getSenateTradingByName({ - name: 'NonExistentName123', - }); + it('should pass through explicit limit', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); + const result = await endpoints.getHouseTradingRSSFeed({ page: 2, limit: 5 }); - // 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, - ); + expect(mockClient.get).toHaveBeenCalledWith('/house-latest', 'stable', { + page: 2, + limit: 5, + }); + expect(result).toEqual(mockResponse); + }); }); describe('getHouseTradingByName', () => { - it( - 'should get house trading data by name', - async () => { - const result = await fmp.senateHouse.getHouseTradingByName({ - name: 'Nancy', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Type safety test - verify SenateHouseTradingByNameResponse structure - if (result.success && result.data) { - const houseData = result.data; - expect(Array.isArray(houseData)).toBe(true); - - if (houseData.length > 0) { - const firstItem = houseData[0]; - // Check for SenateHouseTradingByNameResponse specific fields - expect(firstItem).toHaveProperty('symbol'); - expect(firstItem).toHaveProperty('disclosureDate'); - expect(firstItem).toHaveProperty('transactionDate'); - expect(firstItem).toHaveProperty('firstName'); - expect(firstItem).toHaveProperty('lastName'); - expect(firstItem).toHaveProperty('office'); - expect(firstItem).toHaveProperty('district'); - expect(firstItem).toHaveProperty('owner'); - expect(firstItem).toHaveProperty('assetDescription'); - expect(firstItem).toHaveProperty('assetType'); - expect(firstItem).toHaveProperty('type'); - expect(firstItem).toHaveProperty('amount'); - expect(firstItem).toHaveProperty('capitalGainsOver200USD'); - expect(firstItem).toHaveProperty('comment'); - expect(firstItem).toHaveProperty('link'); - } - } - }, - 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 - } - } - }, - 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, - ); + it('should get house trading by name (stable)', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', firstName: 'Nancy' }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await endpoints.getHouseTradingByName({ name: 'Nancy' }); + + expect(mockClient.get).toHaveBeenCalledWith('/house-trades-by-name', 'stable', { + name: 'Nancy', + }); + expect(result).toEqual(mockResponse); + }); }); }); diff --git a/packages/api/src/__tests__/endpoints/stock.test.ts b/packages/api/src/__tests__/endpoints/stock.test.ts index 7e310a8..95701bd 100644 --- a/packages/api/src/__tests__/endpoints/stock.test.ts +++ b/packages/api/src/__tests__/endpoints/stock.test.ts @@ -1,369 +1,107 @@ -import { FMP } from '../../fmp'; -import { - shouldSkipTests, - createTestClient, - API_TIMEOUT, - FAST_TIMEOUT, - TEST_SYMBOLS, -} from '../utils/test-setup'; -import { StockSplitResponse } from 'fmp-node-types'; +import { StockEndpoints } from '../../endpoints/stock'; +import { FMPClient } from '../../client'; -describe('Stock Endpoints', () => { - let fmp: FMP; +// Mock the FMPClient +jest.mock('../../client'); - beforeAll(() => { - if (shouldSkipTests()) { - console.log('Skipping stock tests - no API key available'); - return; - } - fmp = createTestClient(); +describe('StockEndpoints', () => { + let stockEndpoints: StockEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + stockEndpoints = new StockEndpoints(mockClient); }); describe('getMarketCap', () => { - it( - 'should fetch market cap for AAPL with comprehensive validation', - async () => { - if (shouldSkipTests()) { - console.log('Skipping market cap test - no API key available'); - return; - } - const result = await fmp.stock.getMarketCap(TEST_SYMBOLS.STOCK); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - // Enhanced data validation - const dataKeys = Object.keys(result.data || {}); - expect(dataKeys.length).toBeGreaterThan(0); - - if (result.data) { - const marketCap = Array.isArray(result.data) ? result.data[0] : result.data; - expect(marketCap.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(marketCap.marketCap).toBeGreaterThan(0); - - // Additional validation - expect(typeof marketCap.marketCap).toBe('number'); - expect(marketCap.marketCap).toBeGreaterThan(1000000000); // Should be > $1B for AAPL - } - }, - FAST_TIMEOUT, - ); + it('should get market cap using /market-capitalization/{symbol} endpoint', async () => { + const mockResponse = { + success: true, + data: { symbol: 'AAPL', marketCap: 3000000000000 }, + error: null, + status: 200, + }; + mockClient.getSingle.mockResolvedValue(mockResponse); + + const result = await stockEndpoints.getMarketCap('AAPL'); + + expect(mockClient.getSingle).toHaveBeenCalledWith('/market-capitalization/AAPL', 'v3'); + expect(result).toEqual(mockResponse); + }); }); describe('getStockSplits', () => { - it( - 'should fetch stock splits for AAPL with comprehensive validation', - async () => { - if (shouldSkipTests()) { - console.log('Skipping stock splits test - no API key available'); - return; - } - const result = await fmp.stock.getStockSplits(TEST_SYMBOLS.STOCK); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - // Stock splits returns a single object with historical array - expect(typeof result.data).toBe('object'); - if (result.data && typeof result.data === 'object' && 'symbol' in result.data) { - expect(result.data.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(result.data).toHaveProperty('historical'); - expect(Array.isArray((result.data as StockSplitResponse).historical)).toBe(true); - - const historical = (result.data as StockSplitResponse).historical; - if (historical && historical.length > 0) { - // Validate first record structure - const firstRecord = historical[0]; - expect(firstRecord.date).toBeDefined(); - expect(firstRecord.label).toBeDefined(); - expect(firstRecord.numerator).toBeDefined(); - expect(firstRecord.denominator).toBeDefined(); - - // Validate numeric fields - expect(typeof firstRecord.numerator).toBe('number'); - expect(typeof firstRecord.denominator).toBe('number'); - expect(firstRecord.numerator).toBeGreaterThan(0); - expect(firstRecord.denominator).toBeGreaterThan(0); - } else { - console.log('⚠️ Stock splits returned empty historical array'); - } - } - }, - FAST_TIMEOUT, - ); + it('should get stock splits using /historical-price-full/stock_split/{symbol} endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', historical: [] }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await stockEndpoints.getStockSplits('AAPL'); + + expect(mockClient.get).toHaveBeenCalledWith('/historical-price-full/stock_split/AAPL', 'v3'); + expect(result).toEqual(mockResponse); + }); }); describe('getDividendHistory', () => { - it( - 'should fetch dividend history for AAPL with comprehensive validation', - async () => { - if (shouldSkipTests()) { - console.log('Skipping dividend history test - no API key available'); - return; - } - const result = await fmp.stock.getDividendHistory(TEST_SYMBOLS.STOCK); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - // Dividend history returns a single object with historical array - expect(typeof result.data).toBe('object'); - if (result.data && typeof result.data === 'object' && 'symbol' in result.data) { - expect(result.data.symbol).toBe(TEST_SYMBOLS.STOCK); - expect(result.data).toHaveProperty('historical'); - expect(Array.isArray(result.data.historical)).toBe(true); - - const historical = result.data.historical; - if (historical && historical.length > 0) { - // Validate first record structure - const firstRecord = historical[0]; - expect(firstRecord.date).toBeDefined(); - expect(firstRecord.label).toBeDefined(); - expect(firstRecord.adjDividend).toBeDefined(); - expect(firstRecord.dividend).toBeDefined(); - expect(firstRecord.recordDate).toBeDefined(); - expect(firstRecord.paymentDate).toBeDefined(); - expect(firstRecord.declarationDate).toBeDefined(); - - // Validate numeric fields - expect(typeof firstRecord.adjDividend).toBe('number'); - expect(typeof firstRecord.dividend).toBe('number'); - expect(firstRecord.adjDividend).toBeGreaterThanOrEqual(0); - expect(firstRecord.dividend).toBeGreaterThanOrEqual(0); - } else { - console.log('⚠️ Dividend history returned empty historical array'); - } - } - }, - FAST_TIMEOUT, - ); + it('should get dividend history using /historical-price-full/stock_dividend/{symbol} endpoint', async () => { + const mockResponse = { + success: true, + data: { symbol: 'KO', historical: [] }, + error: null, + status: 200, + }; + mockClient.getSingle.mockResolvedValue(mockResponse); + + const result = await stockEndpoints.getDividendHistory('KO'); + + expect(mockClient.getSingle).toHaveBeenCalledWith( + '/historical-price-full/stock_dividend/KO', + 'v3', + ); + expect(result).toEqual(mockResponse); + }); }); describe('getRealTimePrice', () => { - it( - 'should fetch real time price for single stock with comprehensive validation', - async () => { - if (shouldSkipTests()) { - console.log('Skipping real time price test - no API key available'); - return; - } - const result = await fmp.stock.getRealTimePrice([TEST_SYMBOLS.STOCK]); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - // Validate that at least one record matches the symbol - const found = result.data.find(r => r.symbol === TEST_SYMBOLS.STOCK); - expect(found).toBeDefined(); - expect(found!.price).toBeDefined(); - expect(typeof found!.price).toBe('number'); - expect(found!.price).toBeGreaterThan(0); - } else { - console.log('⚠️ Real time price returned empty array'); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch real time price for multiple stocks', - async () => { - if (shouldSkipTests()) { - console.log('Skipping multiple stocks real time price test - no API key available'); - return; - } - const symbols = [TEST_SYMBOLS.STOCK, 'MSFT', 'GOOGL']; - const result = await fmp.stock.getRealTimePrice(symbols); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - // Validate that we got data for at least some of the requested symbols - const returnedSymbols = result.data.map(item => item.symbol); - const hasRequestedSymbols = symbols.some(symbol => returnedSymbols.includes(symbol)); - expect(hasRequestedSymbols).toBe(true); - - // Validate record structure - result.data.forEach(record => { - expect(record.symbol).toBeDefined(); - expect(record.price).toBeDefined(); - expect(typeof record.price).toBe('number'); - expect(record.price).toBeGreaterThan(0); - }); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch real time price for all stocks when symbols array is empty', - async () => { - if (shouldSkipTests()) { - console.log('Skipping all stocks real time price test - no API key available'); - return; - } - const result = await fmp.stock.getRealTimePrice([]); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - // Should return a substantial number of stocks - expect(result.data.length).toBeGreaterThan(100); - - // Validate record structure - const firstRecord = result.data[0]; - expect(firstRecord.symbol).toBeDefined(); - expect(firstRecord.price).toBeDefined(); - expect(typeof firstRecord.price).toBe('number'); - expect(firstRecord.price).toBeGreaterThan(0); - } else { - console.log('⚠️ All stocks real time price returned empty array'); - } - }, - API_TIMEOUT, - ); + it('should get real-time prices using stock/real-time-price/{symbols} endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', price: 150 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await stockEndpoints.getRealTimePrice(['AAPL', 'MSFT']); + + expect(mockClient.get).toHaveBeenCalledWith('stock/real-time-price/AAPL,MSFT', 'v3'); + // Method filters the array data; with a valid symbol it is returned unchanged + expect(result).toEqual({ ...mockResponse, data: [{ symbol: 'AAPL', price: 150 }] }); + }); }); describe('getRealTimePriceForMultipleStocks', () => { - it( - 'should fetch full real time price data for single stock', - async () => { - if (shouldSkipTests()) { - console.log('Skipping full real time price test - no API key available'); - return; - } - const result = await fmp.stock.getRealTimePriceForMultipleStocks([TEST_SYMBOLS.STOCK]); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - // Validate that at least one record matches the symbol - const found = result.data.find(r => r.symbol === TEST_SYMBOLS.STOCK); - expect(found).toBeDefined(); - expect(found!.bidPrice).toBeDefined(); - expect(found!.askPrice).toBeDefined(); - expect(found!.lastSalePrice).toBeDefined(); - expect(found!.volume).toBeDefined(); - expect(found!.bidSize).toBeDefined(); - expect(found!.askSize).toBeDefined(); - expect(found!.lastSaleSize).toBeDefined(); - expect(found!.lastSaleTime).toBeDefined(); - expect(found!.fmpLast).toBeDefined(); - expect(found!.lastUpdated).toBeDefined(); - - // Validate numeric fields - expect(typeof found!.bidPrice).toBe('number'); - expect(typeof found!.askPrice).toBe('number'); - expect(typeof found!.lastSalePrice).toBe('number'); - expect(typeof found!.volume).toBe('number'); - expect(typeof found!.bidSize).toBe('number'); - expect(typeof found!.askSize).toBe('number'); - expect(typeof found!.lastSaleSize).toBe('number'); - expect(typeof found!.lastSaleTime).toBe('number'); - expect(typeof found!.fmpLast).toBe('number'); - expect(typeof found!.lastUpdated).toBe('number'); - } else { - console.log('⚠️ Full real time price returned empty array'); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch full real time price data for multiple stocks', - async () => { - if (shouldSkipTests()) { - console.log('Skipping multiple stocks full real time price test - no API key available'); - return; - } - const symbols = [TEST_SYMBOLS.STOCK, 'MSFT', 'GOOGL']; - const result = await fmp.stock.getRealTimePriceForMultipleStocks(symbols); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - // Validate that we got data for at least some of the requested symbols - const returnedSymbols = result.data.map(item => item.symbol); - const hasRequestedSymbols = symbols.some(symbol => returnedSymbols.includes(symbol)); - expect(hasRequestedSymbols).toBe(true); - - // Validate record structure - result.data.forEach(record => { - expect(record.symbol).toBeDefined(); - expect(record.bidPrice).toBeDefined(); - expect(record.askPrice).toBeDefined(); - expect(record.lastSalePrice).toBeDefined(); - expect(record.volume).toBeDefined(); - expect(typeof record.bidPrice).toBe('number'); - expect(typeof record.askPrice).toBe('number'); - expect(typeof record.lastSalePrice).toBe('number'); - expect(typeof record.volume).toBe('number'); - }); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should fetch full real time price data for all stocks when symbols array is empty', - async () => { - if (shouldSkipTests()) { - console.log('Skipping all stocks full real time price test - no API key available'); - return; - } - const result = await fmp.stock.getRealTimePriceForMultipleStocks([]); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data && result.data.length > 0) { - // Should return a substantial number of stocks - expect(result.data.length).toBeGreaterThan(100); - - // Validate record structure - be flexible about which properties exist - const firstRecord = result.data[0]; - if (firstRecord && firstRecord.symbol) { - expect(firstRecord.symbol).toBeDefined(); - expect(typeof firstRecord.symbol).toBe('string'); - - // Check for common properties that might exist - if ('bidPrice' in firstRecord) { - expect(typeof firstRecord.bidPrice).toBe('number'); - } - if ('askPrice' in firstRecord) { - expect(typeof firstRecord.askPrice).toBe('number'); - } - if ('lastSalePrice' in firstRecord) { - expect(typeof firstRecord.lastSalePrice).toBe('number'); - } - if ('volume' in firstRecord) { - expect(typeof firstRecord.volume).toBe('number'); - } - if ('price' in firstRecord) { - expect(typeof firstRecord.price).toBe('number'); - } - } else { - console.log('⚠️ First record is invalid or missing required properties'); - } - } else { - console.log( - '⚠️ All stocks full real time price returned empty array (API limitation or no data)', - ); - // Don't run assertions if no data is returned - } - }, - API_TIMEOUT, - ); + it('should get full real-time prices using stock/full/real-time-price/{symbols} endpoint', async () => { + const mockResponse = { + success: true, + data: [{ symbol: 'AAPL', bidPrice: 149, askPrice: 151 }], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await stockEndpoints.getRealTimePriceForMultipleStocks(['AAPL', 'MSFT']); + + expect(mockClient.get).toHaveBeenCalledWith('stock/full/real-time-price/AAPL,MSFT', 'v3'); + expect(result).toEqual({ + ...mockResponse, + data: [{ symbol: 'AAPL', bidPrice: 149, askPrice: 151 }], + }); + }); }); }); diff --git a/packages/api/src/__tests__/fmp.test.ts b/packages/api/src/__tests__/fmp.test.ts index 819a24e..8e8e884 100644 --- a/packages/api/src/__tests__/fmp.test.ts +++ b/packages/api/src/__tests__/fmp.test.ts @@ -1,6 +1,5 @@ import { FMP } from '../fmp'; import { FMPClient } from '../client'; -import { API_KEY } from './utils/test-setup'; // Mock the environment variable const originalEnv = process.env.FMP_API_KEY; @@ -123,16 +122,3 @@ describe('FMP', () => { }); }); }); - -describe('FMP API Smoke Test', () => { - it('should have an API key set for integration tests', () => { - if (!API_KEY) { - console.log('⚠️ No FMP_API_KEY found - integration tests will be skipped'); - console.log( - ' Set FMP_API_KEY in your .env file or environment variables to run integration tests', - ); - } - expect(typeof API_KEY).toBe('string'); - expect(API_KEY?.length || 0).toBeGreaterThan(0); - }); -}); diff --git a/packages/api/src/__tests__/integration.test.ts b/packages/api/src/__tests__/integration.test.ts deleted file mode 100644 index 63b2048..0000000 --- a/packages/api/src/__tests__/integration.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { FMP } from '../fmp'; -import { API_KEY, isCI } from './utils/test-setup'; - -// Helper function to safely access data that could be an array or single object -function getFirstItem(data: T | T[]): T { - return Array.isArray(data) ? data[0] : data; -} - -describe('FMP API Integration Tests', () => { - if (!API_KEY || isCI) { - it('should skip tests when no API key is provided or running in CI', () => { - if (isCI) { - console.log('Skipping integration tests - running in CI environment'); - } else { - console.log('Skipping integration tests - no FMP_API_KEY found in environment'); - } - expect(true).toBe(true); - }); - return; - } - - let fmp: FMP; - - beforeAll(() => { - if (!API_KEY) { - throw new Error('FMP_API_KEY is required for testing'); - } - fmp = new FMP({ - apiKey: API_KEY, - }); - }); - - describe('Company Profile', () => { - it('should fetch company profile for AAPL', async () => { - const result = await fmp.company.getCompanyProfile('AAPL'); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Object.keys(result.data || {}).length).toBeGreaterThan(0); - - if (result.data) { - const profile = result.data; - expect(profile.symbol).toBe('AAPL'); - expect(profile.companyName).toBeDefined(); - expect(profile.industry).toBeDefined(); - expect(profile.sector).toBeDefined(); - } - }, 10000); - }); - - describe('Financial Statements', () => { - it('should fetch income statement for AAPL', 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); - - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const statement = getFirstItem(result.data); - expect(statement.symbol).toBe('AAPL'); - expect(statement.date).toBeDefined(); - expect(['annual', 'FY']).toContain(statement.period); - } - }, 15000); - }); - - describe('Market Data', () => { - it('should fetch market hours', async () => { - const result = await fmp.market.getMarketHours(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(result.data?.isTheStockMarketOpen).toBeDefined(); - }, 10000); - }); -}); diff --git a/packages/api/src/__tests__/live/validate.test.ts b/packages/api/src/__tests__/live/validate.test.ts new file mode 100644 index 0000000..5cbd782 --- /dev/null +++ b/packages/api/src/__tests__/live/validate.test.ts @@ -0,0 +1,96 @@ +import { z } from 'zod'; +import { + classifyTransport, + classifyShape, + classifyResult, + type TransportResult, +} from '../../live/validate'; + +const Sample = z.object({ + symbol: z.string(), + price: z.number(), + marketCap: z.number().nullable(), +}); + +const ok = (over: Partial = {}): TransportResult => ({ + success: true, + data: null, + error: null, + status: 200, + ...over, +}); + +describe('classifyTransport', () => { + it('returns null when the request succeeded', () => { + expect(classifyTransport(ok())).toBeNull(); + }); + + it('classifies 402/403 as plan-locked SKIP', () => { + expect(classifyTransport({ success: false, data: null, error: 'no', status: 402 })?.outcome).toBe('SKIP'); + expect(classifyTransport({ success: false, data: null, error: 'no', status: 403 })?.outcome).toBe('SKIP'); + }); + + it('classifies plan-locked by message even on other status', () => { + const c = classifyTransport({ success: false, data: null, error: 'Exclusive Endpoint: upgrade', status: 200 }); + expect(c?.outcome).toBe('SKIP'); + }); + + it('classifies 429 as SKIP with stopRun', () => { + const c = classifyTransport({ success: false, data: null, error: 'Limit Reach', status: 429 }); + expect(c?.outcome).toBe('SKIP'); + expect(c?.stopRun).toBe(true); + }); + + it('classifies other failures as FAIL', () => { + expect(classifyTransport({ success: false, data: null, error: 'boom', status: 500 })?.outcome).toBe('FAIL'); + }); +}); + +describe('classifyShape', () => { + it('PASS on a clean object (incl. allowed null on nullable field)', () => { + expect(classifyShape(Sample, { symbol: 'AAPL', price: 1, marketCap: null }).outcome).toBe('PASS'); + }); + + it('FAIL on a missing required field', () => { + expect(classifyShape(Sample, { symbol: 'AAPL', marketCap: 1 }).outcome).toBe('FAIL'); + }); + + it('FAIL on a wrong (non-null) type', () => { + expect(classifyShape(Sample, { symbol: 'AAPL', price: 'nope', marketCap: 1 }).outcome).toBe('FAIL'); + }); + + it('DRIFT when a non-nullable field comes back null', () => { + expect(classifyShape(Sample, { symbol: 'AAPL', price: null, marketCap: 1 }).outcome).toBe('DRIFT'); + }); + + it('DRIFT on an extra top-level key', () => { + const c = classifyShape(Sample, { symbol: 'AAPL', price: 1, marketCap: 1, currency: 'USD' }); + expect(c.outcome).toBe('DRIFT'); + expect(c.detail).toContain('currency'); + }); +}); + +describe('classifyResult', () => { + it('SKIPs a transport failure before shape checks', () => { + expect(classifyResult({ success: false, data: null, error: 'x', status: 402 }, Sample, 'object').outcome).toBe('SKIP'); + }); + + it('validates an object payload', () => { + expect(classifyResult(ok({ data: { symbol: 'AAPL', price: 1, marketCap: 1 } }), Sample, 'object').outcome).toBe('PASS'); + }); + + it('treats an empty array as PASS (0 rows)', () => { + const c = classifyResult(ok({ data: [] }), Sample, 'array'); + expect(c.outcome).toBe('PASS'); + expect(c.detail).toContain('0 rows'); + }); + + it('FAILs an array whose elements break the schema', () => { + const c = classifyResult(ok({ data: [{ symbol: 'AAPL', price: 'bad', marketCap: 1 }] }), Sample, 'array'); + expect(c.outcome).toBe('FAIL'); + }); + + it('FAILs when an array is expected but a non-array is returned', () => { + expect(classifyResult(ok({ data: { not: 'an array' } }), Sample, 'array').outcome).toBe('FAIL'); + }); +}); diff --git a/packages/api/src/__tests__/utils/test-setup.ts b/packages/api/src/__tests__/utils/test-setup.ts deleted file mode 100644 index 93d850c..0000000 --- a/packages/api/src/__tests__/utils/test-setup.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Test utilities for FMP API tests - -import { FMP } from '../../fmp'; - -// Get API key from environment (loaded by Jest setup) -export const API_KEY = process.env.FMP_API_KEY; -export const isCI = process.env.CI === 'true'; - -/** - * Check if tests should be skipped - * Only skip if no API key is available - */ -export function shouldSkipTests(): boolean { - return !API_KEY; -} - -/** - * Create FMP client for testing - */ -export function createTestClient(): FMP { - if (!API_KEY) { - throw new Error('FMP_API_KEY is required for testing'); - } - return new FMP({ apiKey: API_KEY }); -} - -/** - * Common test timeout for API calls - */ -export const API_TIMEOUT = 30000; - -/** - * Common test timeout for faster operations - */ -export const FAST_TIMEOUT = 10000; - -/** - * Test data constants - */ -export const TEST_SYMBOLS = { - STOCK: 'AAPL', - FOREX: 'EURUSD', - CRYPTO: 'BTCUSD', - ETF: 'SPY', -} as const; - -/** - * Test date ranges - */ -export const TEST_DATE_RANGES = { - RECENT: { - from: '2024-01-01', - to: '2024-01-31', - }, - HISTORICAL: { - from: '2020-01-01', - to: '2024-01-31', - }, -} as const; diff --git a/packages/api/src/endpoints/market.ts b/packages/api/src/endpoints/market.ts index c620fbd..67be576 100644 --- a/packages/api/src/endpoints/market.ts +++ b/packages/api/src/endpoints/market.ts @@ -74,7 +74,7 @@ export class MarketEndpoints { * * @see {@link https://site.financialmodelingprep.com/developer/docs#market-index-market-overview|FMP Market Performance Documentation} */ - async getMarketPerformance(): Promise> { + async getMarketPerformance(): Promise> { return this.client.get('/quotes/index', 'v3'); } diff --git a/packages/api/src/endpoints/quote.ts b/packages/api/src/endpoints/quote.ts index a97e929..288a4d9 100644 --- a/packages/api/src/endpoints/quote.ts +++ b/packages/api/src/endpoints/quote.ts @@ -1,7 +1,7 @@ // Unified quote endpoints for FMP API - handles stocks, crypto, forex, commodities, and ETFs import { FMPClient } from '@/client'; -import { APIResponse, Quote, HistoricalPriceResponse, HistoricalPriceData } from 'fmp-node-types'; +import { APIResponse, Quote, HistoricalPriceResponse, IntradayPrice } from 'fmp-node-types'; export class QuoteEndpoints { constructor(private client: FMPClient) {} @@ -144,7 +144,7 @@ export class QuoteEndpoints { interval: '1min' | '5min' | '15min' | '30min' | '1hour' | '4hour'; from?: string; to?: string; - }): Promise> { + }): Promise> { const { symbol, interval, from, to } = params; const queryParams: { from?: string; to?: string } = {}; diff --git a/packages/api/src/live/validate.ts b/packages/api/src/live/validate.ts new file mode 100644 index 0000000..e7beb03 --- /dev/null +++ b/packages/api/src/live/validate.ts @@ -0,0 +1,150 @@ +// Relaxed shape validator + result classifier for the live-API check tool. +// +// The canonical schemas in `fmp-node-types` are strict (they ship to users). +// Here we wrap them in a tolerant mode so FMP's real-world looseness surfaces +// as DRIFT signals rather than false FAILs: +// +// PASS - response matches the schema's known fields +// FAIL - an expected field is missing, or has the wrong (non-null) type +// SKIP - transport-level: plan-locked (402/403) or quota/rate-limited (429) +// DRIFT - extra top-level keys, or a non-nullable field came back null +// +// Note: extra-key (DRIFT) detection is top-level only; extra keys nested inside +// sub-objects are not reported in this iteration. + +import { z } from 'zod'; + +export type Outcome = 'PASS' | 'FAIL' | 'SKIP' | 'DRIFT'; + +/** The subset of `APIResponse` the classifier needs. */ +export interface TransportResult { + success: boolean; + data: unknown; + error: string | null; + status: number; +} + +export interface Classification { + outcome: Outcome; + detail: string; + /** Set when a quota/rate limit was hit and the run should stop early. */ + stopRun?: boolean; +} + +const OUTCOME_RANK: Record = { PASS: 0, DRIFT: 1, SKIP: 2, FAIL: 3 }; + +const PLAN_LOCKED_PATTERNS = [ + /exclusive endpoint/i, + /not available under your current/i, + /premium/i, + /special endpoint/i, + /upgrade your plan/i, +]; + +/** + * Classify a transport-level outcome. Returns null when the request succeeded + * (so the caller proceeds to shape validation). + */ +export function classifyTransport(res: TransportResult): Classification | null { + if (res.success) return null; + + const error = res.error ?? ''; + + if (res.status === 429) { + return { outcome: 'SKIP', detail: `quota/rate limit (429): ${error}`.trim(), stopRun: true }; + } + + const planLocked = + res.status === 402 || + res.status === 403 || + PLAN_LOCKED_PATTERNS.some((re) => re.test(error)); + + if (planLocked) { + return { outcome: 'SKIP', detail: `plan-locked (${res.status}): ${error}`.trim() }; + } + + return { outcome: 'FAIL', detail: `request failed (${res.status}): ${error || 'unknown error'}` }; +} + +/** Classify a single object value against an (object) schema. */ +export function classifyShape(schema: z.ZodTypeAny, value: unknown): Classification { + const driftDetails: string[] = []; + + // Pass 1: tolerate unknown keys; judge known fields. + const passthrough = schema instanceof z.ZodObject ? schema.passthrough() : schema; + const result = passthrough.safeParse(value); + + if (!result.success) { + const failIssues: string[] = []; + for (const issue of result.error.issues) { + const path = issue.path.join('.') || '(root)'; + if (issue.code === 'invalid_type') { + if (issue.received === 'null') { + // Non-nullable field came back null -> candidate to make schema nullable. + driftDetails.push(`${path}: null (expected ${issue.expected})`); + continue; + } + // 'undefined' (missing) or a genuinely wrong type -> contract failure. + failIssues.push(`${path}: expected ${issue.expected}, got ${issue.received}`); + } else { + failIssues.push(`${path}: ${issue.message}`); + } + } + if (failIssues.length > 0) { + return { outcome: 'FAIL', detail: failIssues.join('; ') }; + } + // Otherwise: only null-drift issues — fall through to PASS/DRIFT. + } + + // Pass 2: detect extra top-level keys (the API added fields our type lacks). + if (schema instanceof z.ZodObject) { + const strict = schema.strict().safeParse(value); + if (!strict.success) { + for (const issue of strict.error.issues) { + if (issue.code === 'unrecognized_keys') { + driftDetails.push(`extra keys: ${issue.keys.join(', ')}`); + } + } + } + } + + if (driftDetails.length > 0) { + return { outcome: 'DRIFT', detail: driftDetails.join('; ') }; + } + return { outcome: 'PASS', detail: '' }; +} + +/** + * Classify a full result: transport first, then shape. For array payloads the + * first `sampleSize` elements are validated and the worst outcome wins. + */ +export function classifyResult( + res: TransportResult, + schema: z.ZodTypeAny, + kind: 'object' | 'array', + sampleSize = 3, +): Classification { + const transport = classifyTransport(res); + if (transport) return transport; + + const data = res.data; + + if (kind === 'array') { + if (!Array.isArray(data)) { + return { outcome: 'FAIL', detail: `expected array, got ${data === null ? 'null' : typeof data}` }; + } + if (data.length === 0) { + return { outcome: 'PASS', detail: '0 rows' }; + } + let worst: Classification = { outcome: 'PASS', detail: `${data.length} rows` }; + data.slice(0, sampleSize).forEach((el, i) => { + const c = classifyShape(schema, el); + if (OUTCOME_RANK[c.outcome] > OUTCOME_RANK[worst.outcome]) { + worst = { outcome: c.outcome, detail: `[${i}] ${c.detail}` }; + } + }); + return worst; + } + + return classifyShape(schema, data); +} diff --git a/packages/types/package.json b/packages/types/package.json index 9a191fb..38e530e 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -21,6 +21,7 @@ "lint": "npx eslint src/**/*.ts", "lint:fix": "eslint src/**/*.ts --fix", "type-check": "tsc --noEmit", + "gen:schemas": "ts-to-zod src/quote.ts src/generated/quote.zod.ts && ts-to-zod src/market.ts src/generated/market.zod.ts && ts-to-zod src/financial.ts src/generated/financial.zod.ts", "format": "prettier --write src/**/*.ts", "format:check": "prettier --check src/**/*.ts", "clean": "rm -rf dist" @@ -43,7 +44,11 @@ "devDependencies": { "@types/node": "^20.11.0", "prettier": "^3.2.5", + "ts-to-zod": "^5.1.0", "tsup": "^8.0.0", "typescript": "^5.3.3" + }, + "dependencies": { + "zod": "^3.25.76" } } diff --git a/packages/types/src/calendar.ts b/packages/types/src/calendar.ts index 1df68c1..1d8a129 100644 --- a/packages/types/src/calendar.ts +++ b/packages/types/src/calendar.ts @@ -1,72 +1,79 @@ -// Earnings calendar data -export interface EarningsCalendar { - date: string; - symbol: string; - eps: number | null; - epsEstimated: number | null; - time: string; - revenue: number | null; - revenueEstimated: number | null; - fiscalDateEnding: string; - updatedFromDate: string; -} +// calendar types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -// Earnings confirmed data -export interface EarningsConfirmed { - symbol: string; - exchange: string; - time: string; - when: string; - date: string; - publicationDate: string; - title: string; - url: string; -} +export const EarningsCalendarSchema = z.object({ + date: z.string(), + symbol: z.string(), + eps: z.number().nullable(), + epsEstimated: z.number().nullable(), + time: z.string().nullable(), + revenue: z.number().nullable(), + revenueEstimated: z.number().nullable(), + fiscalDateEnding: z.string(), + updatedFromDate: z.string() +}); -// Dividends calendar data -export interface DividendsCalendar { - date: string; - label: string; - adjDividend: number; - symbol: string; - dividend: number; - recordDate: string; - paymentDate: string; - declarationDate: string; -} +export const EarningsConfirmedSchema = z.object({ + symbol: z.string(), + exchange: z.string(), + time: z.string(), + when: z.string(), + date: z.string(), + publicationDate: z.string(), + title: z.string(), + url: z.string() +}); -// Economics calendar data -export interface EconomicsCalendar { - date: string; - country: string; - event: string; - currency: string; - previous: number; - estimate: number | null; - actual: number | null; - change: number; - impact: string; - changePercentage: number; - unit: string | null; -} +export const DividendsCalendarSchema = z.object({ + date: z.string(), + label: z.string(), + adjDividend: z.number(), + symbol: z.string(), + dividend: z.number(), + recordDate: z.string().nullable(), + paymentDate: z.string().nullable(), + declarationDate: z.string().nullable() +}); -// IPO calendar data -export interface IPOCalendar { - date: string; - company: string; - symbol: string; - exchange: string; - actions: string; - shares: number | null; - priceRange: string | null; - marketCap: number | null; -} +export const EconomicsCalendarSchema = z.object({ + date: z.string(), + country: z.string(), + event: z.string(), + currency: z.string(), + previous: z.number().nullable(), + estimate: z.number().nullable(), + actual: z.number().nullable(), + change: z.number().nullable(), + impact: z.string(), + changePercentage: z.number(), + unit: z.string().nullable() +}); -// Splits calendar data -export interface SplitsCalendar { - date: string; - label: string; - symbol: string; - numerator: number; - denominator: number; -} +export const IPOCalendarSchema = z.object({ + date: z.string(), + company: z.string(), + symbol: z.string(), + exchange: z.string(), + actions: z.string(), + shares: z.number().nullable(), + priceRange: z.string().nullable(), + marketCap: z.number().nullable() +}); + +export const SplitsCalendarSchema = z.object({ + date: z.string(), + label: z.string(), + symbol: z.string(), + numerator: z.number(), + denominator: z.number() +}); + +export type EarningsCalendar = z.infer; +export type EarningsConfirmed = z.infer; +export type DividendsCalendar = z.infer; +export type EconomicsCalendar = z.infer; +export type IPOCalendar = z.infer; +export type SplitsCalendar = z.infer; diff --git a/packages/types/src/company.ts b/packages/types/src/company.ts index 503fe97..19c8c36 100644 --- a/packages/types/src/company.ts +++ b/packages/types/src/company.ts @@ -1,105 +1,118 @@ -// Company-related types for FMP API +// company types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -export interface CompanyProfile { - symbol: string; - price: number; - marketCap: number; - beta: number; - lastDividend: number; - range: string; - change: number; - changePercentage: number; - volume: number; - averageVolume: number; - companyName: string; - currency: string; - cik: string; - isin: string; - cusip: string; - exchangeFullName: string; - exchange: string; - industry: string; - website: string; - description: string; - ceo: string; - sector: string; - country: string; - fullTimeEmployees: string; - phone: string; - address: string; - city: string; - state: string; - zip: string; - image: string; - ipoDate: string; - defaultImage: boolean; - isEtf: boolean; - isActivelyTrading: boolean; - isAdr: boolean; - isFund: boolean; -} +export const CompanyProfileSchema = z.object({ + symbol: z.string(), + price: z.number(), + marketCap: z.number(), + beta: z.number(), + lastDividend: z.number(), + range: z.string(), + change: z.number(), + changePercentage: z.number(), + volume: z.number(), + averageVolume: z.number(), + companyName: z.string(), + currency: z.string(), + cik: z.string(), + isin: z.string(), + cusip: z.string(), + exchangeFullName: z.string(), + exchange: z.string(), + industry: z.string(), + website: z.string(), + description: z.string(), + ceo: z.string(), + sector: z.string(), + country: z.string(), + fullTimeEmployees: z.string(), + phone: z.string(), + address: z.string(), + city: z.string(), + state: z.string(), + zip: z.string(), + image: z.string(), + ipoDate: z.string(), + defaultImage: z.boolean(), + isEtf: z.boolean(), + isActivelyTrading: z.boolean(), + isAdr: z.boolean(), + isFund: z.boolean() +}); -export interface ExecutiveCompensation { - cik: string; - symbol: string; - companyName: string; - filingDate: string; - acceptedDate: string; - nameAndPosition: string; - year: number; - salary: number; - bonus: number; - stockAward: number; - optionAward: number; - incentivePlanCompensation: number; - allOtherCompensation: number; - total: number; - link: string; -} +export const ExecutiveCompensationSchema = z.object({ + cik: z.string(), + symbol: z.string(), + companyName: z.string(), + filingDate: z.string(), + acceptedDate: z.string(), + nameAndPosition: z.string(), + year: z.number(), + salary: z.number(), + bonus: z.number(), + stockAward: z.number(), + optionAward: z.number(), + incentivePlanCompensation: z.number(), + allOtherCompensation: z.number(), + total: z.number(), + link: z.string() +}); -export interface CompanyNotes { - cik: string; - symbol: string; - title: string; - exchange: string; -} +export const CompanyNotesSchema = z.object({ + cik: z.string(), + symbol: z.string(), + title: z.string(), + exchange: z.string() +}); -export interface HistoricalEmployeeCount { - symbol: string; - cik: string; - acceptanceTime: string; - periodOfReport: string; - companyName: string; - formType: string; - filingDate: string; - employeeCount: number; - source: string; -} +export const HistoricalEmployeeCountSchema = z.object({ + symbol: z.string(), + cik: z.string(), + acceptanceTime: z.string(), + periodOfReport: z.string(), + companyName: z.string(), + formType: z.string(), + filingDate: z.string(), + employeeCount: z.number(), + source: z.string() +}); -export interface SharesFloat { - symbol: string; - freeFloat: number; - floatShares: number; - outstandingShares: number; - source: string; - date: string; -} +export const SharesFloatSchema = z.object({ + symbol: z.string(), + freeFloat: z.number(), + floatShares: z.number(), + outstandingShares: z.number(), + source: z.string(), + date: z.string() +}); -export interface HistoricalSharesFloat { - symbol: string; - freeFloat: number; - floatShares: string; - outstandingShares: string; - source: string; - date: string; -} +export const HistoricalSharesFloatSchema = z.object({ + symbol: z.string(), + freeFloat: z.number(), + floatShares: z.string(), + outstandingShares: z.string(), + source: z.string().nullable(), + date: z.string() +}); -export interface EarningsCallTranscript { - symbol: string; - quarter: number; - year: number; - date: string; - content: string; -} +export const EarningsCallTranscriptSchema = z.object({ + symbol: z.string(), + quarter: z.number(), + year: z.number(), + date: z.string(), + content: z.string() +}); -export type CompanyTranscriptData = [number, number, string]; +export const CompanyTranscriptDataSchema = z.tuple([z.number(), z.number(), z.string()]); + +export type CompanyProfile = z.infer; +export type ExecutiveCompensation = z.infer; +export type CompanyNotes = z.infer; +export type HistoricalEmployeeCount = z.infer; +export type SharesFloat = z.infer; +export type HistoricalSharesFloat = z.infer; +export type EarningsCallTranscript = z.infer; +export type CompanyTranscriptData = z.infer; diff --git a/packages/types/src/economic.ts b/packages/types/src/economic.ts index 64d2932..d36f0bb 100644 --- a/packages/types/src/economic.ts +++ b/packages/types/src/economic.ts @@ -1,50 +1,33 @@ -// Economic indicator types for FMP API +// economic types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -// Economic data interfaces -export interface EconomicIndicator { - name: string; - date: string; - value: number; -} +export const EconomicIndicatorSchema = z.object({ + name: z.string(), + date: z.string(), + value: z.number() +}); -export interface TreasuryRate { - date: string; - month1: number; - month2: number; - month3: number; - month6: number; - year1: number; - year2: number; - year3: number; - year5: number; - year7: number; - year10: number; - year20: number; - year30: number; -} +export const TreasuryRateSchema = z.object({ + date: z.string(), + month1: z.number(), + month2: z.number(), + month3: z.number(), + month6: z.number(), + year1: z.number(), + year2: z.number(), + year3: z.number(), + year5: z.number(), + year7: z.number(), + year10: z.number(), + year20: z.number(), + year30: z.number() +}); -// Economic indicator names as per FMP API documentation -export type EconomicIndicatorName = - | 'GDP' - | 'realGDP' - | 'nominalPotentialGDP' - | 'realGDPPerCapita' - | 'federalFunds' - | 'CPI' - | 'inflationRate' - | 'inflation' - | 'retailSales' - | 'consumerSentiment' - | 'durableGoods' - | 'unemploymentRate' - | 'totalNonfarmPayroll' - | 'initialClaims' - | 'industrialProductionTotalIndex' - | 'newPrivatelyOwnedHousingUnitsStartedTotalUnits' - | 'totalVehicleSales' - | 'retailMoneyFunds' - | 'smoothedUSRecessionProbabilities' - | '3MonthOr90DayRatesAndYieldsCertificatesOfDeposit' - | 'commercialBankInterestRateOnCreditCardPlansAllAccounts' - | '30YearFixedRateMortgageAverage' - | '15YearFixedRateMortgageAverage'; +export const EconomicIndicatorNameSchema = z.union([z.literal("GDP"), z.literal("realGDP"), z.literal("nominalPotentialGDP"), z.literal("realGDPPerCapita"), z.literal("federalFunds"), z.literal("CPI"), z.literal("inflationRate"), z.literal("inflation"), z.literal("retailSales"), z.literal("consumerSentiment"), z.literal("durableGoods"), z.literal("unemploymentRate"), z.literal("totalNonfarmPayroll"), z.literal("initialClaims"), z.literal("industrialProductionTotalIndex"), z.literal("newPrivatelyOwnedHousingUnitsStartedTotalUnits"), z.literal("totalVehicleSales"), z.literal("retailMoneyFunds"), z.literal("smoothedUSRecessionProbabilities"), z.literal("3MonthOr90DayRatesAndYieldsCertificatesOfDeposit"), z.literal("commercialBankInterestRateOnCreditCardPlansAllAccounts"), z.literal("30YearFixedRateMortgageAverage"), z.literal("15YearFixedRateMortgageAverage")]); + +export type EconomicIndicator = z.infer; +export type TreasuryRate = z.infer; +export type EconomicIndicatorName = z.infer; diff --git a/packages/types/src/etf.ts b/packages/types/src/etf.ts index 44847f0..c9f8c44 100644 --- a/packages/types/src/etf.ts +++ b/packages/types/src/etf.ts @@ -1,85 +1,99 @@ -// ETF-related types for FMP API +// etf types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -export interface ETFHoldingDates { - date: string; -} +export const ETFHoldingDatesSchema = z.object({ + date: z.string() +}); -export interface ETFHolding { - cik: string; - acceptanceTime: string; - date: string; - symbol: string; - name: string; - lei: string; - title: string; - cusip: string; - isin: string; - balance: number; - units: string; - cur_cd: string; - valUsd: number; - pctVal: number; - payoffProfile: string; - assetCat: string; - issuerCat: string; - invCountry: string; - isRestrictedSec: string; - fairValLevel: string; - isCashCollateral: string; - isNonCashCollateral: string; - isLoanByFund: string; -} +export const ETFHoldingSchema = z.object({ + cik: z.string(), + acceptanceTime: z.string(), + date: z.string(), + symbol: z.string(), + name: z.string(), + lei: z.string(), + title: z.string(), + cusip: z.string(), + isin: z.string(), + balance: z.number(), + units: z.string(), + cur_cd: z.string(), + valUsd: z.number(), + pctVal: z.number(), + payoffProfile: z.string(), + assetCat: z.string(), + issuerCat: z.string(), + invCountry: z.string(), + isRestrictedSec: z.string(), + fairValLevel: z.string(), + isCashCollateral: z.string(), + isNonCashCollateral: z.string(), + isLoanByFund: z.string() +}); -export interface ETFHolder { - asset: string; - name: string; - isin: string; - cusip: string; - sharesNumber: number; - weightPercentage: number; - marketValue: number; - updated: string; -} +export const ETFHolderSchema = z.object({ + asset: z.string(), + name: z.string(), + isin: z.string(), + cusip: z.string(), + sharesNumber: z.number(), + weightPercentage: z.number(), + marketValue: z.number(), + updated: z.string() +}); -export interface ETFProfile { - symbol: string; - name: string; - description: string; - isin: string; - assetClass: string; - securityCusip: string; - domicile: string; - website: string; - etfCompany: string; - expenseRatio: number; - assetsUnderManagement: number; - avgVolume: number; - inceptionDate: string; - nav: number; - navCurrency: string; - holdingsCount: number; - updatedAt: string; - sectorsList: { - exposure: string; - industry: string; - }[]; -} +export const ETFProfileSchema = z.object({ + symbol: z.string(), + name: z.string(), + description: z.string(), + isin: z.string(), + assetClass: z.string(), + securityCusip: z.string(), + domicile: z.string(), + website: z.string(), + etfCompany: z.string(), + expenseRatio: z.number(), + assetsUnderManagement: z.number(), + avgVolume: z.number(), + inceptionDate: z.string(), + nav: z.number(), + navCurrency: z.string(), + holdingsCount: z.number(), + updatedAt: z.string(), + isActivelyTrading: z.boolean(), + sectorsList: z.array(z.object({ + exposure: z.number(), + industry: z.string() + })) +}); -export interface ETFWeighting { - symbol: string; - sector: string; - weightPercentage: string; -} +export const ETFWeightingSchema = z.object({ + symbol: z.string(), + sector: z.string(), + // Sector weighting returns a numeric percentage (country weighting returns a string). + weightPercentage: z.number() +}); -export interface ETFCountryWeighting { - country: string; - weightPercentage: string; -} +export const ETFCountryWeightingSchema = z.object({ + country: z.string(), + weightPercentage: z.string() +}); -export interface ETFStockExposure { - etfSymbol: string; - assetExposure: string; - sharesNumber: number; - weightPercentage: number; - marketValue: number; -} +export const ETFStockExposureSchema = z.object({ + etfSymbol: z.string(), + assetExposure: z.string(), + sharesNumber: z.number(), + weightPercentage: z.number(), + marketValue: z.number() +}); + +export type ETFHoldingDates = z.infer; +export type ETFHolding = z.infer; +export type ETFHolder = z.infer; +export type ETFProfile = z.infer; +export type ETFWeighting = z.infer; +export type ETFCountryWeighting = z.infer; +export type ETFStockExposure = z.infer; diff --git a/packages/types/src/financial.ts b/packages/types/src/financial.ts index 1f2187c..c4e31ad 100644 --- a/packages/types/src/financial.ts +++ b/packages/types/src/financial.ts @@ -1,522 +1,532 @@ // Financial statement types for FMP API +// +// Schema-first: Zod schemas are the source of truth; TypeScript types are +// derived via `z.infer`. Base schemas generated via `gen:schemas`. -// Base interface for common fields across all financial statements -// export interface FinancialStatementBase { -// date: string; -// symbol: string; -// reportedCurrency: string; -// cik: string; -// fillingDate: string; -// acceptedDate: string; -// calendarYear: string; -// period: string; -// link: string; -// finalLink: string; -// } - -// Base interface for growth statements -// export interface GrowthStatementBase { -// date: string; -// symbol: string; -// calendarYear: string; -// period: string; -// } +import { z } from 'zod'; // Income Statement -export interface IncomeStatement { - date: string; - symbol: string; - reportedCurrency: string; - cik: string; - filingDate: string; - acceptedDate: string; - fiscalYear: string; - period: string; - revenue: number; - costOfRevenue: number; - grossProfit: number; - researchAndDevelopmentExpenses: number; - generalAndAdministrativeExpenses: number; - sellingAndMarketingExpenses: number; - sellingGeneralAndAdministrativeExpenses: number; - otherExpenses: number; - operatingExpenses: number; - costAndExpenses: number; - netInterestIncome: number; - interestIncome: number; - interestExpense: number; - depreciationAndAmortization: number; - ebitda: number; - ebit: number; - nonOperatingIncomeExcludingInterest: number; - operatingIncome: number; - totalOtherIncomeExpensesNet: number; - incomeBeforeTax: number; - incomeTaxExpense: number; - netIncomeFromContinuingOperations: number; - netIncomeFromDiscontinuedOperations: number; - otherAdjustmentsToNetIncome: number; - netIncome: number; - netIncomeDeductions: number; - bottomLineNetIncome: number; - eps: number; - epsDiluted: number; - weightedAverageShsOut: number; - weightedAverageShsOutDil: number; -} +export const IncomeStatementSchema = z.object({ + date: z.string(), + symbol: z.string(), + reportedCurrency: z.string(), + cik: z.string(), + filingDate: z.string(), + acceptedDate: z.string(), + fiscalYear: z.string(), + period: z.string(), + revenue: z.number(), + costOfRevenue: z.number(), + grossProfit: z.number(), + researchAndDevelopmentExpenses: z.number(), + generalAndAdministrativeExpenses: z.number(), + sellingAndMarketingExpenses: z.number(), + sellingGeneralAndAdministrativeExpenses: z.number(), + otherExpenses: z.number(), + operatingExpenses: z.number(), + costAndExpenses: z.number(), + netInterestIncome: z.number(), + interestIncome: z.number(), + interestExpense: z.number(), + depreciationAndAmortization: z.number(), + ebitda: z.number(), + ebit: z.number(), + nonOperatingIncomeExcludingInterest: z.number(), + operatingIncome: z.number(), + totalOtherIncomeExpensesNet: z.number(), + incomeBeforeTax: z.number(), + incomeTaxExpense: z.number(), + netIncomeFromContinuingOperations: z.number(), + netIncomeFromDiscontinuedOperations: z.number(), + otherAdjustmentsToNetIncome: z.number(), + netIncome: z.number(), + netIncomeDeductions: z.number(), + bottomLineNetIncome: z.number(), + eps: z.number(), + epsDiluted: z.number(), + weightedAverageShsOut: z.number(), + weightedAverageShsOutDil: z.number(), +}); + +export type IncomeStatement = z.infer; // Balance Sheet -export interface BalanceSheet { - date: string; - symbol: string; - reportedCurrency: string; - cik: string; - filingDate: string; - acceptedDate: string; - fiscalYear: string; - period: string; - cashAndCashEquivalents: number; - shortTermInvestments: number; - cashAndShortTermInvestments: number; - netReceivables: number; - accountsReceivables: number; - otherReceivables: number; - inventory: number; - prepaids: number; - otherCurrentAssets: number; - totalCurrentAssets: number; - propertyPlantEquipmentNet: number; - goodwill: number; - intangibleAssets: number; - goodwillAndIntangibleAssets: number; - longTermInvestments: number; - taxAssets: number; - otherNonCurrentAssets: number; - totalNonCurrentAssets: number; - otherAssets: number; - totalAssets: number; - totalPayables: number; - accountPayables: number; - otherPayables: number; - accruedExpenses: number; - shortTermDebt: number; - capitalLeaseObligationsCurrent: number; - taxPayables: number; - deferredRevenue: number; - otherCurrentLiabilities: number; - totalCurrentLiabilities: number; - longTermDebt: number; - deferredRevenueNonCurrent: number; - deferredTaxLiabilitiesNonCurrent: number; - otherNonCurrentLiabilities: number; - totalNonCurrentLiabilities: number; - otherLiabilities: number; - capitalLeaseObligations: number; - totalLiabilities: number; - treasuryStock: number; - preferredStock: number; - commonStock: number; - retainedEarnings: number; - additionalPaidInCapital: number; - accumulatedOtherComprehensiveIncomeLoss: number; - otherTotalStockholdersEquity: number; - totalStockholdersEquity: number; - totalEquity: number; - minorityInterest: number; - totalLiabilitiesAndTotalEquity: number; - totalInvestments: number; - totalDebt: number; - netDebt: number; -} +export const BalanceSheetSchema = z.object({ + date: z.string(), + symbol: z.string(), + reportedCurrency: z.string(), + cik: z.string(), + filingDate: z.string(), + acceptedDate: z.string(), + fiscalYear: z.string(), + period: z.string(), + cashAndCashEquivalents: z.number(), + shortTermInvestments: z.number(), + cashAndShortTermInvestments: z.number(), + netReceivables: z.number(), + accountsReceivables: z.number(), + otherReceivables: z.number(), + inventory: z.number(), + prepaids: z.number(), + otherCurrentAssets: z.number(), + totalCurrentAssets: z.number(), + propertyPlantEquipmentNet: z.number(), + goodwill: z.number(), + intangibleAssets: z.number(), + goodwillAndIntangibleAssets: z.number(), + longTermInvestments: z.number(), + taxAssets: z.number(), + otherNonCurrentAssets: z.number(), + totalNonCurrentAssets: z.number(), + otherAssets: z.number(), + totalAssets: z.number(), + totalPayables: z.number(), + accountPayables: z.number(), + otherPayables: z.number(), + accruedExpenses: z.number(), + shortTermDebt: z.number(), + capitalLeaseObligationsCurrent: z.number(), + taxPayables: z.number(), + deferredRevenue: z.number(), + otherCurrentLiabilities: z.number(), + totalCurrentLiabilities: z.number(), + longTermDebt: z.number(), + deferredRevenueNonCurrent: z.number(), + deferredTaxLiabilitiesNonCurrent: z.number(), + capitalLeaseObligationsNonCurrent: z.number(), + otherNonCurrentLiabilities: z.number(), + totalNonCurrentLiabilities: z.number(), + otherLiabilities: z.number(), + capitalLeaseObligations: z.number(), + totalLiabilities: z.number(), + treasuryStock: z.number(), + preferredStock: z.number(), + commonStock: z.number(), + retainedEarnings: z.number(), + additionalPaidInCapital: z.number(), + accumulatedOtherComprehensiveIncomeLoss: z.number(), + otherTotalStockholdersEquity: z.number(), + totalStockholdersEquity: z.number(), + totalEquity: z.number(), + minorityInterest: z.number(), + totalLiabilitiesAndTotalEquity: z.number(), + totalInvestments: z.number(), + totalDebt: z.number(), + netDebt: z.number(), +}); + +export type BalanceSheet = z.infer; // Cash Flow Statement -export interface CashFlowStatement { - date: string; - symbol: string; - reportedCurrency: string; - cik: string; - filingDate: string; - acceptedDate: string; - fiscalYear: string; - period: string; - netIncome: number; - depreciationAndAmortization: number; - deferredIncomeTax: number; - stockBasedCompensation: number; - changeInWorkingCapital: number; - accountsReceivables: number; - inventory: number; - accountsPayables: number; - otherWorkingCapital: number; - otherNonCashItems: number; - netCashProvidedByOperatingActivities: number; - investmentsInPropertyPlantAndEquipment: number; - acquisitionsNet: number; - purchasesOfInvestments: number; - salesMaturitiesOfInvestments: number; - otherInvestingActivities: number; - netCashProvidedByInvestingActivities: number; - netDebtIssuance: number; - longTermNetDebtIssuance: number; - shortTermNetDebtIssuance: number; - netStockIssuance: number; - netCommonStockIssuance: number; - commonStockIssuance: number; - commonStockRepurchased: number; - netPreferredStockIssuance: number; - netDividendsPaid: number; - commonDividendsPaid: number; - preferredDividendsPaid: number; - otherFinancingActivities: number; - netCashProvidedByFinancingActivities: number; - effectOfForexChangesOnCash: number; - netChangeInCash: number; - cashAtEndOfPeriod: number; - cashAtBeginningOfPeriod: number; - operatingCashFlow: number; - capitalExpenditure: number; - freeCashFlow: number; - incomeTaxesPaid: number; - interestPaid: number; -} +export const CashFlowStatementSchema = z.object({ + date: z.string(), + symbol: z.string(), + reportedCurrency: z.string(), + cik: z.string(), + filingDate: z.string(), + acceptedDate: z.string(), + fiscalYear: z.string(), + period: z.string(), + netIncome: z.number(), + depreciationAndAmortization: z.number(), + deferredIncomeTax: z.number(), + stockBasedCompensation: z.number(), + changeInWorkingCapital: z.number(), + accountsReceivables: z.number(), + inventory: z.number(), + accountsPayables: z.number(), + otherWorkingCapital: z.number(), + otherNonCashItems: z.number(), + netCashProvidedByOperatingActivities: z.number(), + investmentsInPropertyPlantAndEquipment: z.number(), + acquisitionsNet: z.number(), + purchasesOfInvestments: z.number(), + salesMaturitiesOfInvestments: z.number(), + otherInvestingActivities: z.number(), + netCashProvidedByInvestingActivities: z.number(), + netDebtIssuance: z.number(), + longTermNetDebtIssuance: z.number(), + shortTermNetDebtIssuance: z.number(), + netStockIssuance: z.number(), + netCommonStockIssuance: z.number(), + commonStockIssuance: z.number(), + commonStockRepurchased: z.number(), + netPreferredStockIssuance: z.number(), + netDividendsPaid: z.number(), + commonDividendsPaid: z.number(), + preferredDividendsPaid: z.number(), + otherFinancingActivities: z.number(), + netCashProvidedByFinancingActivities: z.number(), + effectOfForexChangesOnCash: z.number(), + netChangeInCash: z.number(), + cashAtEndOfPeriod: z.number(), + cashAtBeginningOfPeriod: z.number(), + operatingCashFlow: z.number(), + capitalExpenditure: z.number(), + freeCashFlow: z.number(), + incomeTaxesPaid: z.number(), + interestPaid: z.number(), +}); + +export type CashFlowStatement = z.infer; // Key Metrics -export interface KeyMetrics { - symbol: string; - date: string; - fiscalYear: string; - period: string; - reportedCurrency: string; - marketCap: number; - enterpriseValue: number; - evToSales: number; - evToOperatingCashFlow: number; - evToFreeCashFlow: number; - evToEBITDA: number; - netDebtToEBITDA: number; - currentRatio: number; - incomeQuality: number; - grahamNumber: number; - grahamNetNet: number; - taxBurden: number; - interestBurden: number; - workingCapital: number; - investedCapital: number; - returnOnAssets: number; - operatingReturnOnAssets: number; - returnOnTangibleAssets: number; - returnOnEquity: number; - returnOnInvestedCapital: number; - returnOnCapitalEmployed: number; - earningsYield: number; - freeCashFlowYield: number; - capexToOperatingCashFlow: number; - capexToDepreciation: number; - capexToRevenue: number; - salesGeneralAndAdministrativeToRevenue: number; - researchAndDevelopementToRevenue: number; - stockBasedCompensationToRevenue: number; - intangiblesToTotalAssets: number; - averageReceivables: number; - averagePayables: number; - averageInventory: number; - daysOfSalesOutstanding: number; - daysOfPayablesOutstanding: number; - daysOfInventoryOutstanding: number; - operatingCycle: number; - cashConversionCycle: number; - freeCashFlowToEquity: number; - freeCashFlowToFirm: number; - tangibleAssetValue: number; - netCurrentAssetValue: number; -} +export const KeyMetricsSchema = z.object({ + symbol: z.string(), + date: z.string(), + fiscalYear: z.string(), + period: z.string(), + reportedCurrency: z.string(), + marketCap: z.number(), + enterpriseValue: z.number(), + evToSales: z.number(), + evToOperatingCashFlow: z.number(), + evToFreeCashFlow: z.number(), + evToEBITDA: z.number(), + netDebtToEBITDA: z.number(), + currentRatio: z.number(), + incomeQuality: z.number(), + grahamNumber: z.number(), + grahamNetNet: z.number(), + taxBurden: z.number(), + interestBurden: z.number(), + workingCapital: z.number(), + investedCapital: z.number(), + returnOnAssets: z.number(), + operatingReturnOnAssets: z.number(), + returnOnTangibleAssets: z.number(), + returnOnEquity: z.number(), + returnOnInvestedCapital: z.number(), + returnOnCapitalEmployed: z.number(), + earningsYield: z.number(), + freeCashFlowYield: z.number(), + capexToOperatingCashFlow: z.number(), + capexToDepreciation: z.number(), + capexToRevenue: z.number(), + salesGeneralAndAdministrativeToRevenue: z.number(), + researchAndDevelopementToRevenue: z.number(), + stockBasedCompensationToRevenue: z.number(), + intangiblesToTotalAssets: z.number(), + averageReceivables: z.number(), + averagePayables: z.number(), + averageInventory: z.number(), + daysOfSalesOutstanding: z.number(), + daysOfPayablesOutstanding: z.number(), + daysOfInventoryOutstanding: z.number(), + operatingCycle: z.number(), + cashConversionCycle: z.number(), + freeCashFlowToEquity: z.number(), + freeCashFlowToFirm: z.number(), + tangibleAssetValue: z.number(), + netCurrentAssetValue: z.number(), +}); + +export type KeyMetrics = z.infer; // Financial Ratios -export interface FinancialRatios { - symbol: string; - date: string; - fiscalYear: string; - period: string; - reportedCurrency: string; - grossProfitMargin: number; - ebitMargin: number; - ebitdaMargin: number; - operatingProfitMargin: number; - pretaxProfitMargin: number; - continuousOperationsProfitMargin: number; - netProfitMargin: number; - bottomLineProfitMargin: number; - receivablesTurnover: number; - payablesTurnover: number; - inventoryTurnover: number; - fixedAssetTurnover: number; - assetTurnover: number; - currentRatio: number; - quickRatio: number; - solvencyRatio: number; - cashRatio: number; - priceToEarningsRatio: number; - priceToEarningsGrowthRatio: number; - forwardPriceToEarningsGrowthRatio: number; - priceToBookRatio: number; - priceToSalesRatio: number; - priceToFreeCashFlowRatio: number; - priceToOperatingCashFlowRatio: number; - debtToAssetsRatio: number; - debtToEquityRatio: number; - debtToCapitalRatio: number; - longTermDebtToCapitalRatio: number; - financialLeverageRatio: number; - workingCapitalTurnoverRatio: number; - operatingCashFlowRatio: number; - operatingCashFlowSalesRatio: number; - freeCashFlowOperatingCashFlowRatio: number; - debtServiceCoverageRatio: number; - interestCoverageRatio: number; - shortTermOperatingCashFlowCoverageRatio: number; - operatingCashFlowCoverageRatio: number; - capitalExpenditureCoverageRatio: number; - dividendPaidAndCapexCoverageRatio: number; - dividendPayoutRatio: number; - dividendYield: number; - dividendYieldPercentage: number; - revenuePerShare: number; - netIncomePerShare: number; - interestDebtPerShare: number; - cashPerShare: number; - bookValuePerShare: number; - tangibleBookValuePerShare: number; - shareholdersEquityPerShare: number; - operatingCashFlowPerShare: number; - capexPerShare: number; - freeCashFlowPerShare: number; - netIncomePerEBT: number; - ebtPerEbit: number; - priceToFairValue: number; - debtToMarketCap: number; - effectiveTaxRate: number; - enterpriseValueMultiple: number; -} +export const FinancialRatiosSchema = z.object({ + symbol: z.string(), + date: z.string(), + fiscalYear: z.string(), + period: z.string(), + reportedCurrency: z.string(), + grossProfitMargin: z.number(), + ebitMargin: z.number(), + ebitdaMargin: z.number(), + operatingProfitMargin: z.number(), + pretaxProfitMargin: z.number(), + continuousOperationsProfitMargin: z.number(), + netProfitMargin: z.number(), + bottomLineProfitMargin: z.number(), + receivablesTurnover: z.number(), + payablesTurnover: z.number(), + inventoryTurnover: z.number(), + fixedAssetTurnover: z.number(), + assetTurnover: z.number(), + currentRatio: z.number(), + quickRatio: z.number(), + solvencyRatio: z.number(), + cashRatio: z.number(), + priceToEarningsRatio: z.number(), + priceToEarningsGrowthRatio: z.number(), + forwardPriceToEarningsGrowthRatio: z.number(), + priceToBookRatio: z.number(), + priceToSalesRatio: z.number(), + priceToFreeCashFlowRatio: z.number(), + priceToOperatingCashFlowRatio: z.number(), + debtToAssetsRatio: z.number(), + debtToEquityRatio: z.number(), + debtToCapitalRatio: z.number(), + longTermDebtToCapitalRatio: z.number(), + financialLeverageRatio: z.number(), + workingCapitalTurnoverRatio: z.number(), + operatingCashFlowRatio: z.number(), + operatingCashFlowSalesRatio: z.number(), + freeCashFlowOperatingCashFlowRatio: z.number(), + debtServiceCoverageRatio: z.number(), + interestCoverageRatio: z.number(), + shortTermOperatingCashFlowCoverageRatio: z.number(), + operatingCashFlowCoverageRatio: z.number(), + capitalExpenditureCoverageRatio: z.number(), + dividendPaidAndCapexCoverageRatio: z.number(), + dividendPayoutRatio: z.number(), + dividendYield: z.number(), + dividendYieldPercentage: z.number(), + dividendPerShare: z.number(), + revenuePerShare: z.number(), + netIncomePerShare: z.number(), + interestDebtPerShare: z.number(), + cashPerShare: z.number(), + bookValuePerShare: z.number(), + tangibleBookValuePerShare: z.number(), + shareholdersEquityPerShare: z.number(), + operatingCashFlowPerShare: z.number(), + capexPerShare: z.number(), + freeCashFlowPerShare: z.number(), + netIncomePerEBT: z.number(), + ebtPerEbit: z.number(), + priceToFairValue: z.number(), + debtToMarketCap: z.number(), + effectiveTaxRate: z.number(), + enterpriseValueMultiple: z.number(), +}); + +export type FinancialRatios = z.infer; // Enterprise Value -export interface EnterpriseValue { - symbol: string; - date: string; - stockPrice: number; - numberOfShares: number; - marketCapitalization: number; - minusCashAndCashEquivalents: number; - addTotalDebt: number; - enterpriseValue: number; -} +export const EnterpriseValueSchema = z.object({ + symbol: z.string(), + date: z.string(), + stockPrice: z.number(), + numberOfShares: z.number(), + marketCapitalization: z.number(), + minusCashAndCashEquivalents: z.number(), + addTotalDebt: z.number(), + enterpriseValue: z.number(), +}); + +export type EnterpriseValue = z.infer; // Cashflow Growth -export interface CashflowGrowth { - symbol: string; - date: string; - fiscalYear: string; - period: string; - reportedCurrency: string; - growthNetIncome: number; - growthDepreciationAndAmortization: number; - growthDeferredIncomeTax: number; - growthStockBasedCompensation: number; - growthChangeInWorkingCapital: number; - growthAccountsReceivables: number; - growthInventory: number; - growthAccountsPayables: number; - growthOtherWorkingCapital: number; - growthOtherNonCashItems: number; - growthNetCashProvidedByOperatingActivites: number; - growthInvestmentsInPropertyPlantAndEquipment: number; - growthAcquisitionsNet: number; - growthPurchasesOfInvestments: number; - growthSalesMaturitiesOfInvestments: number; - growthOtherInvestingActivites: number; - growthNetCashUsedForInvestingActivites: number; - growthDebtRepayment: number; - growthCommonStockIssued: number; - growthCommonStockRepurchased: number; - growthDividendsPaid: number; - growthOtherFinancingActivites: number; - growthNetCashUsedProvidedByFinancingActivities: number; - growthEffectOfForexChangesOnCash: number; - growthNetChangeInCash: number; - growthCashAtEndOfPeriod: number; - growthCashAtBeginningOfPeriod: number; - growthOperatingCashFlow: number; - growthCapitalExpenditure: number; - growthFreeCashFlow: number; - growthNetDebtIssuance: number; - growthLongTermNetDebtIssuance: number; - growthShortTermNetDebtIssuance: number; - growthNetStockIssuance: number; - growthPreferredDividendsPaid: number; - growthIncomeTaxesPaid: number; - growthInterestPaid: number; -} +export const CashflowGrowthSchema = z.object({ + symbol: z.string(), + date: z.string(), + fiscalYear: z.string(), + period: z.string(), + reportedCurrency: z.string(), + growthNetIncome: z.number(), + growthDepreciationAndAmortization: z.number(), + growthDeferredIncomeTax: z.number(), + growthStockBasedCompensation: z.number(), + growthChangeInWorkingCapital: z.number(), + growthAccountsReceivables: z.number(), + growthInventory: z.number(), + growthAccountsPayables: z.number(), + growthOtherWorkingCapital: z.number(), + growthOtherNonCashItems: z.number(), + growthNetCashProvidedByOperatingActivites: z.number(), + growthInvestmentsInPropertyPlantAndEquipment: z.number(), + growthAcquisitionsNet: z.number(), + growthPurchasesOfInvestments: z.number(), + growthSalesMaturitiesOfInvestments: z.number(), + growthOtherInvestingActivites: z.number(), + growthNetCashUsedForInvestingActivites: z.number(), + growthDebtRepayment: z.number(), + growthCommonStockIssued: z.number(), + growthCommonStockRepurchased: z.number(), + growthDividendsPaid: z.number(), + growthOtherFinancingActivites: z.number(), + growthNetCashUsedProvidedByFinancingActivities: z.number(), + growthEffectOfForexChangesOnCash: z.number(), + growthNetChangeInCash: z.number(), + growthCashAtEndOfPeriod: z.number(), + growthCashAtBeginningOfPeriod: z.number(), + growthOperatingCashFlow: z.number(), + growthCapitalExpenditure: z.number(), + growthFreeCashFlow: z.number(), + growthNetDebtIssuance: z.number(), + growthLongTermNetDebtIssuance: z.number(), + growthShortTermNetDebtIssuance: z.number(), + growthNetStockIssuance: z.number(), + growthPreferredDividendsPaid: z.number(), + growthIncomeTaxesPaid: z.number(), + growthInterestPaid: z.number(), +}); + +export type CashflowGrowth = z.infer; // Income Growth -export interface IncomeGrowth { - symbol: string; - date: string; - fiscalYear: string; - period: string; - reportedCurrency: string; - growthRevenue: number; - growthCostOfRevenue: number; - growthGrossProfit: number; - growthGrossProfitRatio: number; - growthResearchAndDevelopmentExpenses: number; - growthGeneralAndAdministrativeExpenses: number; - growthSellingAndMarketingExpenses: number; - growthOtherExpenses: number; - growthOperatingExpenses: number; - growthCostAndExpenses: number; - growthInterestIncome: number; - growthInterestExpense: number; - growthDepreciationAndAmortization: number; - growthEBITDA: number; - growthOperatingIncome: number; - growthIncomeBeforeTax: number; - growthIncomeTaxExpense: number; - growthNetIncome: number; - growthEPS: number; - growthEPSDiluted: number; - growthWeightedAverageShsOut: number; - growthWeightedAverageShsOutDil: number; - growthEBIT: number; - growthNonOperatingIncomeExcludingInterest: number; - growthNetInterestIncome: number; - growthTotalOtherIncomeExpensesNet: number; - growthNetIncomeFromContinuingOperations: number; - growthOtherAdjustmentsToNetIncome: number; - growthNetIncomeDeductions: number; -} +export const IncomeGrowthSchema = z.object({ + symbol: z.string(), + date: z.string(), + fiscalYear: z.string(), + period: z.string(), + reportedCurrency: z.string(), + growthRevenue: z.number(), + growthCostOfRevenue: z.number(), + growthGrossProfit: z.number(), + growthGrossProfitRatio: z.number(), + growthResearchAndDevelopmentExpenses: z.number(), + growthGeneralAndAdministrativeExpenses: z.number(), + growthSellingAndMarketingExpenses: z.number(), + growthOtherExpenses: z.number(), + growthOperatingExpenses: z.number(), + growthCostAndExpenses: z.number(), + growthInterestIncome: z.number(), + growthInterestExpense: z.number(), + growthDepreciationAndAmortization: z.number(), + growthEBITDA: z.number(), + growthOperatingIncome: z.number(), + growthIncomeBeforeTax: z.number(), + growthIncomeTaxExpense: z.number(), + growthNetIncome: z.number(), + growthEPS: z.number(), + growthEPSDiluted: z.number(), + growthWeightedAverageShsOut: z.number(), + growthWeightedAverageShsOutDil: z.number(), + growthEBIT: z.number(), + growthNonOperatingIncomeExcludingInterest: z.number(), + growthNetInterestIncome: z.number(), + growthTotalOtherIncomeExpensesNet: z.number(), + growthNetIncomeFromContinuingOperations: z.number(), + growthOtherAdjustmentsToNetIncome: z.number(), + growthNetIncomeDeductions: z.number(), +}); + +export type IncomeGrowth = z.infer; // Balance Sheet Growth -export interface BalanceSheetGrowth { - symbol: string; - date: string; - fiscalYear: string; - period: string; - reportedCurrency: string; - growthCashAndCashEquivalents: number; - growthShortTermInvestments: number; - growthCashAndShortTermInvestments: number; - growthNetReceivables: number; - growthInventory: number; - growthOtherCurrentAssets: number; - growthTotalCurrentAssets: number; - growthPropertyPlantEquipmentNet: number; - growthGoodwill: number; - growthIntangibleAssets: number; - growthGoodwillAndIntangibleAssets: number; - growthLongTermInvestments: number; - growthTaxAssets: number; - growthOtherNonCurrentAssets: number; - growthTotalNonCurrentAssets: number; - growthOtherAssets: number; - growthTotalAssets: number; - growthAccountPayables: number; - growthShortTermDebt: number; - growthTaxPayables: number; - growthDeferredRevenue: number; - growthOtherCurrentLiabilities: number; - growthTotalCurrentLiabilities: number; - growthLongTermDebt: number; - growthDeferredRevenueNonCurrent: number; - growthDeferredTaxLiabilitiesNonCurrent: number; - growthOtherNonCurrentLiabilities: number; - growthTotalNonCurrentLiabilities: number; - growthOtherLiabilities: number; - growthTotalLiabilities: number; - growthPreferredStock: number; - growthCommonStock: number; - growthRetainedEarnings: number; - growthAccumulatedOtherComprehensiveIncomeLoss: number; - growthOthertotalStockholdersEquity: number; - growthTotalStockholdersEquity: number; - growthMinorityInterest: number; - growthTotalEquity: number; - growthTotalLiabilitiesAndStockholdersEquity: number; - growthTotalInvestments: number; - growthTotalDebt: number; - growthNetDebt: number; - growthAccountsReceivables: number; - growthOtherReceivables: number; - growthPrepaids: number; - growthTotalPayables: number; - growthOtherPayables: number; - growthAccruedExpenses: number; - growthCapitalLeaseObligationsCurrent: number; - growthAdditionalPaidInCapital: number; - growthTreasuryStock: number; -} +export const BalanceSheetGrowthSchema = z.object({ + symbol: z.string(), + date: z.string(), + fiscalYear: z.string(), + period: z.string(), + reportedCurrency: z.string(), + growthCashAndCashEquivalents: z.number(), + growthShortTermInvestments: z.number(), + growthCashAndShortTermInvestments: z.number(), + growthNetReceivables: z.number(), + growthInventory: z.number(), + growthOtherCurrentAssets: z.number(), + growthTotalCurrentAssets: z.number(), + growthPropertyPlantEquipmentNet: z.number(), + growthGoodwill: z.number(), + growthIntangibleAssets: z.number(), + growthGoodwillAndIntangibleAssets: z.number(), + growthLongTermInvestments: z.number(), + growthTaxAssets: z.number(), + growthOtherNonCurrentAssets: z.number(), + growthTotalNonCurrentAssets: z.number(), + growthOtherAssets: z.number(), + growthTotalAssets: z.number(), + growthAccountPayables: z.number(), + growthShortTermDebt: z.number(), + growthTaxPayables: z.number(), + growthDeferredRevenue: z.number(), + growthOtherCurrentLiabilities: z.number(), + growthTotalCurrentLiabilities: z.number(), + growthLongTermDebt: z.number(), + growthDeferredRevenueNonCurrent: z.number(), + growthDeferredTaxLiabilitiesNonCurrent: z.number(), + growthOtherNonCurrentLiabilities: z.number(), + growthTotalNonCurrentLiabilities: z.number(), + growthOtherLiabilities: z.number(), + growthTotalLiabilities: z.number(), + growthPreferredStock: z.number(), + growthCommonStock: z.number(), + growthRetainedEarnings: z.number(), + growthAccumulatedOtherComprehensiveIncomeLoss: z.number(), + growthOthertotalStockholdersEquity: z.number(), + growthTotalStockholdersEquity: z.number(), + growthMinorityInterest: z.number(), + growthTotalEquity: z.number(), + growthTotalLiabilitiesAndStockholdersEquity: z.number(), + growthTotalInvestments: z.number(), + growthTotalDebt: z.number(), + growthNetDebt: z.number(), + growthAccountsReceivables: z.number(), + growthOtherReceivables: z.number(), + growthPrepaids: z.number(), + growthTotalPayables: z.number(), + growthOtherPayables: z.number(), + growthAccruedExpenses: z.number(), + growthCapitalLeaseObligationsCurrent: z.number(), + growthAdditionalPaidInCapital: z.number(), + growthTreasuryStock: z.number(), +}); + +export type BalanceSheetGrowth = z.infer; // Financial Growth -export interface FinancialGrowth { - symbol: string; - date: string; - fiscalYear: string; - period: string; - reportedCurrency: string; - revenueGrowth: number; - grossProfitGrowth: number; - ebitgrowth: number; - operatingIncomeGrowth: number; - netIncomeGrowth: number; - epsgrowth: number; - epsdilutedGrowth: number; - weightedAverageSharesGrowth: number; - weightedAverageSharesDilutedGrowth: number; - dividendsPerShareGrowth: number; - operatingCashFlowGrowth: number; - receivablesGrowth: number; - inventoryGrowth: number; - assetGrowth: number; - bookValueperShareGrowth: number; - debtGrowth: number; - rdexpenseGrowth: number; - sgaexpensesGrowth: number; - freeCashFlowGrowth: number; - tenYRevenueGrowthPerShare: number; - fiveYRevenueGrowthPerShare: number; - threeYRevenueGrowthPerShare: number; - tenYOperatingCFGrowthPerShare: number; - fiveYOperatingCFGrowthPerShare: number; - threeYOperatingCFGrowthPerShare: number; - tenYNetIncomeGrowthPerShare: number; - fiveYNetIncomeGrowthPerShare: number; - threeYNetIncomeGrowthPerShare: number; - tenYShareholdersEquityGrowthPerShare: number; - fiveYShareholdersEquityGrowthPerShare: number; - threeYShareholdersEquityGrowthPerShare: number; - tenYDividendperShareGrowthPerShare: number; - fiveYDividendperShareGrowthPerShare: number; - threeYDividendperShareGrowthPerShare: number; - ebitdaGrowth: number; - growthCapitalExpenditure: number; - tenYBottomLineNetIncomeGrowthPerShare: number; - fiveYBottomLineNetIncomeGrowthPerShare: number; - threeYBottomLineNetIncomeGrowthPerShare: number; -} +export const FinancialGrowthSchema = z.object({ + symbol: z.string(), + date: z.string(), + fiscalYear: z.string(), + period: z.string(), + reportedCurrency: z.string(), + revenueGrowth: z.number(), + grossProfitGrowth: z.number(), + ebitgrowth: z.number(), + operatingIncomeGrowth: z.number(), + netIncomeGrowth: z.number(), + epsgrowth: z.number(), + epsdilutedGrowth: z.number(), + weightedAverageSharesGrowth: z.number(), + weightedAverageSharesDilutedGrowth: z.number(), + dividendsPerShareGrowth: z.number(), + operatingCashFlowGrowth: z.number(), + receivablesGrowth: z.number(), + inventoryGrowth: z.number(), + assetGrowth: z.number(), + bookValueperShareGrowth: z.number(), + debtGrowth: z.number(), + rdexpenseGrowth: z.number(), + sgaexpensesGrowth: z.number(), + freeCashFlowGrowth: z.number(), + tenYRevenueGrowthPerShare: z.number(), + fiveYRevenueGrowthPerShare: z.number(), + threeYRevenueGrowthPerShare: z.number(), + tenYOperatingCFGrowthPerShare: z.number(), + fiveYOperatingCFGrowthPerShare: z.number(), + threeYOperatingCFGrowthPerShare: z.number(), + tenYNetIncomeGrowthPerShare: z.number(), + fiveYNetIncomeGrowthPerShare: z.number(), + threeYNetIncomeGrowthPerShare: z.number(), + tenYShareholdersEquityGrowthPerShare: z.number(), + fiveYShareholdersEquityGrowthPerShare: z.number(), + threeYShareholdersEquityGrowthPerShare: z.number(), + tenYDividendperShareGrowthPerShare: z.number(), + fiveYDividendperShareGrowthPerShare: z.number(), + threeYDividendperShareGrowthPerShare: z.number(), + ebitdaGrowth: z.number(), + growthCapitalExpenditure: z.number(), + tenYBottomLineNetIncomeGrowthPerShare: z.number(), + fiveYBottomLineNetIncomeGrowthPerShare: z.number(), + threeYBottomLineNetIncomeGrowthPerShare: z.number(), +}); + +export type FinancialGrowth = z.infer; // Earnings Historical -export interface EarningsHistorical { - date: string; - symbol: string; - epsActual: number; - epsEstimated: number; - revenueActual: number; - revenueEstimated: number; - lastUpdated: string; -} +export const EarningsHistoricalSchema = z.object({ + date: z.string(), + symbol: z.string(), + // Actuals are null for earnings that haven't been reported yet. + epsActual: z.number().nullable(), + epsEstimated: z.number().nullable(), + revenueActual: z.number().nullable(), + revenueEstimated: z.number().nullable(), + lastUpdated: z.string(), +}); + +export type EarningsHistorical = z.infer; // Earnings Surprises -export interface EarningsSurprises { - date: string; - symbol: string; - actualEarningResult: number; - estimatedEarning: number; -} +export const EarningsSurprisesSchema = z.object({ + date: z.string(), + symbol: z.string(), + actualEarningResult: z.number(), + estimatedEarning: z.number(), +}); + +export type EarningsSurprises = z.infer; diff --git a/packages/types/src/insider.ts b/packages/types/src/insider.ts index 5f1104d..961ee15 100644 --- a/packages/types/src/insider.ts +++ b/packages/types/src/insider.ts @@ -1,112 +1,137 @@ -// Insider Trading Types +// insider types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Hand-written (not generated) because ts-to-zod can't process the TransactionType enum. + +import { z } from 'zod'; // RSS Feed Response -export interface InsiderTradingRSSResponse { - symbol: string; - filingDate: string; - transactionDate: string; - reportingCik: string; - companyCik: string; - transactionType: string; - securitiesOwned: number; - reportingName: string; - typeOfOwner: string; - acquisitionOrDisposition: string; - directOrIndirect: string; - formType: string; - securitiesTransacted: number; - price: number; - securityName: string; - url: string; -} +export const InsiderTradingRSSResponseSchema = z.object({ + symbol: z.string(), + filingDate: z.string(), + transactionDate: z.string(), + reportingCik: z.string(), + companyCik: z.string(), + transactionType: z.string(), + securitiesOwned: z.number(), + reportingName: z.string(), + typeOfOwner: z.string(), + acquisitionOrDisposition: z.string(), + directOrIndirect: z.string().nullable(), + formType: z.string(), + securitiesTransacted: z.number(), + price: z.number(), + securityName: z.string(), + url: z.string(), +}); + +export type InsiderTradingRSSResponse = z.infer; // Insider Trading Search Response -export interface InsiderTradingSearchResponse { - symbol: string; - filingDate: string; - transactionDate: string; - reportingCik: string; - companyCik: string; - transactionType: string; - securitiesOwned: number; - reportingName: string; - typeOfOwner: string; - acquisitionOrDisposition: string; - directOrIndirect: string; - formType: string; - securitiesTransacted: number; - price: number; - securityName: string; - url: string; -} +export const InsiderTradingSearchResponseSchema = z.object({ + symbol: z.string(), + filingDate: z.string(), + transactionDate: z.string(), + reportingCik: z.string(), + companyCik: z.string(), + transactionType: z.string(), + securitiesOwned: z.number(), + reportingName: z.string(), + typeOfOwner: z.string(), + acquisitionOrDisposition: z.string(), + directOrIndirect: z.string().nullable(), + formType: z.string(), + securitiesTransacted: z.number(), + price: z.number(), + securityName: z.string(), + url: z.string(), +}); + +export type InsiderTradingSearchResponse = z.infer; -// Transaction Types Response -export type TransactionTypesResponse = { transactionType: string }[]; +// Transaction Types Response (array of records) +export const TransactionTypeRecordSchema = z.object({ + transactionType: z.string(), +}); + +export type TransactionTypesResponse = z.infer[]; // Insiders By Symbol Response -export interface InsidersBySymbolResponse { - typeOfOwner: string; - transactionDate: string; - owner: string; -} +export const InsidersBySymbolResponseSchema = z.object({ + typeOfOwner: z.string().nullable(), + transactionDate: z.string(), + owner: z.string(), +}); + +export type InsidersBySymbolResponse = z.infer; // Insider Trade Statistics Response -export interface InsiderTradeStatisticsResponse { - symbol: string; - cik: string; - year: number; - quarter: number; - acquiredTransactions: number; - disposedTransactions: number; - acquiredDisposedRatio: number; - totalAcquired: number; - totalDisposed: number; - averageAcquired: number; - averageDisposed: number; - totalPurchases: number; - totalSales: number; -} +export const InsiderTradeStatisticsResponseSchema = z.object({ + symbol: z.string(), + cik: z.string(), + year: z.number(), + quarter: z.number(), + acquiredTransactions: z.number(), + disposedTransactions: z.number(), + acquiredDisposedRatio: z.number(), + totalAcquired: z.number(), + totalDisposed: z.number(), + averageAcquired: z.number(), + averageDisposed: z.number(), + totalPurchases: z.number(), + totalSales: z.number(), +}); + +export type InsiderTradeStatisticsResponse = z.infer; // CIK Mapper Response -export interface CikMapperResponse { - reportingCik: string; - reportingName: string; -} +export const CikMapperResponseSchema = z.object({ + reportingCik: z.string(), + reportingName: z.string(), +}); + +export type CikMapperResponse = z.infer; // CIK Mapper By Symbol Response -export interface CikMapperBySymbolResponse { - symbol: string; - companyCik: string; -} +export const CikMapperBySymbolResponseSchema = z.object({ + symbol: z.string(), + companyCik: z.string(), +}); + +export type CikMapperBySymbolResponse = z.infer; // Beneficial Ownership Response -export interface BeneficialOwnershipResponse { - cik: string; - symbol: string; - filingDate: string; - acceptedDate: string; - cusip: string; - nameOfReportingPerson: string; - citizenshipOrPlaceOfOrganization: string; - soleVotingPower: string; - sharedVotingPower: string; - soleDispositivePower: string; - sharedDispositivePower: string; - amountBeneficiallyOwned: string; - percentOfClass: string; - typeOfReportingPerson: string; - url: string; -} +export const BeneficialOwnershipResponseSchema = z.object({ + cik: z.string(), + symbol: z.string(), + filingDate: z.string(), + acceptedDate: z.string(), + cusip: z.string(), + nameOfReportingPerson: z.string(), + citizenshipOrPlaceOfOrganization: z.string(), + soleVotingPower: z.string(), + sharedVotingPower: z.string(), + soleDispositivePower: z.string(), + sharedDispositivePower: z.string(), + amountBeneficiallyOwned: z.string(), + percentOfClass: z.string(), + typeOfReportingPerson: z.string(), + url: z.string(), +}); + +export type BeneficialOwnershipResponse = z.infer; // Fail to Deliver Response -export interface FailToDeliverResponse { - symbol: string; - date: string; - price: number; - quantity: number; - cusip: string; - name: string; -} +export const FailToDeliverResponseSchema = z.object({ + symbol: z.string(), + date: z.string(), + price: z.number(), + quantity: z.number(), + cusip: z.string(), + name: z.string(), +}); + +export type FailToDeliverResponse = z.infer; // Transaction type enum for better type safety export enum TransactionType { diff --git a/packages/types/src/institutional.ts b/packages/types/src/institutional.ts index 5b0e092..4807c7b 100644 --- a/packages/types/src/institutional.ts +++ b/packages/types/src/institutional.ts @@ -1,25 +1,30 @@ -// Institutional trading types for FMP API +// institutional types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -// Form 13F response interface -export interface Form13FResponse { - date: string; - fillingDate: string; - acceptedDate: string; - cik: string; - cusip: string; - tickercusip: string; - nameOfIssuer: string; - shares: number; - titleOfClass: string; - value: number; - link: string; - finalLink: string; -} +export const Form13FResponseSchema = z.object({ + date: z.string(), + fillingDate: z.string(), + acceptedDate: z.string(), + cik: z.string(), + cusip: z.string(), + tickercusip: z.string(), + nameOfIssuer: z.string(), + shares: z.number(), + titleOfClass: z.string(), + value: z.number(), + link: z.string(), + finalLink: z.string() +}); -// Institutional holder response interface -export interface InstitutionalHolderResponse { - holder: string; - shares: number; - dateReported: string; - change: number; -} +export const InstitutionalHolderResponseSchema = z.object({ + holder: z.string(), + shares: z.number(), + dateReported: z.string(), + change: z.number() +}); + +export type Form13FResponse = z.infer; +export type InstitutionalHolderResponse = z.infer; diff --git a/packages/types/src/list.ts b/packages/types/src/list.ts index 57155c4..7323551 100644 --- a/packages/types/src/list.ts +++ b/packages/types/src/list.ts @@ -1,40 +1,53 @@ -export interface StockList { - symbol: string; - exchange: string; - exchangeShortName: string; - price: number; - name: string; - type: string; -} +// list types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -export interface ETFList { - symbol: string; - exchange: string; - exchangeShortName: string; - price: number; - name: string; -} +export const StockListSchema = z.object({ + symbol: z.string(), + exchange: z.string(), + exchangeShortName: z.string(), + price: z.number(), + name: z.string(), + type: z.string() +}); -export interface CryptoList { - symbol: string; - name: string; - currency: string; - stockExchange: string; - exchangeShortName: string; -} +export const ETFListSchema = z.object({ + symbol: z.string(), + exchange: z.string(), + exchangeShortName: z.string(), + price: z.number(), + name: z.string(), + type: z.string() +}); -export interface ForexList { - symbol: string; - name: string; - currency: string; - stockExchange: string; - exchangeShortName: string; -} +export const CryptoListSchema = z.object({ + symbol: z.string(), + name: z.string(), + currency: z.string(), + stockExchange: z.string(), + exchangeShortName: z.string() +}); -export interface AvailableIndexesList { - symbol: string; - name: string; - currency: string; - stockExchange: string; - exchangeShortName: string; -} +export const ForexListSchema = z.object({ + symbol: z.string(), + name: z.string(), + currency: z.string(), + stockExchange: z.string(), + exchangeShortName: z.string() +}); + +export const AvailableIndexesListSchema = z.object({ + symbol: z.string(), + name: z.string(), + currency: z.string(), + stockExchange: z.string(), + exchangeShortName: z.string() +}); + +export type StockList = z.infer; +export type ETFList = z.infer; +export type CryptoList = z.infer; +export type ForexList = z.infer; +export type AvailableIndexesList = z.infer; diff --git a/packages/types/src/market.ts b/packages/types/src/market.ts index fe977d5..4234785 100644 --- a/packages/types/src/market.ts +++ b/packages/types/src/market.ts @@ -1,66 +1,68 @@ // Market data types for FMP API +// +// Schema-first: Zod schemas are the source of truth; TypeScript types are +// derived via `z.infer`. Base schemas generated via `gen:schemas`. -// Market data interfaces -export interface MarketHours { - stockExchangeName: string; - stockMarketHours: { - openingHour: string; - closingHour: string; - }; - stockMarketHolidays: MarketHoliday[]; - isTheStockMarketOpen: boolean; - isTheEuronextMarketOpen: boolean; - isTheForexMarketOpen: boolean; - isTheCryptoMarketOpen: boolean; -} +import { z } from 'zod'; +import { QuoteSchema } from './quote'; -export interface MarketHoliday { - year: number; - 'Martin Luther King, Jr. Day': string; - "Presidents' Day": string; - 'Good Friday': string; - 'Memorial Day': string; - Juneteenth: string; - 'Independence Day': string; - 'Labor Day': string; - 'Thanksgiving Day': string; - Christmas: string; -} +// The named-holiday keys vary year to year (e.g. Juneteenth only appears from +// 2021), so every holiday field except `year` is optional. +export const MarketHolidaySchema = z.object({ + year: z.number(), + 'Martin Luther King, Jr. Day': z.string().optional(), + "Presidents' Day": z.string().optional(), + 'Good Friday': z.string().optional(), + 'Memorial Day': z.string().optional(), + Juneteenth: z.string().optional(), + 'Independence Day': z.string().optional(), + 'Labor Day': z.string().optional(), + 'Thanksgiving Day': z.string().optional(), + Christmas: z.string().optional(), +}); -export interface MarketPerformance { - symbol: string; - name: string; - change: number; - price: number; - changesPercentage: number; -} +export type MarketHoliday = z.infer; -export interface MarketSectorPerformance { - sector: string; - changesPercentage: number; -} +export const MarketHoursSchema = z.object({ + stockExchangeName: z.string(), + stockMarketHours: z.object({ + openingHour: z.string(), + closingHour: z.string(), + }), + stockMarketHolidays: z.array(MarketHolidaySchema), + isTheStockMarketOpen: z.boolean(), + isTheEuronextMarketOpen: z.boolean(), + isTheForexMarketOpen: z.boolean(), + isTheCryptoMarketOpen: z.boolean(), +}); -export interface MarketIndex { - symbol: string; - price: number; - extendedPrice: number | null; - change: number; - dayHigh: number; - dayLow: number; - previousClose: number; - volume: number | null; - open: number; - close: number | null; - lastTradeTime: string; - lastExtendedTradeTime: string | null; - updatedAt: string; - createdAt: string; - type: string; - name: string; - range: string; - yearHigh: number; - yearLow: number; - priceAvg50: number | null; - priceAvg200: number | null; - changesPercentage: number; -} +export type MarketHours = z.infer; + +export const MarketPerformanceSchema = z.object({ + symbol: z.string(), + name: z.string(), + change: z.number(), + price: z.number(), + changesPercentage: z.number(), + // Present on the biggest-gainers/losers/most-actives (stable) endpoints. + exchange: z.string().optional(), +}); + +export type MarketPerformance = z.infer; + +export const MarketSectorPerformanceSchema = z.object({ + sector: z.string(), + // The sector-performance endpoint returns this as a formatted string (e.g. "1.23%"). + changesPercentage: z.string(), +}); + +export type MarketSectorPerformance = z.infer; + +// The `/quotes/index` endpoint (used by both getMarketIndex and +// getMarketPerformance) returns full quote objects for market indices — the +// same shape as Quote, plus an optional `type` discriminator on some responses. +export const MarketIndexSchema = QuoteSchema.extend({ + type: z.string().optional(), +}); + +export type MarketIndex = z.infer; diff --git a/packages/types/src/mutual-fund.ts b/packages/types/src/mutual-fund.ts index e3e68b8..78c35b0 100644 --- a/packages/types/src/mutual-fund.ts +++ b/packages/types/src/mutual-fund.ts @@ -1,9 +1,15 @@ -// Mutual fund types for FMP API +// mutual-fund types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -export interface MutualFundHolding { - holder: string; - shares: number; - dateReported: string; - change: number; - weightPercent: number; -} +export const MutualFundHoldingSchema = z.object({ + holder: z.string(), + shares: z.number(), + dateReported: z.string(), + change: z.number(), + weightPercent: z.number() +}); + +export type MutualFundHolding = z.infer; diff --git a/packages/types/src/news.ts b/packages/types/src/news.ts index f8bd7af..d709149 100644 --- a/packages/types/src/news.ts +++ b/packages/types/src/news.ts @@ -1,23 +1,30 @@ -// News types for FMP API +// news types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -export interface News { - symbol: string; - publishedDate: string; - publisher: string; - title: string; - image: string; - site: string; - text: string; - url: string; -} +export const NewsSchema = z.object({ + symbol: z.string(), + publishedDate: z.string(), + publisher: z.string(), + title: z.string(), + image: z.string(), + site: z.string(), + text: z.string(), + url: z.string() +}); -export interface Article { - title: string; - date: string; - content: string; - tickers: string; - image: string; - link: string; - author: string; - site: string; -} +export const ArticleSchema = z.object({ + title: z.string(), + date: z.string(), + content: z.string(), + tickers: z.string(), + image: z.string(), + link: z.string(), + author: z.string(), + site: z.string() +}); + +export type News = z.infer; +export type Article = z.infer; diff --git a/packages/types/src/quote.ts b/packages/types/src/quote.ts index 6a1c294..ad137eb 100644 --- a/packages/types/src/quote.ts +++ b/packages/types/src/quote.ts @@ -1,50 +1,75 @@ // Quote-related types for FMP API +// +// Schema-first: Zod schemas are the source of truth; the TypeScript types are +// derived via `z.infer`. Regenerate the base schemas from interfaces with +// `pnpm --filter fmp-node-types gen:schemas` (see scripts/), then fold edits here. + +import { z } from 'zod'; // Quote data structure - unified for all asset types -export interface Quote { - symbol: string; - name: string; - price: number; - changesPercentage: number; - change: number; - dayLow: number; - dayHigh: number; - yearHigh: number; - yearLow: number; - marketCap: number | null; - priceAvg50: number; - priceAvg200: number; - exchange: string; - volume: number; - avgVolume: number; - open: number; - previousClose: number; - eps: number | null; - pe: number | null; - earningsAnnouncement: string | null; - sharesOutstanding: number | null; - timestamp: number; -} +export const QuoteSchema = z.object({ + symbol: z.string(), + name: z.string(), + price: z.number(), + changesPercentage: z.number(), + change: z.number(), + dayLow: z.number(), + dayHigh: z.number(), + yearHigh: z.number(), + yearLow: z.number(), + marketCap: z.number().nullable(), + priceAvg50: z.number(), + priceAvg200: z.number(), + exchange: z.string(), + volume: z.number(), + avgVolume: z.number(), + open: z.number(), + previousClose: z.number(), + eps: z.number().nullable(), + pe: z.number().nullable(), + earningsAnnouncement: z.string().nullable(), + sharesOutstanding: z.number().nullable(), + timestamp: z.number(), +}); + +export type Quote = z.infer; // Historical price data structure -export interface HistoricalPriceData { - date: string; - open: number; - high: number; - low: number; - close: number; - adjClose: number; - volume: number; - unadjustedVolume: number; - change: number; - changePercent: number; - vwap: number; - label: string; - changeOverTime: number; -} +export const HistoricalPriceDataSchema = z.object({ + date: z.string(), + open: z.number(), + high: z.number(), + low: z.number(), + close: z.number(), + adjClose: z.number(), + volume: z.number(), + unadjustedVolume: z.number(), + change: z.number(), + changePercent: z.number(), + vwap: z.number(), + label: z.string(), + changeOverTime: z.number(), +}); + +export type HistoricalPriceData = z.infer; // Historical price response wrapper -export interface HistoricalPriceResponse { - symbol: string; - historical: HistoricalPriceData[]; -} +export const HistoricalPriceResponseSchema = z.object({ + symbol: z.string(), + historical: z.array(HistoricalPriceDataSchema), +}); + +export type HistoricalPriceResponse = z.infer; + +// Intraday bars (/historical-chart/{interval}/{symbol}) return a leaner shape +// than HistoricalPriceData — only OHLCV, no adjusted/derived fields. +export const IntradayPriceSchema = z.object({ + date: z.string(), + open: z.number(), + low: z.number(), + high: z.number(), + close: z.number(), + volume: z.number(), +}); + +export type IntradayPrice = z.infer; diff --git a/packages/types/src/screener.ts b/packages/types/src/screener.ts index cbd3229..9c0ce8d 100644 --- a/packages/types/src/screener.ts +++ b/packages/types/src/screener.ts @@ -1,60 +1,73 @@ -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; -} +// screener types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -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 const ScreenerParamsSchema = z.object({ + marketCapMoreThan: z.number().optional(), + marketCapLowerThan: z.number().optional(), + sector: z.string().optional(), + industry: z.string().optional(), + betaMoreThan: z.number().optional(), + betaLowerThan: z.number().optional(), + priceMoreThan: z.number().optional(), + priceLowerThan: z.number().optional(), + dividendMoreThan: z.number().optional(), + dividendLowerThan: z.number().optional(), + volumeMoreThan: z.number().optional(), + volumeLowerThan: z.number().optional(), + exchange: z.string().optional(), + country: z.string().optional(), + isEtf: z.boolean().optional(), + isFund: z.boolean().optional(), + isActivelyTrading: z.boolean().optional(), + limit: z.number().optional(), + includeAllShareClasses: z.boolean().optional() +}); -export interface AvailableExchanges { - exchange: string; - name: string; - countryName: string; - countryCode: string; - symbolSuffix: string; - delay: string; -} +export const ScreenerSchema = z.object({ + symbol: z.string(), + companyName: z.string(), + marketCap: z.number().nullable(), + sector: z.string().nullable(), + industry: z.string().nullable(), + beta: z.number().nullable(), + price: z.number(), + lastAnnualDividend: z.number().nullable(), + volume: z.number(), + exchange: z.string(), + exchangeShortName: z.string(), + country: z.string(), + isEtf: z.boolean(), + isFund: z.boolean(), + isActivelyTrading: z.boolean() +}); -export interface AvailableSectors { - sector: string; -} +export const AvailableExchangesSchema = z.object({ + exchange: z.string(), + name: z.string(), + countryName: z.string(), + countryCode: z.string(), + symbolSuffix: z.string(), + delay: z.string().nullable() +}); -export interface AvailableIndustries { - industry: string; -} +export const AvailableSectorsSchema = z.object({ + sector: z.string() +}); -export interface AvailableCountries { - country: string; -} +export const AvailableIndustriesSchema = z.object({ + industry: z.string() +}); + +export const AvailableCountriesSchema = z.object({ + country: z.string() +}); + +export type ScreenerParams = z.infer; +export type Screener = z.infer; +export type AvailableExchanges = z.infer; +export type AvailableSectors = z.infer; +export type AvailableIndustries = z.infer; +export type AvailableCountries = z.infer; diff --git a/packages/types/src/sec.ts b/packages/types/src/sec.ts index fd4d57a..e2b1ea2 100644 --- a/packages/types/src/sec.ts +++ b/packages/types/src/sec.ts @@ -1,118 +1,123 @@ -// SEC (Securities and Exchange Commission) types +// sec types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -// RSS Feed API (v4) Response -export interface RSSFeedItem { - title: string; - date: string; - link: string; - cik: string; - form_type: string; - ticker: string; - done: boolean; -} +export const RSSFeedItemSchema = z.object({ + title: z.string(), + date: z.string(), + link: z.string(), + cik: z.string(), + form_type: z.string(), + ticker: z.string(), + done: z.boolean() +}); -// RSS Feed V3 API Response -export interface RSSFeedV3Item { - title: string; - date: string; - link: string; - cik: string; - form_type: string; - ticker: string; - done: boolean; -} +export const RSSFeedV3ItemSchema = z.object({ + title: z.string(), + date: z.string(), + link: z.string(), + cik: z.string(), + form_type: z.string(), + ticker: z.string(), + done: z.boolean() +}); -// RSS Feed 8-K API Response -export interface RSSFeed8KItem { - title: string; - symbol: string; - cik: string; - link: string; - finalLink: string; - date: string; - process: string; - hasFinancials: string; -} +export const RSSFeed8KItemSchema = z.object({ + title: z.string(), + symbol: z.string(), + cik: z.string(), + link: z.string(), + finalLink: z.string(), + date: z.string(), + process: z.string(), + hasFinancials: z.string() +}); -// SEC Filings API Response -export interface SECFiling { - symbol: string; - cik: string; - type: string; - link: string; - finalLink: string; - acceptedDate: string; - fillingDate: string; -} +export const SECFilingSchema = z.object({ + symbol: z.string(), + cik: z.string(), + type: z.string(), + link: z.string(), + finalLink: z.string(), + acceptedDate: z.string(), + fillingDate: z.string() +}); -// Industry Classification API Response -export interface IndustryClassification { - symbol: string; - name: string; - cik: string; - sicCode: string; - industryTitle: string; - businessAdress: string; - phoneNumber: string; -} +export const IndustryClassificationSchema = z.object({ + symbol: z.string(), + name: z.string(), + cik: z.string(), + sicCode: z.string(), + industryTitle: z.string(), + businessAdress: z.string(), + phoneNumber: z.string() +}); -// Industry Classification Codes API Response -export interface IndustryClassificationCode { - office: string; - sicCode: string; - industryTitle: string; -} +export const IndustryClassificationCodeSchema = z.object({ + office: z.string(), + sicCode: z.string(), + industryTitle: z.string() +}); -// RSS Feed API (v4) Parameters -export interface RSSFeedParams { - limit?: number; - type?: string; - from?: string; - to?: string; - isDone?: boolean; -} +export const RSSFeedParamsSchema = z.object({ + limit: z.number().optional(), + type: z.string().optional(), + from: z.string().optional(), + to: z.string().optional(), + isDone: z.boolean().optional() +}); -// RSS Feed V3 API Parameters -export interface RSSFeedV3Params { - page?: number; - datatype?: string; -} +export const RSSFeedV3ParamsSchema = z.object({ + page: z.number().optional(), + datatype: z.string().optional() +}); -// RSS Feed All API Response -export interface RSSFeedAllItem { - symbol: string; - fillingDate: string; - acceptedDate: string; - cik: string; - type: string; - link: string; - finalLink: string; -} +export const RSSFeedAllItemSchema = z.object({ + symbol: z.string(), + fillingDate: z.string(), + acceptedDate: z.string(), + cik: z.string(), + type: z.string(), + link: z.string(), + finalLink: z.string() +}); -// RSS Feed 8-K API Parameters -export interface RSSFeed8KParams { - page?: number; - from?: string; - to?: string; - hasFinancial?: boolean; - limit?: number; -} +export const RSSFeed8KParamsSchema = z.object({ + page: z.number().optional(), + from: z.string().optional(), + to: z.string().optional(), + hasFinancial: z.boolean().optional(), + limit: z.number().optional() +}); -// SEC Filings API Parameters -export interface SECFilingsParams { - page?: number; - type?: string; -} +export const SECFilingsParamsSchema = z.object({ + page: z.number().optional(), + type: z.string().optional() +}); -// Individual Industry Classification API Parameters -export interface IndividualIndustryClassificationParams { - symbol?: string; - cik?: number; - sicCode?: number; -} +export const IndividualIndustryClassificationParamsSchema = z.object({ + symbol: z.string().optional(), + cik: z.number().optional(), + sicCode: z.number().optional() +}); -// Industry Classification Codes API Parameters -export interface IndustryClassificationCodesParams { - industryTitle?: string; - sicCode?: number; -} +export const IndustryClassificationCodesParamsSchema = z.object({ + industryTitle: z.string().optional(), + sicCode: z.number().optional() +}); + +export type RSSFeedItem = z.infer; +export type RSSFeedV3Item = z.infer; +export type RSSFeed8KItem = z.infer; +export type SECFiling = z.infer; +export type IndustryClassification = z.infer; +export type IndustryClassificationCode = z.infer; +export type RSSFeedParams = z.infer; +export type RSSFeedV3Params = z.infer; +export type RSSFeedAllItem = z.infer; +export type RSSFeed8KParams = z.infer; +export type SECFilingsParams = z.infer; +export type IndividualIndustryClassificationParams = z.infer; +export type IndustryClassificationCodesParams = z.infer; diff --git a/packages/types/src/senate-house.ts b/packages/types/src/senate-house.ts index 8c23fdf..1587d89 100644 --- a/packages/types/src/senate-house.ts +++ b/packages/types/src/senate-house.ts @@ -1,57 +1,64 @@ -// Senate and House trading types for FMP API +// senate-house types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Base schemas generated via `pnpm --filter fmp-node-types gen:schemas`. +import { z } from "zod"; -// Senate trading response interface -export interface SenateTradingResponse { - symbol: string; - disclosureDate: string; - transactionDate: string; - firstName: string; - lastName: string; - office: string; - district: string; - owner: string; - assetDescription: string; - assetType: string; - type: string; - amount: string; - capitalGainsOver200USD: string; - comment: string; - link: string; -} +export const SenateTradingResponseSchema = z.object({ + symbol: z.string(), + disclosureDate: z.string(), + transactionDate: z.string(), + firstName: z.string(), + lastName: z.string(), + office: z.string(), + district: z.string(), + owner: z.string(), + assetDescription: z.string(), + assetType: z.string(), + type: z.string(), + amount: z.string(), + // The senate-latest RSS feed omits this field that senate-trades includes. + capitalGainsOver200USD: z.string().optional(), + comment: z.string(), + link: z.string() +}); -// House trading response interface -export interface HouseTradingResponse { - symbol: string; - disclosureDate: string; - transactionDate: string; - firstName: string; - lastName: string; - office: string; - district: string; - owner: string; - assetDescription: string; - assetType: string; - type: string; - amount: string; - capitalGainsOver200USD: string; - comment: string; - link: string; -} +export const HouseTradingResponseSchema = z.object({ + symbol: z.string(), + disclosureDate: z.string(), + transactionDate: z.string(), + firstName: z.string(), + lastName: z.string(), + office: z.string(), + district: z.string(), + owner: z.string(), + assetDescription: z.string(), + assetType: z.string(), + type: z.string(), + amount: z.string(), + capitalGainsOver200USD: z.string(), + comment: z.string(), + link: z.string() +}); -export interface SenateHouseTradingByNameResponse { - symbol: string; - disclosureDate: string; - transactionDate: string; - firstName: string; - lastName: string; - office: string; - district: string; - owner: string; - assetDescription: string; - assetType: string; - type: string; - amount: string; - capitalGainsOver200USD: string; - comment: string; - link: string; -} +export const SenateHouseTradingByNameResponseSchema = z.object({ + symbol: z.string(), + disclosureDate: z.string(), + transactionDate: z.string(), + firstName: z.string(), + lastName: z.string(), + office: z.string(), + district: z.string(), + owner: z.string(), + assetDescription: z.string(), + assetType: z.string(), + type: z.string(), + amount: z.string(), + capitalGainsOver200USD: z.string(), + comment: z.string(), + link: z.string() +}); + +export type SenateTradingResponse = z.infer; +export type HouseTradingResponse = z.infer; +export type SenateHouseTradingByNameResponse = z.infer; diff --git a/packages/types/src/stock.ts b/packages/types/src/stock.ts index 55f19e2..ec1d3bd 100644 --- a/packages/types/src/stock.ts +++ b/packages/types/src/stock.ts @@ -1,56 +1,79 @@ // Stock-related types for FMP API +// +// Schema-first: Zod schemas are the source of truth; TypeScript types are +// derived via `z.infer`. Schemas here are hand-written because `ts-to-zod` +// cannot generate from the generic wrapper interfaces below. -export interface StockSplit { - date: string; - label: string; - numerator: number; - denominator: number; -} +import { z } from 'zod'; -export interface StockSplitResponse { - symbol: string; - historical: StockSplit[]; -} +export const StockSplitSchema = z.object({ + date: z.string(), + label: z.string(), + numerator: z.number(), + denominator: z.number(), +}); -export interface StockDividend { - date: string; - label: string; - adjDividend: number; - dividend: number; - recordDate: string; - paymentDate: string; - declarationDate: string; -} +export type StockSplit = z.infer; -export interface StockDividendResponse { - symbol: string; - historical: StockDividend[]; -} +export const StockSplitResponseSchema = z.object({ + symbol: z.string(), + historical: z.array(StockSplitSchema), +}); -export interface MarketCap { - symbol: string; - date: string; - marketCap: number; -} +export type StockSplitResponse = z.infer; -export interface StockRealTimePrice { - symbol: string; - price: number; -} +export const StockDividendSchema = z.object({ + date: z.string(), + label: z.string(), + adjDividend: z.number(), + dividend: z.number(), + recordDate: z.string(), + paymentDate: z.string(), + declarationDate: z.string(), +}); -export interface StockRealTimePriceFull { - bidSize: number; - askPrice: number; - volume: number; - askSize: number; - bidPrice: number; - lastSalePrice: number; - lastSaleSize: number; - lastSaleTime: number; - fmpLast: number; - lastUpdated: number; - symbol: string; -} +export type StockDividend = z.infer; + +export const StockDividendResponseSchema = z.object({ + symbol: z.string(), + historical: z.array(StockDividendSchema), +}); + +export type StockDividendResponse = z.infer; + +export const MarketCapSchema = z.object({ + symbol: z.string(), + date: z.string(), + marketCap: z.number(), +}); + +export type MarketCap = z.infer; + +export const StockRealTimePriceSchema = z.object({ + symbol: z.string(), + price: z.number(), +}); + +export type StockRealTimePrice = z.infer; + +export const StockRealTimePriceFullSchema = z.object({ + bidSize: z.number(), + askPrice: z.number(), + volume: z.number(), + askSize: z.number(), + bidPrice: z.number(), + lastSalePrice: z.number(), + lastSaleSize: z.number(), + lastSaleTime: z.number(), + fmpLast: z.number(), + lastUpdated: z.number(), + symbol: z.string(), +}); + +export type StockRealTimePriceFull = z.infer; + +// Generic response wrappers — kept as plain interfaces because Zod schemas +// cannot represent open generics. Not part of the schema-first surface. // For endpoints that return { stockList: [...] } export interface StockListResponse { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 304fe9d..b8256b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -267,6 +267,9 @@ importers: typescript: specifier: ^5.3.3 version: 5.8.3 + zod: + specifier: ^3.25.76 + version: 3.25.76 packages/tools: dependencies: @@ -321,6 +324,10 @@ importers: version: 3.25.76 packages/types: + dependencies: + zod: + specifier: ^3.25.76 + version: 3.25.76 devDependencies: '@types/node': specifier: ^20.11.0 @@ -328,6 +335,9 @@ importers: prettier: specifier: ^3.2.5 version: 3.6.2 + ts-to-zod: + specifier: ^5.1.0 + version: 5.1.0 tsup: specifier: ^8.0.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) @@ -597,6 +607,12 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@clack/core@1.0.0-alpha.4': + resolution: {integrity: sha512-VCtU+vjyKPMSakVrB9q1bOnXN7QW/w4+YQDQCOF59GrzydW+169i0fVx/qzRRXJgt8KGj/pZZ/JxXroFZIDByg==} + + '@clack/prompts@1.0.0-alpha.4': + resolution: {integrity: sha512-KnmtDF2xQGoI5AlBme9akHtvCRV0RKAARUXHBQO2tMwnY8B08/4zPWigT7uLK25UPrMCEqnyQPkKRjNdhPbf8g==} + '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} @@ -1212,6 +1228,10 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@oclif/core@4.11.4': + resolution: {integrity: sha512-URwiQ5ALx/sJ2iH4vzXEd+H4K6NAI7LRs6Jag3hrgKEpGmaE6alfRC8qjO4GIgb6A3ACaJumqP9twi/M9ywdHQ==} + engines: {node: '>=18.0.0'} + '@openai/agents-core@0.1.0': resolution: {integrity: sha512-SASFdtW71/3Fmjl1gSCIIDTqeDkRQxU7H8SqpMFeB+lbXtnNFTxR5Wt6XnEdj++dRRY8x3EbRnAx8lT7CZGioA==} peerDependencies: @@ -1642,6 +1662,11 @@ packages: resolution: {integrity: sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/vfs@1.6.4': + resolution: {integrity: sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==} + peerDependencies: + typescript: '*' + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -1785,6 +1810,10 @@ packages: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1793,6 +1822,10 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1805,6 +1838,14 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@3.17.0: + resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} + engines: {node: '>=14'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -1931,6 +1972,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -1949,6 +1994,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2074,6 +2123,22 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + clean-stack@3.0.1: + resolution: {integrity: sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==} + engines: {node: '>=10'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -2109,6 +2174,9 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2205,6 +2273,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} @@ -2310,6 +2387,9 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2328,6 +2408,10 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -2566,6 +2650,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.0.3: resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} engines: {node: '>=20.0.0'} @@ -2640,6 +2727,15 @@ packages: picomatch: optional: true + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2762,6 +2858,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2926,6 +3026,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -3007,6 +3111,11 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3019,6 +3128,14 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} @@ -3112,6 +3229,10 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -3435,6 +3556,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3459,6 +3584,10 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -3696,6 +3825,14 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3880,6 +4017,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + openai@4.104.0: resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} hasBin: true @@ -4012,6 +4153,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -4286,10 +4431,17 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -4341,6 +4493,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} @@ -4409,6 +4566,18 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4475,6 +4644,14 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} @@ -4509,6 +4686,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -4593,9 +4774,72 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + text-camel-case@1.2.11: + resolution: {integrity: sha512-2ZsM/gOlB1tyza+8lGLvs6gtPuZ9qEYuKPa+gwo38m65wkY4k323SK4hT7ku8r5wIKyspUYIWSk1aB9/Jjxr7A==} + + text-capital-case@1.2.11: + resolution: {integrity: sha512-30A7B7+VUvevEmPE0xWK1Z2z0ncl/JTjSUBLfjpoXrkwuPpmNTVbjHShRTN3cX9GIuZn/P3jvR+TO9JiTZcl8A==} + + text-case@1.2.11: + resolution: {integrity: sha512-LbdWNQeuXWbfav+pxBxvaefkziffMYeSA53BHp52cgJa9rjiC0dkjum9AKrH8iQWoQJ4InGPSGexLeerGFaZ1Q==} + + text-constant-case@1.2.11: + resolution: {integrity: sha512-XnTBILsa7UpMWncUCchqIybZlg15FUcrlyNaWIJ8ybPy54qcoN513EXFswueyizuAgyJFXPCwwSFbSji6kw/Uw==} + + text-dot-case@1.2.11: + resolution: {integrity: sha512-7SLKiT45KZO0qad0+p+GvC0+F+6pZ851HJcTcBJiSF88HsK/e1qErlGLtVBT6hkTHIaAj48WfSyQr4lZRv1xJQ==} + + text-header-case@1.2.11: + resolution: {integrity: sha512-7OBHd2g7X+aH6rXMC3cANFh6yvhXjXkyumw2NaRwJRIk343pP2e1SQCTCfowPDmmi8wkZVqz1fdWNq5LwvcBOQ==} + + text-is-lower-case@1.2.11: + resolution: {integrity: sha512-dBqPAkNmX7eTM7ZbS3D/UBCQ5i9EXt5tujF2wIGGbZ1+aN8bY7Qda4mDpxgd6Hbzf/z10uQWRNzupl99wFQ8CQ==} + + text-is-upper-case@1.2.11: + resolution: {integrity: sha512-MZeUIYEYfKZ2FSeg0vnHCH4mHXLgGzes+iz2K+4BYnhnkEa2svKA1nNjQAqTUiVNHOPqPCuzmUr1LsyQZ73uyA==} + + text-kebab-case@1.2.11: + resolution: {integrity: sha512-RIg9iN6VwH+JrX9dFdm1nd1efPGR9LjNc0CiQz496sQETeKGkDEzxES/ZzxbkerrAL2DFEMGdLXckzDz1OEDBQ==} + + text-lower-case-first@1.2.11: + resolution: {integrity: sha512-QR483XLyuyIpq8tKu1ds3Q1jfsgfaa/p9rtoQKHe6Rv5ah9ic/SUzTGN0MQ7UIS9APADd8SUPn5TTh1Z2/ACyg==} + + text-lower-case@1.2.11: + resolution: {integrity: sha512-txTy6y0y8M23Lhf0mk8WcvXTqlf4OQ3AGnDsRB6o3uMNfIa0CJDol2s1PdKNa63rt5B2277zkZCCn6Xeq//big==} + + text-no-case@1.2.11: + resolution: {integrity: sha512-wazS7FEq0Ct3aJzeE8MEMcSs0eW4+/X/fwdotv/rG66bLS+g1T0pa0gUsbBGjjLFs191AIXVIry+bYE0uaaBBQ==} + + text-param-case@1.2.11: + resolution: {integrity: sha512-3EMMAMLSz/mJXOnATNnrS+dZAvghpq09VhOVYDOkUnbm5zlYc6iU5AZOKVDpiAVVllQ9P1h5IKVZzsEYrdIRGw==} + + text-pascal-case@1.2.11: + resolution: {integrity: sha512-BNhQ1O/g/Q4dH5gPyLIJLDLDknl2dipBwV629ScsiZCKJaCLGXYhTXp23rp9Htg3O5OSSsiU3mqDKq+pBmwTSw==} + + text-path-case@1.2.11: + resolution: {integrity: sha512-FsJU4BmMdtLtmnBK/XRJPqTwLF8yFiTEClHjxlQjSAG5Xt9R4p6D1WNaM1CI2dG5Lr4rsFM4jiVC620m0AsRbw==} + + text-sentence-case@1.2.11: + resolution: {integrity: sha512-ApiVsvdLy+Wb8x7mZRVuoy8VO12jJ22G2djVM3ZZbUhXVIkqGgHxmiXRwdhRPoWGojK9n53m7jviJgBVNdRn+g==} + + text-snake-case@1.2.11: + resolution: {integrity: sha512-NOEQvjyyVABB41SS8dUx423Y6hWS+Z4TrAAJg1xzCkOD3q9y0JtdJjCvCA1FWI8oDu+HiIOp/N446uDM8j54XQ==} + + text-swap-case@1.2.11: + resolution: {integrity: sha512-PBmC5xvZdDZ4suikydpeXH0s4JV2XHelMj9/OEXEbA3oLpdV2A+B4BspVDWVw7C2Gi5eCareqk/7EE8I1/WwgQ==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + text-title-case@1.2.11: + resolution: {integrity: sha512-V1GZy0XlqdkYUQm0tqm1jqtYlXJqFVMreBCTUReOaz8d/JbozTSpZrcakIeV8+1bN7LsvfhPFhA5zREiax6YIA==} + + text-upper-case-first@1.2.11: + resolution: {integrity: sha512-vgfbwKo8TEJbRsapR9LWWvIJRnv8u9aXVa6cyYOAQQmurCx54Cnt59x5fKdiq+hFaBJ51AbzCgMpbP3p65/pHQ==} + + text-upper-case@1.2.11: + resolution: {integrity: sha512-BfTL7yB1YIRlVGNdZUvno013hOq2cRs07fDR2ApppOXRDuKrEmsLDEY82xXlDzQHELp0jexqkI+NeyPIl6MtMw==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -4614,6 +4858,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -4681,9 +4929,16 @@ packages: jest-util: optional: true + ts-to-zod@5.1.0: + resolution: {integrity: sha512-giqqlvRHunlJqG9tBL/KAO3wWIVZGF//mZiWLKm/fdQnKnz4EN2mtiK5cugN9slytBkdMEXQIaLvMzIScbhhFw==} + hasBin: true + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -4706,6 +4961,12 @@ packages: typescript: optional: true + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + tsx@4.20.3: resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} engines: {node: '>=18.0.0'} @@ -4912,10 +5173,17 @@ packages: engines: {node: '>= 8'} hasBin: true + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -4924,6 +5192,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4983,6 +5255,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -5360,6 +5635,17 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 + '@clack/core@1.0.0-alpha.4': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@1.0.0-alpha.4': + dependencies: + '@clack/core': 1.0.0-alpha.4 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@emnapi/core@1.4.3': dependencies: '@emnapi/wasi-threads': 1.0.2 @@ -5988,6 +6274,27 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@oclif/core@4.11.4': + dependencies: + ansi-escapes: 4.3.2 + ansis: 3.17.0 + clean-stack: 3.0.1 + cli-spinners: 2.9.2 + debug: 4.4.3(supports-color@8.1.1) + ejs: 3.1.10 + get-package-type: 0.1.0 + indent-string: 4.0.0 + is-wsl: 2.2.0 + lilconfig: 3.1.3 + minimatch: 10.2.5 + semver: 7.8.1 + string-width: 4.2.3 + supports-color: 8.1.1 + tinyglobby: 0.2.16 + widest-line: 3.1.0 + wordwrap: 1.0.0 + wrap-ansi: 7.0.0 + '@openai/agents-core@0.1.0(ws@8.18.3)(zod@3.25.76)': dependencies: debug: 4.4.1 @@ -6480,6 +6787,13 @@ snapshots: '@typescript-eslint/types': 8.39.0 eslint-visitor-keys: 4.2.1 + '@typescript/vfs@1.6.4(typescript@5.8.3)': + dependencies: + debug: 4.4.3(supports-color@8.1.1) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-android-arm-eabi@1.9.2': @@ -6590,10 +6904,16 @@ snapshots: dependencies: type-fest: 0.21.3 + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -6602,6 +6922,10 @@ snapshots: ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} + + ansis@3.17.0: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -6783,6 +7107,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -6813,6 +7139,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -6926,6 +7256,21 @@ snapshots: dependencies: clsx: 2.1.1 + clean-stack@3.0.1: + dependencies: + escape-string-regexp: 4.0.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.1 + client-only@0.0.1: {} cliui@8.0.1: @@ -6960,6 +7305,8 @@ snapshots: color-string: 1.9.1 optional: true + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -7081,6 +7428,12 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -7161,6 +7514,8 @@ snapshots: emittery@0.13.1: {} + emoji-regex@10.4.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -7178,6 +7533,8 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + environment@1.1.0: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -7364,8 +7721,8 @@ snapshots: '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.32.0(jiti@2.4.2)) @@ -7399,7 +7756,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -7410,7 +7767,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.9.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -7425,14 +7782,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -7465,7 +7822,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7476,7 +7833,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -7747,6 +8104,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@5.0.4: {} + eventsource-parser@3.0.3: {} eventsource@3.0.7: @@ -7862,6 +8221,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -7997,6 +8360,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8208,6 +8573,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -8297,6 +8664,8 @@ snapshots: is-decimal@2.0.1: {} + is-docker@2.2.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -8305,6 +8674,14 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.6.0 + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 + is-generator-fn@2.1.0: {} is-generator-function@1.1.0: @@ -8387,6 +8764,10 @@ snapshots: is-windows@1.0.2: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + isarray@2.0.5: {} isexe@2.0.0: {} @@ -8968,6 +9349,15 @@ snapshots: lines-and-columns@1.2.4: {} + listr2@9.0.5: + dependencies: + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + load-tsconfig@0.2.5: {} locate-path@5.0.0: @@ -8986,6 +9376,14 @@ snapshots: lodash.startcase@4.4.0: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -9485,6 +9883,12 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -9665,6 +10069,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + openai@4.104.0(ws@8.18.3)(zod@3.25.76): dependencies: '@types/node': 18.19.122 @@ -9793,6 +10201,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.4: {} + pify@2.3.0: {} pify@4.0.1: {} @@ -10106,8 +10516,15 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + reusify@1.1.0: {} + rfdc@1.4.1: {} + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -10187,6 +10604,8 @@ snapshots: semver@7.7.2: {} + semver@7.8.1: {} + send@1.2.0: dependencies: debug: 4.4.1 @@ -10315,6 +10734,18 @@ snapshots: slash@3.0.0: {} + slash@5.1.0: {} + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} source-map-support@0.5.13: @@ -10377,6 +10808,17 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.1.0 + + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 @@ -10440,6 +10882,10 @@ snapshots: dependencies: ansi-regex: 6.1.0 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-bom@3.0.0: {} strip-bom@4.0.0: {} @@ -10539,8 +10985,101 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + text-camel-case@1.2.11: + dependencies: + text-pascal-case: 1.2.11 + + text-capital-case@1.2.11: + dependencies: + text-no-case: 1.2.11 + text-upper-case-first: 1.2.11 + + text-case@1.2.11: + dependencies: + text-camel-case: 1.2.11 + text-capital-case: 1.2.11 + text-constant-case: 1.2.11 + text-dot-case: 1.2.11 + text-header-case: 1.2.11 + text-is-lower-case: 1.2.11 + text-is-upper-case: 1.2.11 + text-kebab-case: 1.2.11 + text-lower-case: 1.2.11 + text-lower-case-first: 1.2.11 + text-no-case: 1.2.11 + text-param-case: 1.2.11 + text-pascal-case: 1.2.11 + text-path-case: 1.2.11 + text-sentence-case: 1.2.11 + text-snake-case: 1.2.11 + text-swap-case: 1.2.11 + text-title-case: 1.2.11 + text-upper-case: 1.2.11 + text-upper-case-first: 1.2.11 + + text-constant-case@1.2.11: + dependencies: + text-no-case: 1.2.11 + text-upper-case: 1.2.11 + + text-dot-case@1.2.11: + dependencies: + text-no-case: 1.2.11 + + text-header-case@1.2.11: + dependencies: + text-capital-case: 1.2.11 + + text-is-lower-case@1.2.11: {} + + text-is-upper-case@1.2.11: {} + + text-kebab-case@1.2.11: + dependencies: + text-no-case: 1.2.11 + + text-lower-case-first@1.2.11: {} + + text-lower-case@1.2.11: {} + + text-no-case@1.2.11: + dependencies: + text-lower-case: 1.2.11 + + text-param-case@1.2.11: + dependencies: + text-dot-case: 1.2.11 + + text-pascal-case@1.2.11: + dependencies: + text-no-case: 1.2.11 + + text-path-case@1.2.11: + dependencies: + text-dot-case: 1.2.11 + + text-sentence-case@1.2.11: + dependencies: + text-no-case: 1.2.11 + text-upper-case-first: 1.2.11 + + text-snake-case@1.2.11: + dependencies: + text-dot-case: 1.2.11 + + text-swap-case@1.2.11: {} + text-table@0.2.0: {} + text-title-case@1.2.11: + dependencies: + text-no-case: 1.2.11 + text-upper-case-first: 1.2.11 + + text-upper-case-first@1.2.11: {} + + text-upper-case@1.2.11: {} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -10558,6 +11097,11 @@ snapshots: fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -10650,6 +11194,22 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.27.7) jest-util: 29.7.0 + ts-to-zod@5.1.0: + dependencies: + '@clack/prompts': 1.0.0-alpha.4 + '@oclif/core': 4.11.4 + '@typescript/vfs': 1.6.4(typescript@5.8.3) + chokidar: 4.0.3 + listr2: 9.0.5 + slash: 5.1.0 + text-case: 1.2.11 + tslib: 2.8.1 + tsutils: 3.21.0(typescript@5.8.3) + typescript: 5.8.3 + zod: 4.4.3 + transitivePeerDependencies: + - supports-color + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -10657,6 +11217,8 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@1.14.1: {} + tslib@2.8.1: {} tsup@8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0): @@ -10687,6 +11249,11 @@ snapshots: - tsx - yaml + tsutils@3.21.0(typescript@5.8.3): + dependencies: + tslib: 1.14.1 + typescript: 5.8.3 + tsx@4.20.3: dependencies: esbuild: 0.25.5 @@ -10959,8 +11526,14 @@ snapshots: dependencies: isexe: 2.0.0 + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -10973,6 +11546,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} write-file-atomic@4.0.2: @@ -11012,4 +11591,6 @@ snapshots: zod@3.25.76: {} + zod@4.4.3: {} + zwitch@2.0.4: {} diff --git a/turbo.json b/turbo.json index bc4f94b..e0a5333 100644 --- a/turbo.json +++ b/turbo.json @@ -42,11 +42,6 @@ "outputs": ["coverage/**"], "env": ["FMP_API_KEY", "NODE_ENV"] }, - "test:integration": { - "dependsOn": ["^build"], - "outputs": ["coverage/**"], - "env": ["FMP_API_KEY", "NODE_ENV"] - }, "test:endpoints": { "dependsOn": ["^build"], "outputs": ["coverage/**"], From 7688f5f772bde13daba70507fd277b008c372bce Mon Sep 17 00:00:00 2001 From: e-roy Date: Mon, 25 May 2026 00:12:38 -0400 Subject: [PATCH 03/13] update tools --- apps/examples/openai/package.json | 5 +- apps/examples/vercel-ai/package.json | 12 +- .../vercel-ai/src/app/api/chat/route.ts | 6 +- packages/tools/package.json | 8 +- .../utils/openai-tool-wrapper.test.ts | 129 +--- .../tools/src/utils/aisdk-tool-wrapper.ts | 8 +- .../tools/src/utils/openai-tool-wrapper.ts | 35 +- packages/tools/src/utils/version-check.ts | 2 +- pnpm-lock.yaml | 680 +++++++++--------- 9 files changed, 379 insertions(+), 506 deletions(-) diff --git a/apps/examples/openai/package.json b/apps/examples/openai/package.json index 93517fb..095e943 100644 --- a/apps/examples/openai/package.json +++ b/apps/examples/openai/package.json @@ -9,13 +9,12 @@ "lint": "next lint" }, "dependencies": { - "@openai/agents": "^0.1.0", + "@openai/agents": "^0.11.5", "fmp-ai-tools": "workspace:*", "next": "15.3.0", - "openai": "^4.63.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "zod": "^3.25.76" + "zod": "^4.0.0" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/apps/examples/vercel-ai/package.json b/apps/examples/vercel-ai/package.json index 2834e32..8787998 100644 --- a/apps/examples/vercel-ai/package.json +++ b/apps/examples/vercel-ai/package.json @@ -10,13 +10,13 @@ }, "dependencies": { "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", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "ai": "^6.0.191", + "@ai-sdk/openai": "^3.0.65", + "@ai-sdk/react": "^3.0.193", "fmp-ai-tools": "workspace:*", - "zod": "^3.25.76" + "zod": "^4.0.0" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/apps/examples/vercel-ai/src/app/api/chat/route.ts b/apps/examples/vercel-ai/src/app/api/chat/route.ts index 5e6ddac..0836f92 100644 --- a/apps/examples/vercel-ai/src/app/api/chat/route.ts +++ b/apps/examples/vercel-ai/src/app/api/chat/route.ts @@ -7,9 +7,13 @@ export async function POST(req: Request) { const result = streamText({ model: openai('gpt-4o-mini'), - messages: convertToModelMessages(messages), + // ai@6: convertToModelMessages is now async. + messages: await convertToModelMessages(messages), tools: fmpTools, stopWhen: stepCountIs(5), + // @ai-sdk/openai@3 defaults strictJsonSchema:true; FMP tools use optional + // params, which OpenAI strict function schemas reject — so disable it. + providerOptions: { openai: { strictJsonSchema: false } }, }); return result.toUIMessageStreamResponse(); diff --git a/packages/tools/package.json b/packages/tools/package.json index ea72e47..a2b9b84 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -52,15 +52,15 @@ }, "homepage": "https://fmp-node-wrapper-docs.vercel.app", "dependencies": { - "@openai/agents": "^0.1.0", - "ai": "^5.0.5", + "@openai/agents": "^0.11.5", + "ai": "^6.0.191", "fmp-node-api": "workspace:*" }, "peerDependencies": { - "zod": "^3.25.76 || ^4.0.0" + "zod": "^4.0.0" }, "devDependencies": { - "zod": "^3.25.76", + "zod": "^4.0.0", "@types/jest": "^29.5.0", "@types/node": "^20.11.0", "@typescript-eslint/eslint-plugin": "^8.0.0", 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 20022cc..65c74f9 100644 --- a/packages/tools/src/__tests__/utils/openai-tool-wrapper.test.ts +++ b/packages/tools/src/__tests__/utils/openai-tool-wrapper.test.ts @@ -1,17 +1,12 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; +// The wrapper now passes the Zod schema straight to @openai/agents `tool()`, +// which derives the JSON schema and validates input itself. (The hand-rolled +// all-string JSON schema + manual parse/error handling was removed.) describe('createOpenAITool', () => { - it('maps Zod schema to OpenAI parameters (string, number, boolean, enum, array, optional, default)', async () => { - const schema = z.object({ - aString: z.string().describe('A string field'), - aNumber: z.number(), - aBoolean: z.boolean(), - anEnum: z.enum(['A', 'B']).describe('Enum field'), - anArray: z.array(z.string()), - optionalField: z.string().optional().describe('Optional string'), - defaultField: z.number().default(42).describe('Default number'), - }); + it('passes name, description, and the Zod schema as `parameters`', () => { + const schema = z.object({ symbol: z.string(), limit: z.string().optional() }); const tool = createOpenAITool({ name: 'testTool', @@ -20,119 +15,25 @@ describe('createOpenAITool', () => { execute: async () => 'ok', }); - // 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'); expect(tool.description).toBe('Test tool'); - - // Check that the tool has the expected structure for the new API - expect(tool).toBeDefined(); - expect(typeof tool).toBe('object'); - }); - - it('validates input and returns validation errors from Zod', async () => { - const schema = z.object({ sym: z.string().min(1, 'required') }); - const tool = createOpenAITool({ - name: 'validateTool', - description: 'Validates', - inputSchema: schema, - execute: async () => 'ok', - }); - - const result = await (tool as any).execute({ sym: '' }); - expect(result).toContain('Invalid input:'); - expect(result).toContain('required'); - }); - - it('returns execution errors as string messages', async () => { - const schema = z.object({ x: z.string() }); - const tool = createOpenAITool({ - name: 'errorTool', - description: 'Errors out', - inputSchema: schema, - execute: async () => { - throw new Error('boom'); - }, - }); - - const result = await (tool as any).execute({ x: 'y' }); - expect(result).toContain('Error executing errorTool: boom'); + // The Zod schema is handed to the SDK as-is (no JSON-schema conversion here). + expect((tool as any).parameters).toBe(schema); }); - it('passes validated input to execute', async () => { - const schema = z.object({ n: z.number().default(1), s: z.string().optional() }); - const spy = jest.fn(async () => 'done'); + it('invokes the provided execute with the input and returns its result', async () => { + const spy = jest.fn(async (args: { symbol: string }) => `got ${args.symbol}`); const tool = createOpenAITool({ - name: 'passTool', - description: 'Passes args', - inputSchema: schema, + name: 'execTool', + description: 'Executes', + inputSchema: z.object({ symbol: z.string() }), execute: spy, }); - await (tool as any).execute({}); - - expect(spy).toHaveBeenCalledWith({ n: 1 }); - }); - - it('extracts description from inner schema for optional/default and maps unknown types via fallback', () => { - const schema = z.object({ - // Description only on inner schema, not on optional wrapper - optInner: z.string().describe('Inner optional').optional(), - // Description only on inner schema, not on default wrapper - defInner: z.number().describe('Inner default').default(7), - // Unknown type triggers fallback mapping - unknown: z.any(), - }); - - const tool = createOpenAITool({ - name: 'branchTool', - description: 'Covers branches', - inputSchema: schema, - execute: async () => 'ok', - }); - - // 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'); - expect(tool.description).toBe('Covers branches'); + const result = await (tool as any).execute({ symbol: 'AAPL' }); - // Check that the tool has the expected structure for the new API - expect(tool).toBeDefined(); - expect(typeof tool).toBe('object'); + expect(spy).toHaveBeenCalledWith({ symbol: 'AAPL' }); + expect(result).toBe('got AAPL'); }); }); diff --git a/packages/tools/src/utils/aisdk-tool-wrapper.ts b/packages/tools/src/utils/aisdk-tool-wrapper.ts index 6a9a060..5032232 100644 --- a/packages/tools/src/utils/aisdk-tool-wrapper.ts +++ b/packages/tools/src/utils/aisdk-tool-wrapper.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { tool, ToolSet } from 'ai'; +import { tool } from 'ai'; import { logApiExecutionWithTiming } from './logger'; interface AISDKToolConfig { @@ -13,9 +13,11 @@ export const createTool = (config: AISDKToolConfig) => { const { name, description, inputSchema, execute } = config; return tool({ description, - inputSchema, + // `as any` avoids ai@6 tool()'s excessively-deep type instantiation on a + // generic Zod schema; the real Zod schema is still passed at runtime. + inputSchema: inputSchema as any, execute: async (input: any) => { return await logApiExecutionWithTiming(name, input, () => execute(input)); }, - } as ToolSet); + }); }; diff --git a/packages/tools/src/utils/openai-tool-wrapper.ts b/packages/tools/src/utils/openai-tool-wrapper.ts index ac0fc7f..5a85011 100644 --- a/packages/tools/src/utils/openai-tool-wrapper.ts +++ b/packages/tools/src/utils/openai-tool-wrapper.ts @@ -12,35 +12,18 @@ 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[] = []; - - Object.entries(inputSchema.shape).forEach(([key, _fieldSchema]) => { - properties[key] = { type: 'string' }; - required.push(key); - }); - + // @openai/agents derives the JSON schema from the Zod schema and validates + // input against it before invoking execute. The cast widens our generic T to + // the SDK's ZodObjectLike `parameters` type (runtime value is the real schema). + // We also parse here so Zod defaults/coercion are applied consistently before + // execute, regardless of whether the caller pre-validated. return tool({ name, description, - parameters: { - type: 'object', - properties, - required, - additionalProperties: false, - } as any, - strict: true, - execute: async (args: z.TypeOf) => { - try { - const validatedInput = inputSchema.parse(args); - return await logApiExecutionWithTiming(name, validatedInput, () => execute(validatedInput)); - } catch (error) { - if (error instanceof z.ZodError) { - return `Invalid input: ${error.errors.map(e => e.message).join(', ')}`; - } - return `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`; - } + parameters: inputSchema as z.ZodObject, + execute: async (input: unknown) => { + const args = inputSchema.parse(input) as z.infer; + return await logApiExecutionWithTiming(name, args, () => execute(args)); }, }); } diff --git a/packages/tools/src/utils/version-check.ts b/packages/tools/src/utils/version-check.ts index f99ec31..4a48712 100644 --- a/packages/tools/src/utils/version-check.ts +++ b/packages/tools/src/utils/version-check.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; -const REQUIRED_VERSION = '0.1.0'; +const REQUIRED_VERSION = '0.11.0'; /** * Gets the actual installed version of a package diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8256b6..d92f930 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,17 +133,14 @@ importers: apps/examples/openai: dependencies: '@openai/agents': - specifier: ^0.1.0 - version: 0.1.0(ws@8.18.3)(zod@3.25.76) + specifier: ^0.11.5 + version: 0.11.5(ws@8.18.3)(zod@4.4.3) fmp-ai-tools: specifier: workspace:* version: link:../../../packages/tools next: 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) react: specifier: ^19.0.0 version: 19.1.0 @@ -151,8 +148,8 @@ importers: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.0.0 + version: 4.4.3 devDependencies: '@types/node': specifier: ^20.0.0 @@ -185,29 +182,29 @@ importers: apps/examples/vercel-ai: dependencies: '@ai-sdk/openai': - specifier: ^2.0.3 - version: 2.0.4(zod@3.25.76) + specifier: ^3.0.65 + version: 3.0.65(zod@4.4.3) '@ai-sdk/react': - specifier: ^2.0.3 - version: 2.0.6(react@19.1.0)(zod@3.25.76) + specifier: ^3.0.193 + version: 3.0.193(react@19.2.6)(zod@4.4.3) ai: - specifier: ^5.0.5 - version: 5.0.5(zod@3.25.76) + specifier: ^6.0.191 + version: 6.0.191(zod@4.4.3) fmp-ai-tools: specifier: workspace:* version: link:../../../packages/tools next: 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) + version: 15.3.0(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: - specifier: ^19.0.0 - version: 19.1.0 + specifier: ^19.2.1 + version: 19.2.6 react-dom: - specifier: ^19.0.0 - version: 19.1.0(react@19.1.0) + specifier: ^19.2.1 + version: 19.2.6(react@19.2.6) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.0.0 + version: 4.4.3 devDependencies: '@types/node': specifier: ^20.0.0 @@ -257,7 +254,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.2 - version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) tsup: specifier: ^8.0.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) @@ -274,11 +271,11 @@ importers: packages/tools: dependencies: '@openai/agents': - specifier: ^0.1.0 - version: 0.1.0(ws@8.18.3)(zod@3.25.76) + specifier: ^0.11.5 + version: 0.11.5(ws@8.18.3)(zod@4.4.3) ai: - specifier: ^5.0.5 - version: 5.0.5(zod@3.25.76) + specifier: ^6.0.191 + version: 6.0.191(zod@4.4.3) fmp-node-api: specifier: workspace:* version: link:../api @@ -309,7 +306,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.0 - version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3) tsup: specifier: ^8.0.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) @@ -320,8 +317,8 @@ importers: specifier: ^5.3.3 version: 5.8.3 zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.0.0 + version: 4.4.3 packages/types: dependencies: @@ -347,37 +344,33 @@ importers: packages: - '@ai-sdk/gateway@1.0.3': - resolution: {integrity: sha512-QRGz2vH1WR9NvCv8gWocoebAKiXcuqj22mug6i8COeVsp33x5K5cK2DT4TwiQx5SfYbqJbVoBT+UqnHF7A3PHA==} + '@ai-sdk/gateway@3.0.120': + resolution: {integrity: sha512-MYKAeD2q7/sa1ZdqtL2tw0Me0B8Tok6Q/fhkJDhJl39dG8u+VBlWO9yk9lcdm784bM418o1EKObo4aOxs6+18Q==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4 + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai@2.0.4': - resolution: {integrity: sha512-PnVUFosX2zWk6emiX4SulZrGvGw79YJjh/DgxahpAcBpz71GlQyzVNTFAQx7ycFGLBg/YniJr1M2WJybYZZ3ug==} + '@ai-sdk/openai@3.0.65': + resolution: {integrity: sha512-ZlVoWH+zrdiYDiUt6n/xvfCsk33mzsB81TUQkBRVx79rxU1FKZqVH9J/QCtEpSLqx0cUzjvtIw9l9p7EbUv+dw==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4 + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@3.0.1': - resolution: {integrity: sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g==} + '@ai-sdk/provider-utils@4.0.27': + resolution: {integrity: sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4 + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider@2.0.0': - resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + '@ai-sdk/provider@3.0.10': + resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} engines: {node: '>=18'} - '@ai-sdk/react@2.0.6': - resolution: {integrity: sha512-7JbOxUbebjFVbaSO61Z+SvY44hLohWNSn0ZsZyvc/SGTBDtqTwRTQYXe4lMfboPDzVbtFBUVMgEbeeqYvBdoqg==} + '@ai-sdk/react@3.0.193': + resolution: {integrity: sha512-El0jUZ/B7mvBHAD5rfSDqOAhWxutVTq7BCNhfGuwfDPT9SO0TMHybh2bMkieJQI7YOfl+qNBoWrRAOHHaFb99Q==} engines: {node: '>=18'} peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.25.76 || ^4 - peerDependenciesMeta: - zod: - optional: true + react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} @@ -818,6 +811,12 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1086,9 +1085,15 @@ packages: '@types/react': '>=16' react: '>=16' - '@modelcontextprotocol/sdk@1.17.4': - resolution: {integrity: sha512-zq24hfuAmmlNZvik0FLI58uE5sriN0WWsQzIlYnzSuKDAHFqJtBFrl/LfB1NLgJT5Y7dEBzaX4yAKqOPrcetaw==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} @@ -1232,28 +1237,28 @@ packages: resolution: {integrity: sha512-URwiQ5ALx/sJ2iH4vzXEd+H4K6NAI7LRs6Jag3hrgKEpGmaE6alfRC8qjO4GIgb6A3ACaJumqP9twi/M9ywdHQ==} engines: {node: '>=18.0.0'} - '@openai/agents-core@0.1.0': - resolution: {integrity: sha512-SASFdtW71/3Fmjl1gSCIIDTqeDkRQxU7H8SqpMFeB+lbXtnNFTxR5Wt6XnEdj++dRRY8x3EbRnAx8lT7CZGioA==} + '@openai/agents-core@0.11.5': + resolution: {integrity: sha512-djqc3VUMw6wZlGOHa/+1jqIcEJnzI6PBNKPuf6tXO1MoE+o4NuTYgMpZ+pDUDvV6cor8+dEdmF5bsuqhb2dOMw==} peerDependencies: - zod: ^3.25.40 + zod: ^4.0.0 peerDependenciesMeta: zod: optional: true - '@openai/agents-openai@0.1.0': - resolution: {integrity: sha512-EdubPzCx4wj4YS07gX0mnpt1mHvDZXGfjDz+hFMOVbQHczIcLpv5gubRiMgFfALjhnCWVpOkeLCY/ikTY7YR0w==} + '@openai/agents-openai@0.11.5': + resolution: {integrity: sha512-gd/UiHtjMiqQFgzrqSS2wiBIlvqzIsf8/kSms1iRM0x2jO4eaVbK8txX1APT/om5AhZfduLSCzBsRbEur7MvNQ==} peerDependencies: - zod: ^3.25.40 + zod: ^4.0.0 - '@openai/agents-realtime@0.1.0': - resolution: {integrity: sha512-KCdAosaG3vy5WfZigiShsiV1HhqUuFc27BqYHaY5NkBecqvg8TnZMdgvXyxj/xVmw5csw43EXCc9t85Yugeatg==} + '@openai/agents-realtime@0.11.5': + resolution: {integrity: sha512-TEoxtzOv/+Pk0rNvypubyRmVajpebX3DkfdXp++uEH02MZhWQTmUI9khn+vsBvzTbdBo2Ery+FwdeJeVPjiVRA==} peerDependencies: - zod: ^3.25.40 + zod: ^4.0.0 - '@openai/agents@0.1.0': - resolution: {integrity: sha512-SHuJOKvBkLi64+LaZ7JZibkKjtruZkVVKamZudwFZQ5Iw7yvR7XjRnAg8CTbZ9OePjoq3sfa/PnA4V7EjaZo6A==} + '@openai/agents@0.11.5': + resolution: {integrity: sha512-tS9U5RKlSm/ZL/P0wyo7Ta6mtr8CXrQBmjOfj3rI/NttIm/W+uAFZXOQdvCh2y8lxezPmSfoR8mJG6IDX+XCWA==} peerDependencies: - zod: ^3.25.40 + zod: ^4.0.0 '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} @@ -1396,8 +1401,8 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -1553,15 +1558,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node-fetch@2.6.13': - resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} - '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@18.19.122': - resolution: {integrity: sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA==} - '@types/node@20.19.1': resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==} @@ -1765,9 +1764,9 @@ packages: cpu: [x64] os: [win32] - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -1783,25 +1782,26 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agentkeepalive@4.6.0: - resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} - engines: {node: '>= 8.0.0'} - - ai@5.0.5: - resolution: {integrity: sha512-NPQ8Yv4lR7o/1rM+HQ0JT18zKxVFy/DrFDpvxlBQqmemSwsf3FlLNTK0asWXo9YtDAc0BOFL/y4kB56Hffi4yg==} + ai@6.0.191: + resolution: {integrity: sha512-zAxvjKebQE7YkSyyNIl0OM7i6/zygnKeF+yNUjD4nWOelYrG+LpDd6RnH6mjySI4zUpZ7o4wbnmAy8jc6u98vQ==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4 + zod: ^3.25.76 || ^4.1.8 - ai@5.0.6: - resolution: {integrity: sha512-h643LlDEoUImDuu0v9oN52dHG5ck9sigVk/gjhYF+2x+TJtzTrMwQEUBKUOQjQ+Sb1ZFRKA3IxEYm5nUig0KzQ==} - engines: {node: '>=18'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: - zod: ^3.25.76 || ^4 + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1984,8 +1984,8 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} brace-expansion@1.1.12: @@ -2646,16 +2646,12 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - eventsource-parser@3.0.3: - resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} - engines: {node: '>=20.0.0'} + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} @@ -2673,14 +2669,14 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} extend@3.0.2: @@ -2710,6 +2706,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -2794,25 +2793,14 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data-encoder@1.7.2: - resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} - form-data@4.0.3: resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} engines: {node: '>= 6'} - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} - engines: {node: '>= 6'} - format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} - formdata-node@4.4.1: - resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} - engines: {node: '>= 12.20'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2979,11 +2967,15 @@ packages: highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + hono@4.12.23: + resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} + engines: {node: '>=16.9.0'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} human-id@4.1.1: @@ -2994,15 +2986,12 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} ignore@5.3.2: @@ -3044,6 +3033,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -3412,6 +3405,9 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -3441,6 +3437,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -3938,20 +3940,6 @@ packages: sass: optional: true - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -4021,24 +4009,12 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - openai@4.104.0: - resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} - hasBin: true - peerDependencies: - ws: ^8.18.0 - zod: ^3.23.8 - peerDependenciesMeta: - ws: - optional: true - zod: - optional: true - - openai@5.19.1: - resolution: {integrity: sha512-zSqnUF7oR9ksmpusKkpUgkNrj8Sl57U+OyzO8jzc7LUjTMg4DRfR3uCm+EIMA6iw06sRPNp4t7ojp3sCpEUZRQ==} + openai@6.39.0: + resolution: {integrity: sha512-O61LIsimY3acVabwvomwFhwrnN36yvHY2quIfy9keEcFytGgWeV35yLHQ6NVMLSBxRpHmcg2yuhCnlu2HT4pLQ==} hasBin: true peerDependencies: ws: ^8.18.0 - zod: ^3.23.8 + zod: ^3.25 || ^4.0 peerDependenciesMeta: ws: optional: true @@ -4305,8 +4281,8 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} quansync@0.2.10: @@ -4319,15 +4295,20 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: react: ^19.1.0 + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4343,6 +4324,10 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -4403,6 +4388,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -4484,6 +4473,9 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4616,10 +4608,6 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -4877,9 +4865,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -5061,9 +5046,6 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -5136,19 +5118,9 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - web-streams-polyfill@4.0.0-beta.3: - resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} - engines: {node: '>= 14'} - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -5247,10 +5219,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod-to-json-schema@3.24.5: - resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: - zod: ^3.24.1 + zod: ^3.25.28 || ^4 zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -5263,39 +5235,39 @@ packages: snapshots: - '@ai-sdk/gateway@1.0.3(zod@3.25.76)': + '@ai-sdk/gateway@3.0.120(zod@4.4.3)': dependencies: - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.1(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + '@vercel/oidc': 3.2.0 + zod: 4.4.3 - '@ai-sdk/openai@2.0.4(zod@3.25.76)': + '@ai-sdk/openai@3.0.65(zod@4.4.3)': dependencies: - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.1(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + zod: 4.4.3 - '@ai-sdk/provider-utils@3.0.1(zod@3.25.76)': + '@ai-sdk/provider-utils@4.0.27(zod@4.4.3)': dependencies: - '@ai-sdk/provider': 2.0.0 - '@standard-schema/spec': 1.0.0 - eventsource-parser: 3.0.3 - zod: 3.25.76 - zod-to-json-schema: 3.24.5(zod@3.25.76) + '@ai-sdk/provider': 3.0.10 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.8 + zod: 4.4.3 - '@ai-sdk/provider@2.0.0': + '@ai-sdk/provider@3.0.10': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@2.0.6(react@19.1.0)(zod@3.25.76)': + '@ai-sdk/react@3.0.193(react@19.2.6)(zod@4.4.3)': dependencies: - '@ai-sdk/provider-utils': 3.0.1(zod@3.25.76) - ai: 5.0.6(zod@3.25.76) - react: 19.1.0 - swr: 2.3.3(react@19.1.0) + '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + ai: 6.0.191(zod@4.4.3) + react: 19.2.6 + swr: 2.3.3(react@19.2.6) throttleit: 2.1.0 - optionalDependencies: - zod: 3.25.76 + transitivePeerDependencies: + - zod '@alloc/quick-lru@5.2.0': {} @@ -5481,7 +5453,7 @@ snapshots: '@babel/parser': 7.27.7 '@babel/template': 7.27.2 '@babel/types': 7.27.7 - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5802,6 +5774,11 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@hono/node-server@1.19.14(hono@4.12.23)': + dependencies: + hono: 4.12.23 + optional: true + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -6168,20 +6145,25 @@ snapshots: '@types/react': 19.1.8 react: 19.1.0 - '@modelcontextprotocol/sdk@1.17.4': + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: - ajv: 6.12.6 + '@hono/node-server': 1.19.14(hono@4.12.23) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 cors: 2.8.5 cross-spawn: 7.0.6 eventsource: 3.0.7 - eventsource-parser: 3.0.3 - express: 5.1.0 - express-rate-limit: 7.5.1(express@5.1.0) + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.23 + jose: 6.2.3 + json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 - raw-body: 3.0.0 - zod: 3.25.76 - zod-to-json-schema: 3.24.5(zod@3.25.76) + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) transitivePeerDependencies: - supports-color optional: true @@ -6295,48 +6277,52 @@ snapshots: wordwrap: 1.0.0 wrap-ansi: 7.0.0 - '@openai/agents-core@0.1.0(ws@8.18.3)(zod@3.25.76)': + '@openai/agents-core@0.11.5(ws@8.18.3)(zod@4.4.3)': dependencies: - debug: 4.4.1 - openai: 5.19.1(ws@8.18.3)(zod@3.25.76) + debug: 4.4.3(supports-color@8.1.1) + openai: 6.39.0(ws@8.18.3)(zod@4.4.3) optionalDependencies: - '@modelcontextprotocol/sdk': 1.17.4 - zod: 3.25.76 + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + zod: 4.4.3 transitivePeerDependencies: + - '@cfworker/json-schema' - supports-color - ws - '@openai/agents-openai@0.1.0(ws@8.18.3)(zod@3.25.76)': + '@openai/agents-openai@0.11.5(ws@8.18.3)(zod@4.4.3)': dependencies: - '@openai/agents-core': 0.1.0(ws@8.18.3)(zod@3.25.76) - debug: 4.4.1 - openai: 5.19.1(ws@8.18.3)(zod@3.25.76) - zod: 3.25.76 + '@openai/agents-core': 0.11.5(ws@8.18.3)(zod@4.4.3) + debug: 4.4.3(supports-color@8.1.1) + openai: 6.39.0(ws@8.18.3)(zod@4.4.3) + zod: 4.4.3 transitivePeerDependencies: + - '@cfworker/json-schema' - supports-color - ws - '@openai/agents-realtime@0.1.0(zod@3.25.76)': + '@openai/agents-realtime@0.11.5(zod@4.4.3)': dependencies: - '@openai/agents-core': 0.1.0(ws@8.18.3)(zod@3.25.76) + '@openai/agents-core': 0.11.5(ws@8.18.3)(zod@4.4.3) '@types/ws': 8.18.1 - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) ws: 8.18.3 - zod: 3.25.76 + zod: 4.4.3 transitivePeerDependencies: + - '@cfworker/json-schema' - bufferutil - supports-color - utf-8-validate - '@openai/agents@0.1.0(ws@8.18.3)(zod@3.25.76)': + '@openai/agents@0.11.5(ws@8.18.3)(zod@4.4.3)': dependencies: - '@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.19.1(ws@8.18.3)(zod@3.25.76) - zod: 3.25.76 + '@openai/agents-core': 0.11.5(ws@8.18.3)(zod@4.4.3) + '@openai/agents-openai': 0.11.5(ws@8.18.3)(zod@4.4.3) + '@openai/agents-realtime': 0.11.5(zod@4.4.3) + debug: 4.4.3(supports-color@8.1.1) + openai: 6.39.0(ws@8.18.3)(zod@4.4.3) + zod: 4.4.3 transitivePeerDependencies: + - '@cfworker/json-schema' - bufferutil - supports-color - utf-8-validate @@ -6434,7 +6420,7 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} '@swc/counter@0.1.3': {} @@ -6589,17 +6575,8 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node-fetch@2.6.13': - dependencies: - '@types/node': 20.19.2 - form-data: 4.0.4 - '@types/node@12.20.55': {} - '@types/node@18.19.122': - dependencies: - undici-types: 5.26.5 - '@types/node@20.19.1': dependencies: undici-types: 6.21.0 @@ -6855,9 +6832,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.9.2': optional: true - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 + '@vercel/oidc@3.2.0': {} accepts@2.0.0: dependencies: @@ -6871,25 +6846,18 @@ snapshots: acorn@8.15.0: {} - agentkeepalive@4.6.0: - dependencies: - humanize-ms: 1.2.1 - - ai@5.0.5(zod@3.25.76): + ai@6.0.191(zod@4.4.3): dependencies: - '@ai-sdk/gateway': 1.0.3(zod@3.25.76) - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.1(zod@3.25.76) + '@ai-sdk/gateway': 3.0.120(zod@4.4.3) + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) '@opentelemetry/api': 1.9.0 - zod: 3.25.76 + zod: 4.4.3 - ai@5.0.6(zod@3.25.76): - dependencies: - '@ai-sdk/gateway': 1.0.3(zod@3.25.76) - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.1(zod@3.25.76) - '@opentelemetry/api': 1.9.0 - zod: 3.25.76 + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + optional: true ajv@6.12.6: dependencies: @@ -6898,6 +6866,14 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + optional: true + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -7115,16 +7091,16 @@ snapshots: binary-extensions@2.3.0: {} - body-parser@2.2.0: + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1 - http-errors: 2.0.0 - iconv-lite: 0.6.3 + debug: 4.4.3(supports-color@8.1.1) + http-errors: 2.0.1 + iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.0 + qs: 6.15.2 + raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: - supports-color @@ -8102,15 +8078,13 @@ snapshots: etag@1.8.1: optional: true - event-target-shim@5.0.1: {} - eventemitter3@5.0.4: {} - eventsource-parser@3.0.3: {} + eventsource-parser@3.0.8: {} eventsource@3.0.7: dependencies: - eventsource-parser: 3.0.3 + eventsource-parser: 3.0.8 optional: true execa@5.1.1: @@ -8135,33 +8109,35 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - express-rate-limit@7.5.1(express@5.1.0): + express-rate-limit@8.5.2(express@5.2.1): dependencies: - express: 5.1.0 + express: 5.2.1 + ip-address: 10.2.0 optional: true - express@5.1.0: + express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.0 + body-parser: 2.2.2 content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 finalhandler: 2.1.0 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 2.0.0 mime-types: 3.0.1 on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.15.2 range-parser: 1.2.1 router: 2.2.0 send: 1.2.0 @@ -8205,6 +8181,9 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.2: + optional: true + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -8243,7 +8222,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -8293,8 +8272,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data-encoder@1.7.2: {} - form-data@4.0.3: dependencies: asynckit: 0.4.0 @@ -8303,21 +8280,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - form-data@4.0.4: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - format@0.2.2: {} - formdata-node@4.4.1: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 4.0.0-beta.3 - forwarded@0.2.0: optional: true @@ -8529,14 +8493,17 @@ snapshots: highlightjs-vue@1.0.0: {} + hono@4.12.23: + optional: true + html-escaper@2.0.2: {} - http-errors@2.0.0: + http-errors@2.0.1: dependencies: depd: 2.0.0 inherits: 2.0.4 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 toidentifier: 1.0.1 optional: true @@ -8544,15 +8511,11 @@ snapshots: human-signals@2.1.0: {} - humanize-ms@1.2.1: - dependencies: - ms: 2.1.3 - iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.6.3: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 optional: true @@ -8590,6 +8553,9 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ip-address@10.2.0: + optional: true + ipaddr.js@1.9.1: optional: true @@ -9239,6 +9205,9 @@ snapshots: jiti@2.4.2: {} + jose@6.2.3: + optional: true + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -9260,6 +9229,12 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: + optional: true + + json-schema-typed@8.0.2: + optional: true + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -9968,6 +9943,32 @@ snapshots: - '@babel/core' - babel-plugin-macros + next@15.3.0(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@next/env': 15.3.0 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001726 + postcss: 8.4.31 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + styled-jsx: 5.1.6(@babel/core@7.27.7)(react@19.2.6) + optionalDependencies: + '@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.34.2 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + next@15.3.4(@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.3.4 @@ -9994,12 +9995,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - node-domexception@1.0.0: {} - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - node-int64@0.4.0: {} node-releases@2.0.19: {} @@ -10073,25 +10068,10 @@ snapshots: dependencies: mimic-function: 5.0.1 - openai@4.104.0(ws@8.18.3)(zod@3.25.76): - dependencies: - '@types/node': 18.19.122 - '@types/node-fetch': 2.6.13 - abort-controller: 3.0.0 - agentkeepalive: 4.6.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - optionalDependencies: - ws: 8.18.3 - zod: 3.25.76 - transitivePeerDependencies: - - encoding - - openai@5.19.1(ws@8.18.3)(zod@3.25.76): + openai@6.39.0(ws@8.18.3)(zod@4.4.3): optionalDependencies: ws: 8.18.3 - zod: 3.25.76 + zod: 4.4.3 optionator@0.9.4: dependencies: @@ -10327,7 +10307,7 @@ snapshots: pure-rand@6.1.0: {} - qs@6.14.0: + qs@6.15.2: dependencies: side-channel: 1.1.0 optional: true @@ -10339,11 +10319,11 @@ snapshots: range-parser@1.2.1: optional: true - raw-body@3.0.0: + raw-body@3.0.2: dependencies: bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 unpipe: 1.0.0 optional: true @@ -10352,6 +10332,11 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + react-is@16.13.1: {} react-is@18.3.1: {} @@ -10368,6 +10353,8 @@ snapshots: react@19.1.0: {} + react@19.2.6: {} + read-cache@1.0.0: dependencies: pify: 2.3.0 @@ -10492,6 +10479,9 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: + optional: true + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -10561,7 +10551,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -10600,6 +10590,8 @@ snapshots: scheduler@0.26.0: {} + scheduler@0.27.0: {} + semver@6.3.1: {} semver@7.7.2: {} @@ -10608,12 +10600,12 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 mime-types: 3.0.1 ms: 2.1.3 on-finished: 2.4.1 @@ -10778,9 +10770,6 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 - statuses@2.0.1: - optional: true - statuses@2.0.2: optional: true @@ -10909,6 +10898,13 @@ snapshots: optionalDependencies: '@babel/core': 7.27.7 + styled-jsx@5.1.6(@babel/core@7.27.7)(react@19.2.6): + dependencies: + client-only: 0.0.1 + react: 19.2.6 + optionalDependencies: + '@babel/core': 7.27.7 + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -10929,11 +10925,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.3.3(react@19.1.0): + swr@2.3.3(react@19.2.6): dependencies: dequal: 2.0.3 - react: 19.1.0 - use-sync-external-store: 1.5.0(react@19.1.0) + react: 19.2.6 + use-sync-external-store: 1.5.0(react@19.2.6) tailwind-merge@3.3.1: {} @@ -11115,8 +11111,6 @@ snapshots: toidentifier@1.0.1: optional: true - tr46@0.0.3: {} - tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -11133,12 +11127,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.19.2) + jest: 29.7.0(@types/node@20.19.1) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -11154,12 +11148,12 @@ snapshots: esbuild: 0.25.5 jest-util: 29.7.0 - ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.19.1) + jest: 29.7.0(@types/node@20.19.2) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -11353,8 +11347,6 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@5.26.5: {} - undici-types@6.21.0: {} undici-types@7.8.0: {} @@ -11435,9 +11427,9 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.5.0(react@19.1.0): + use-sync-external-store@1.5.0(react@19.2.6): dependencies: - react: 19.1.0 + react: 19.2.6 util-deprecate@1.0.2: {} @@ -11464,17 +11456,8 @@ snapshots: dependencies: makeerror: 1.0.12 - web-streams-polyfill@4.0.0-beta.3: {} - - webidl-conversions@3.0.1: {} - webidl-conversions@4.0.2: {} - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 @@ -11585,9 +11568,10 @@ snapshots: yocto-queue@0.1.0: {} - zod-to-json-schema@3.24.5(zod@3.25.76): + zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: - zod: 3.25.76 + zod: 4.4.3 + optional: true zod@3.25.76: {} From e7042b4a69adaec62164b1418a561f88a4fb1bfd Mon Sep 17 00:00:00 2001 From: e-roy Date: Mon, 25 May 2026 15:56:54 -0400 Subject: [PATCH 04/13] working beta versions --- .changeset/loosen-tool-dep-ranges.md | 5 ++ .changeset/openai-bundler-safe.md | 5 ++ .changeset/pre.json | 17 ++++ .changeset/zod4-v6-beta.md | 11 +++ apps/examples/openai/package.json | 2 +- apps/examples/vercel-ai/package.json | 6 +- packages/api/CHANGELOG.md | 9 +++ packages/api/package.json | 2 +- packages/tools/CHANGELOG.md | 26 +++++++ packages/tools/README.md | 55 ++++++------- packages/tools/package.json | 16 +++- .../__tests__/providers/openai/index.test.ts | 5 -- .../src/__tests__/utils/version-check.test.ts | 31 -------- packages/tools/src/providers/openai/index.ts | 4 - packages/tools/src/utils/version-check.ts | 78 ------------------- packages/types/CHANGELOG.md | 9 +++ packages/types/package.json | 2 +- pnpm-lock.yaml | 56 ++++++------- 18 files changed, 158 insertions(+), 181 deletions(-) create mode 100644 .changeset/loosen-tool-dep-ranges.md create mode 100644 .changeset/openai-bundler-safe.md create mode 100644 .changeset/pre.json create mode 100644 .changeset/zod4-v6-beta.md delete mode 100644 packages/tools/src/__tests__/utils/version-check.test.ts delete mode 100644 packages/tools/src/utils/version-check.ts diff --git a/.changeset/loosen-tool-dep-ranges.md b/.changeset/loosen-tool-dep-ranges.md new file mode 100644 index 0000000..a3ad738 --- /dev/null +++ b/.changeset/loosen-tool-dep-ranges.md @@ -0,0 +1,5 @@ +--- +"fmp-ai-tools": patch +--- + +Loosen the `@openai/agents` (`^0.11.0`) and `ai` (`^6.0.0`) dependency ranges so installs resolve to settled, widely-available versions instead of being pinned to the newest release. This avoids install failures for consumers using pnpm's `minimum-release-age` supply-chain guard, which previously had no mature version to resolve. diff --git a/.changeset/openai-bundler-safe.md b/.changeset/openai-bundler-safe.md new file mode 100644 index 0000000..d2d0d10 --- /dev/null +++ b/.changeset/openai-bundler-safe.md @@ -0,0 +1,5 @@ +--- +"fmp-ai-tools": patch +--- + +Make the `fmp-ai-tools/openai` entry point bundler-safe so it works in Next.js (App Router / Turbopack) with a normal static import. Removed the filesystem-based `checkOpenAIAgentsVersion()` that ran on import (it used `require.resolve`/`fs` and broke under bundlers), and moved `@openai/agents` and `ai` to **optional** `peerDependencies`. Consumers no longer need `serverExternalPackages`, `createRequire`, or any other workaround. diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000..af9d5ee --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,17 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "fmp-docs": "0.0.0", + "fmp-ai-tools-openai-example": "0.1.0", + "fmp-ai-tools-vercel-ai-example": "0.1.0", + "fmp-node-api": "0.1.9", + "fmp-ai-tools": "0.1.0", + "fmp-node-types": "0.1.4" + }, + "changesets": [ + "loosen-tool-dep-ranges", + "openai-bundler-safe", + "zod4-v6-beta" + ] +} diff --git a/.changeset/zod4-v6-beta.md b/.changeset/zod4-v6-beta.md new file mode 100644 index 0000000..ac858e8 --- /dev/null +++ b/.changeset/zod4-v6-beta.md @@ -0,0 +1,11 @@ +--- +"fmp-node-types": minor +"fmp-node-api": minor +"fmp-ai-tools": minor +--- + +Schema-first types and updated AI SDKs. + +- **fmp-node-types**: now ships Zod schemas as the source of truth, with TypeScript types derived via `z.infer`. +- **fmp-node-api**: consumes the schema-first types; response types corrected against the live FMP API (e.g. `getIntraday` → `IntradayPrice[]`, `getMarketPerformance`/`getMarketIndex` → quote-shaped `MarketIndex[]`, plus nullability fixes). Adds an internal live-API shape-check tool. +- **fmp-ai-tools**: updated to Vercel AI SDK v6 (`ai@6`, `@ai-sdk/*@3`), `@openai/agents@0.11.5`, and Zod 4. The OpenAI tool wrapper now passes the Zod schema natively to `tool()`. diff --git a/apps/examples/openai/package.json b/apps/examples/openai/package.json index 095e943..0f26809 100644 --- a/apps/examples/openai/package.json +++ b/apps/examples/openai/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@openai/agents": "^0.11.5", + "@openai/agents": "^0.11.0", "fmp-ai-tools": "workspace:*", "next": "15.3.0", "react": "^19.0.0", diff --git a/apps/examples/vercel-ai/package.json b/apps/examples/vercel-ai/package.json index 8787998..6723699 100644 --- a/apps/examples/vercel-ai/package.json +++ b/apps/examples/vercel-ai/package.json @@ -12,9 +12,9 @@ "next": "15.3.0", "react": "^19.2.1", "react-dom": "^19.2.1", - "ai": "^6.0.191", - "@ai-sdk/openai": "^3.0.65", - "@ai-sdk/react": "^3.0.193", + "ai": "^6.0.0", + "@ai-sdk/openai": "^3.0.0", + "@ai-sdk/react": "^3.0.0", "fmp-ai-tools": "workspace:*", "zod": "^4.0.0" }, diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index f2ca419..713af47 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,14 @@ # fmp-node-api +## 0.2.0-beta.0 + +### Minor Changes + +- Schema-first types and updated AI SDKs. + - **fmp-node-types**: now ships Zod schemas as the source of truth, with TypeScript types derived via `z.infer`. + - **fmp-node-api**: consumes the schema-first types; response types corrected against the live FMP API (e.g. `getIntraday` → `IntradayPrice[]`, `getMarketPerformance`/`getMarketIndex` → quote-shaped `MarketIndex[]`, plus nullability fixes). Adds an internal live-API shape-check tool. + - **fmp-ai-tools**: updated to Vercel AI SDK v6 (`ai@6`, `@ai-sdk/*@3`), `@openai/agents@0.11.5`, and Zod 4. The OpenAI tool wrapper now passes the Zod schema natively to `tool()`. + ## 0.1.9 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index d0a67fb..5de4ba7 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-api", - "version": "0.1.9", + "version": "0.2.0-beta.0", "description": "A comprehensive Node.js wrapper for Financial Modeling Prep API", "disclaimer": "This package is not affiliated with, endorsed by, or sponsored by Financial Modeling Prep (FMP). This is an independent, community-developed wrapper.", "main": "./dist/index.js", diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index 038f866..ed8ef0a 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -1,5 +1,31 @@ # fmp-ai-tools +## 0.2.0-beta.2 + +### Patch Changes + +- Make the `fmp-ai-tools/openai` entry point bundler-safe so it works in Next.js (App Router / Turbopack) with a normal static import. Removed the filesystem-based `checkOpenAIAgentsVersion()` that ran on import (it used `require.resolve`/`fs` and broke under bundlers), and moved `@openai/agents` and `ai` to **optional** `peerDependencies`. Consumers no longer need `serverExternalPackages`, `createRequire`, or any other workaround. + +## 0.2.0-beta.1 + +### Patch Changes + +- Loosen the `@openai/agents` (`^0.11.0`) and `ai` (`^6.0.0`) dependency ranges so installs resolve to settled, widely-available versions instead of being pinned to the newest release. This avoids install failures for consumers using pnpm's `minimum-release-age` supply-chain guard, which previously had no mature version to resolve. + +## 0.2.0-beta.0 + +### Minor Changes + +- Schema-first types and updated AI SDKs. + - **fmp-node-types**: now ships Zod schemas as the source of truth, with TypeScript types derived via `z.infer`. + - **fmp-node-api**: consumes the schema-first types; response types corrected against the live FMP API (e.g. `getIntraday` → `IntradayPrice[]`, `getMarketPerformance`/`getMarketIndex` → quote-shaped `MarketIndex[]`, plus nullability fixes). Adds an internal live-API shape-check tool. + - **fmp-ai-tools**: updated to Vercel AI SDK v6 (`ai@6`, `@ai-sdk/*@3`), `@openai/agents@0.11.5`, and Zod 4. The OpenAI tool wrapper now passes the Zod schema natively to `tool()`. + +### Patch Changes + +- Updated dependencies + - fmp-node-api@0.2.0-beta.0 + ## 0.0.12 ### Patch Changes diff --git a/packages/tools/README.md b/packages/tools/README.md index 31ed473..c62b761 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -7,44 +7,39 @@ This package provides pre-built AI tools that can be used with various AI framew ## Installation ```bash -npm install fmp-ai-tools -# or -pnpm add fmp-ai-tools -# or -yarn add fmp-ai-tools +pnpm add fmp-ai-tools # or: npm install fmp-ai-tools / yarn add fmp-ai-tools ``` -### Peer Dependencies +`fmp-ai-tools` has two entry points. `ai` and `@openai/agents` are **optional peer dependencies**, so install only the SDK for the entry point you use, plus `zod`. -This package requires the following peer dependencies to be installed in your project: +### Vercel AI SDK — `fmp-ai-tools/vercel-ai` ```bash -# For Vercel AI SDK -npm install ai zod -# or pnpm add ai zod -# or -yarn add ai zod ``` -**Required versions:** +- `ai`: `>=6.0.0` +- `zod`: `^4.0.0` -- `ai`: ^5.0.0 -- `zod`: ^3.25.76 +### OpenAI Agents — `fmp-ai-tools/openai` -**⚠️ 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 +```bash +pnpm add @openai/agents zod +``` -### OpenAI Agents Compatibility +- `@openai/agents`: `>=0.11.0` +- `zod`: `^4.0.0` -**⚠️ Important**: This package requires `@openai/agents` version `^0.1.0` or higher due to breaking changes in the API. Older versions are not supported and will fail when the tools are imported. +Both entry points are imported with a normal static `import` — no `serverExternalPackages`, `createRequire`, or other bundler workarounds are needed (Next.js App Router and Turbopack included). ## Quick Start ### Vercel AI SDK (Recommended) +`convertToModelMessages` is **async in `ai` v6** — be sure to `await` it. + ```typescript +// app/api/chat/route.ts import { openai } from '@ai-sdk/openai'; import { streamText, convertToModelMessages, stepCountIs } from 'ai'; import { fmpTools } from 'fmp-ai-tools/vercel-ai'; @@ -54,9 +49,12 @@ export async function POST(req: Request) { const result = streamText({ model: openai('gpt-4o-mini'), - messages: convertToModelMessages(messages), + messages: await convertToModelMessages(messages), tools: fmpTools, stopWhen: stepCountIs(5), + // FMP tools have optional params; relax strict JSON schema so the model + // can omit them (@ai-sdk/openai v3 defaults strictJsonSchema: true). + providerOptions: { openai: { strictJsonSchema: false } }, }); return result.toUIMessageStreamResponse(); @@ -66,19 +64,24 @@ export async function POST(req: Request) { ### OpenAI Agents ```typescript +// app/api/chat/route.ts import { Agent, run } from '@openai/agents'; import { fmpTools } from 'fmp-ai-tools/openai'; const agent = new Agent({ name: 'Financial Analyst', + model: 'gpt-4o-mini', instructions: 'You are a financial analyst with access to real-time market data.', tools: fmpTools, }); -const result = await run( - agent, - 'Get the current stock quote for Apple (AAPL) and show me their latest balance sheet', -); +export async function POST(req: Request) { + const { message } = await req.json(); + + const result = await run(agent, message); + + return Response.json({ output: result.finalOutput }); +} ``` ## Configuration @@ -222,7 +225,7 @@ const selectedTools = { // Use with Vercel AI SDK const result = streamText({ model: openai('gpt-4o-mini'), - messages: convertToModelMessages(messages), + messages: await convertToModelMessages(messages), tools: selectedTools, }); ``` diff --git a/packages/tools/package.json b/packages/tools/package.json index a2b9b84..049a20a 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.12", + "version": "0.2.0-beta.2", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { @@ -52,14 +52,24 @@ }, "homepage": "https://fmp-node-wrapper-docs.vercel.app", "dependencies": { - "@openai/agents": "^0.11.5", - "ai": "^6.0.191", "fmp-node-api": "workspace:*" }, "peerDependencies": { + "@openai/agents": ">=0.11.0", + "ai": ">=6.0.0", "zod": "^4.0.0" }, + "peerDependenciesMeta": { + "@openai/agents": { + "optional": true + }, + "ai": { + "optional": true + } + }, "devDependencies": { + "@openai/agents": "^0.11.0", + "ai": "^6.0.0", "zod": "^4.0.0", "@types/jest": "^29.5.0", "@types/node": "^20.11.0", diff --git a/packages/tools/src/__tests__/providers/openai/index.test.ts b/packages/tools/src/__tests__/providers/openai/index.test.ts index 20ffda5..a0d311a 100644 --- a/packages/tools/src/__tests__/providers/openai/index.test.ts +++ b/packages/tools/src/__tests__/providers/openai/index.test.ts @@ -1,10 +1,5 @@ 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', () => { diff --git a/packages/tools/src/__tests__/utils/version-check.test.ts b/packages/tools/src/__tests__/utils/version-check.test.ts deleted file mode 100644 index e7bb465..0000000 --- a/packages/tools/src/__tests__/utils/version-check.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Mock console.warn to capture output -const mockConsoleWarn = jest.fn(); -const originalConsoleWarn = console.warn; - -describe('Version Check Utility', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockConsoleWarn.mockClear(); - console.warn = mockConsoleWarn; - }); - - afterAll(() => { - console.warn = originalConsoleWarn; - }); - - describe('error messages', () => { - it('should provide helpful error messages', async () => { - const { checkOpenAIAgentsVersion } = await import('../../utils/version-check'); - - const originalResolve = require.resolve; - require.resolve = jest.fn().mockImplementation(() => { - throw new Error('Cannot resolve module'); - }) as unknown as typeof require.resolve; - - expect(() => checkOpenAIAgentsVersion()).toThrow('npm install @openai/agents'); - expect(() => checkOpenAIAgentsVersion()).toThrow('@openai/agents package not found'); - - require.resolve = originalResolve; - }); - }); -}); diff --git a/packages/tools/src/providers/openai/index.ts b/packages/tools/src/providers/openai/index.ts index 191246e..74a7bd4 100644 --- a/packages/tools/src/providers/openai/index.ts +++ b/packages/tools/src/providers/openai/index.ts @@ -1,5 +1,4 @@ import type { Tool } from '@openai/agents'; -import { checkOpenAIAgentsVersion } from '@/utils/version-check'; import { getCompanyProfile, getCompanySharesFloat, @@ -164,6 +163,3 @@ export const fmpTools: Tool[] = [ getStockSplits, getDividendHistory, ]; - -// Check version compatibility when the module is imported -checkOpenAIAgentsVersion(); diff --git a/packages/tools/src/utils/version-check.ts b/packages/tools/src/utils/version-check.ts deleted file mode 100644 index 4a48712..0000000 --- a/packages/tools/src/utils/version-check.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -const REQUIRED_VERSION = '0.11.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 { - const installedVersion = getInstalledPackageVersion('@openai/agents'); - - if (!installedVersion) { - throw new Error( - `@openai/agents package not found. ` + - `This package requires @openai/agents to be installed. ` + - `Please install with: npm install @openai/agents`, - ); - } - - 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 146c55f..cd0276b 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,14 @@ # fmp-node-types +## 0.2.0-beta.0 + +### Minor Changes + +- Schema-first types and updated AI SDKs. + - **fmp-node-types**: now ships Zod schemas as the source of truth, with TypeScript types derived via `z.infer`. + - **fmp-node-api**: consumes the schema-first types; response types corrected against the live FMP API (e.g. `getIntraday` → `IntradayPrice[]`, `getMarketPerformance`/`getMarketIndex` → quote-shaped `MarketIndex[]`, plus nullability fixes). Adds an internal live-API shape-check tool. + - **fmp-ai-tools**: updated to Vercel AI SDK v6 (`ai@6`, `@ai-sdk/*@3`), `@openai/agents@0.11.5`, and Zod 4. The OpenAI tool wrapper now passes the Zod schema natively to `tool()`. + ## 0.1.4 ### Patch Changes diff --git a/packages/types/package.json b/packages/types/package.json index 38e530e..302211e 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-types", - "version": "0.1.4", + "version": "0.2.0-beta.0", "description": "Shared TypeScript types for FMP Node Wrapper ecosystem (internal package)", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d92f930..0c33ef5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,7 +133,7 @@ importers: apps/examples/openai: dependencies: '@openai/agents': - specifier: ^0.11.5 + specifier: ^0.11.0 version: 0.11.5(ws@8.18.3)(zod@4.4.3) fmp-ai-tools: specifier: workspace:* @@ -182,13 +182,13 @@ importers: apps/examples/vercel-ai: dependencies: '@ai-sdk/openai': - specifier: ^3.0.65 + specifier: ^3.0.0 version: 3.0.65(zod@4.4.3) '@ai-sdk/react': - specifier: ^3.0.193 + specifier: ^3.0.0 version: 3.0.193(react@19.2.6)(zod@4.4.3) ai: - specifier: ^6.0.191 + specifier: ^6.0.0 version: 6.0.191(zod@4.4.3) fmp-ai-tools: specifier: workspace:* @@ -254,7 +254,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.2 - version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) tsup: specifier: ^8.0.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) @@ -270,16 +270,13 @@ importers: packages/tools: dependencies: - '@openai/agents': - specifier: ^0.11.5 - version: 0.11.5(ws@8.18.3)(zod@4.4.3) - ai: - specifier: ^6.0.191 - version: 6.0.191(zod@4.4.3) fmp-node-api: specifier: workspace:* version: link:../api devDependencies: + '@openai/agents': + specifier: ^0.11.0 + version: 0.11.5(ws@8.18.3)(zod@4.4.3) '@types/jest': specifier: ^29.5.0 version: 29.5.14 @@ -292,6 +289,9 @@ importers: '@typescript-eslint/parser': specifier: ^8.0.0 version: 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) + ai: + specifier: ^6.0.0 + version: 6.0.191(zod@4.4.3) dotenv: specifier: ^16.5.0 version: 16.5.0 @@ -306,7 +306,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.0 - version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3) tsup: specifier: ^8.0.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) @@ -5297,7 +5297,7 @@ snapshots: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6681,7 +6681,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.39.0(typescript@5.8.3) '@typescript-eslint/types': 8.39.0 - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -7697,8 +7697,8 @@ snapshots: '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.32.0(jiti@2.4.2)) @@ -7732,7 +7732,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -7743,7 +7743,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.9.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -7758,14 +7758,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -7798,7 +7798,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7809,7 +7809,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -8768,7 +8768,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -9818,7 +9818,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -11127,12 +11127,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.19.1) + jest: 29.7.0(@types/node@20.19.2) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -11148,12 +11148,12 @@ snapshots: esbuild: 0.25.5 jest-util: 29.7.0 - ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.19.2) + jest: 29.7.0(@types/node@20.19.1) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 From 005a6e9d1513d49ff299b5abb7e618d2238c4e09 Mon Sep 17 00:00:00 2001 From: e-roy Date: Mon, 25 May 2026 21:21:58 -0400 Subject: [PATCH 05/13] feat: typed FMP error classification surfaced through AI tools Add APIResponse.errorType (plan-restricted | rate-limit | auth | not-found | bad-request | network | unknown). The API client now reads FMP's real error message from the response body and classifies failures via a shared classifyError(); the live-check tool reuses the same classifier. AI tools route every result through toToolResponse() and catch thrown errors via toToolError(), so failures reach the model as a structured { error, type, message, status } object instead of null or a raw throw (e.g. a missing FMP_API_KEY now reports type: auth). Versions: fmp-node-types/fmp-node-api 0.2.0-beta.1, fmp-ai-tools 0.2.0-beta.4. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/error-handling.md | 11 +++ .changeset/pre.json | 2 + .changeset/tools-catch-thrown-errors.md | 5 ++ packages/api/CHANGELOG.md | 9 +++ packages/api/package.json | 2 +- packages/api/src/__tests__/client.test.ts | 45 ++++++++++-- .../__tests__/utils/error-classifier.test.ts | 48 +++++++++++++ packages/api/src/client.ts | 44 ++++++++---- packages/api/src/index.ts | 5 ++ packages/api/src/live/validate.ts | 30 +++----- packages/api/src/utils/error-classifier.ts | 61 ++++++++++++++++ packages/tools/CHANGELOG.md | 20 ++++++ packages/tools/README.md | 23 ++++-- packages/tools/package.json | 2 +- .../__tests__/providers/openai/quote.test.ts | 17 +++++ .../__tests__/utils/format-response.test.ts | 72 +++++++++++++++++++ .../utils/openai-tool-wrapper.test.ts | 18 +++++ .../tools/src/providers/openai/calendar.ts | 5 +- .../tools/src/providers/openai/company.ts | 7 +- .../tools/src/providers/openai/economic.ts | 5 +- packages/tools/src/providers/openai/etf.ts | 5 +- .../tools/src/providers/openai/financial.ts | 23 +++--- .../tools/src/providers/openai/insider.ts | 3 +- .../src/providers/openai/institutional.ts | 3 +- packages/tools/src/providers/openai/market.ts | 11 +-- packages/tools/src/providers/openai/quote.ts | 3 +- .../src/providers/openai/senate-house.ts | 13 ++-- packages/tools/src/providers/openai/stock.ts | 7 +- .../tools/src/providers/vercel-ai/calendar.ts | 5 +- .../tools/src/providers/vercel-ai/company.ts | 7 +- .../tools/src/providers/vercel-ai/economic.ts | 5 +- packages/tools/src/providers/vercel-ai/etf.ts | 5 +- .../src/providers/vercel-ai/financial.ts | 23 +++--- .../tools/src/providers/vercel-ai/insider.ts | 3 +- .../src/providers/vercel-ai/institutional.ts | 3 +- .../tools/src/providers/vercel-ai/market.ts | 11 +-- .../tools/src/providers/vercel-ai/quote.ts | 3 +- .../src/providers/vercel-ai/senate-house.ts | 13 ++-- .../tools/src/providers/vercel-ai/stock.ts | 7 +- .../tools/src/utils/aisdk-tool-wrapper.ts | 9 ++- packages/tools/src/utils/format-response.ts | 50 +++++++++++++ .../tools/src/utils/openai-tool-wrapper.ts | 11 ++- packages/types/CHANGELOG.md | 9 +++ packages/types/package.json | 2 +- packages/types/src/common.ts | 13 ++++ 45 files changed, 555 insertions(+), 123 deletions(-) create mode 100644 .changeset/error-handling.md create mode 100644 .changeset/tools-catch-thrown-errors.md create mode 100644 packages/api/src/__tests__/utils/error-classifier.test.ts create mode 100644 packages/api/src/utils/error-classifier.ts create mode 100644 packages/tools/src/__tests__/utils/format-response.test.ts create mode 100644 packages/tools/src/utils/format-response.ts diff --git a/.changeset/error-handling.md b/.changeset/error-handling.md new file mode 100644 index 0000000..934f7b6 --- /dev/null +++ b/.changeset/error-handling.md @@ -0,0 +1,11 @@ +--- +"fmp-node-types": minor +"fmp-node-api": minor +"fmp-ai-tools": minor +--- + +Typed error classification for FMP failures, surfaced through the AI tools. + +- **fmp-node-types**: `APIResponse` gains an optional `errorType` (`plan-restricted | rate-limit | auth | not-found | bad-request | network | unknown`). +- **fmp-node-api**: the client now reads FMP's real error message from the response body and classifies failures (new `classifyError` export). Plan/subscription-restricted endpoints (402/403 or "Exclusive/Special Endpoint") are reported as `plan-restricted` instead of a generic error. +- **fmp-ai-tools**: every tool now returns a structured error (`{ error, type, message, status }`) to the model on failure instead of `null`, so an agent can explain *why* a call failed — e.g. that the data requires a higher FMP plan. diff --git a/.changeset/pre.json b/.changeset/pre.json index af9d5ee..0f1e396 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -10,8 +10,10 @@ "fmp-node-types": "0.1.4" }, "changesets": [ + "error-handling", "loosen-tool-dep-ranges", "openai-bundler-safe", + "tools-catch-thrown-errors", "zod4-v6-beta" ] } diff --git a/.changeset/tools-catch-thrown-errors.md b/.changeset/tools-catch-thrown-errors.md new file mode 100644 index 0000000..3ba7692 --- /dev/null +++ b/.changeset/tools-catch-thrown-errors.md @@ -0,0 +1,5 @@ +--- +"fmp-ai-tools": patch +--- + +Tools no longer throw out of `execute`. A thrown error — most importantly a missing/invalid `FMP_API_KEY`, which throws from the `FMP` constructor before any request is made — is now caught at the tool boundary and returned to the model as the same structured `{ error, type, message, status }` shape (with `type: "auth"` for key problems). Previously the raw exception reached the AI SDK and the agent received only a vague failure with no reason. diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 713af47..8c1cab0 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,14 @@ # fmp-node-api +## 0.2.0-beta.1 + +### Minor Changes + +- Typed error classification for FMP failures, surfaced through the AI tools. + - **fmp-node-types**: `APIResponse` gains an optional `errorType` (`plan-restricted | rate-limit | auth | not-found | bad-request | network | unknown`). + - **fmp-node-api**: the client now reads FMP's real error message from the response body and classifies failures (new `classifyError` export). Plan/subscription-restricted endpoints (402/403 or "Exclusive/Special Endpoint") are reported as `plan-restricted` instead of a generic error. + - **fmp-ai-tools**: every tool now returns a structured error (`{ error, type, message, status }`) to the model on failure instead of `null`, so an agent can explain _why_ a call failed — e.g. that the data requires a higher FMP plan. + ## 0.2.0-beta.0 ### Minor Changes diff --git a/packages/api/package.json b/packages/api/package.json index 5de4ba7..ca7fa70 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-api", - "version": "0.2.0-beta.0", + "version": "0.2.0-beta.1", "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/src/__tests__/client.test.ts b/packages/api/src/__tests__/client.test.ts index 8d024dd..51f0d70 100644 --- a/packages/api/src/__tests__/client.test.ts +++ b/packages/api/src/__tests__/client.test.ts @@ -163,7 +163,7 @@ describe('FMPClient', () => { expect(result.data).toEqual([]); }); - it('should handle network errors', async () => { + it('should handle network errors (no HTTP response)', async () => { const networkError = new Error('Network Error'); mockAxiosInstance.get.mockRejectedValue(networkError); @@ -172,7 +172,8 @@ describe('FMPClient', () => { expect(result).toEqual({ success: false, data: null, - error: 'Network Error', + error: 'Network error: Network Error', + errorType: 'network', status: 500, }); }); @@ -187,11 +188,40 @@ describe('FMPClient', () => { expect(result).toEqual({ success: false, data: null, - error: 'HTTP Error', + error: 'Not found: HTTP Error', + errorType: 'not-found', status: 404, }); }); + it('should classify plan-restricted (403) errors using the FMP message body', async () => { + const planError: any = new Error('Request failed with status code 403'); + planError.response = { + status: 403, + data: { 'Error Message': 'Special Endpoint : This endpoint is only for premium users.' }, + }; + mockAxiosInstance.get.mockRejectedValue(planError); + + const result = await client.get('/test-endpoint'); + + expect(result.success).toBe(false); + expect(result.errorType).toBe('plan-restricted'); + expect(result.status).toBe(403); + // Surfaces FMP's real reason, not axios's generic message. + expect(result.error).toContain('Special Endpoint'); + }); + + it('should classify rate-limit (429) errors', async () => { + const rateError: any = new Error('Request failed with status code 429'); + rateError.response = { status: 429, data: 'Limit Reach . Please upgrade your plan.' }; + mockAxiosInstance.get.mockRejectedValue(rateError); + + const result = await client.get('/test-endpoint'); + + expect(result.errorType).toBe('rate-limit'); + expect(result.status).toBe(429); + }); + it('should handle errors without response object', async () => { const error = new Error('Unknown error'); mockAxiosInstance.get.mockRejectedValue(error); @@ -201,7 +231,8 @@ describe('FMPClient', () => { expect(result).toEqual({ success: false, data: null, - error: 'Unknown error', + error: 'Network error: Unknown error', + errorType: 'network', status: 500, }); }); @@ -215,7 +246,8 @@ describe('FMPClient', () => { expect(result).toEqual({ success: false, data: null, - error: 'Unknown error occurred', + error: 'Network error: Unknown error occurred', + errorType: 'network', status: 500, }); }); @@ -296,7 +328,8 @@ describe('FMPClient', () => { expect(result).toEqual({ success: false, data: null, - error: 'Network Error', + error: 'Network error: Network Error', + errorType: 'network', status: 500, }); }); diff --git a/packages/api/src/__tests__/utils/error-classifier.test.ts b/packages/api/src/__tests__/utils/error-classifier.test.ts new file mode 100644 index 0000000..0ca136b --- /dev/null +++ b/packages/api/src/__tests__/utils/error-classifier.test.ts @@ -0,0 +1,48 @@ +import { classifyError } from '../../utils/error-classifier'; + +describe('classifyError', () => { + it('classifies 401 as auth', () => { + const c = classifyError(401, 'Invalid API KEY'); + expect(c.errorType).toBe('auth'); + expect(c.message).toContain('FMP_API_KEY'); + }); + + it('classifies 402 and 403 as plan-restricted', () => { + expect(classifyError(402, 'nope').errorType).toBe('plan-restricted'); + expect(classifyError(403, 'nope').errorType).toBe('plan-restricted'); + }); + + it('classifies plan-locked message patterns regardless of status', () => { + expect(classifyError(200, 'Exclusive Endpoint: upgrade').errorType).toBe('plan-restricted'); + expect( + classifyError(200, 'Special Endpoint : This endpoint is only for premium users.').errorType, + ).toBe('plan-restricted'); + expect(classifyError(200, 'Please upgrade your plan').errorType).toBe('plan-restricted'); + }); + + it('classifies 429 as rate-limit', () => { + const c = classifyError(429, 'Limit Reach'); + expect(c.errorType).toBe('rate-limit'); + expect(c.message).toContain('rate limit'); + }); + + it('classifies 404 as not-found', () => { + expect(classifyError(404, 'nothing here').errorType).toBe('not-found'); + }); + + it('classifies 400 as bad-request', () => { + expect(classifyError(400, 'bad symbol').errorType).toBe('bad-request'); + }); + + it('classifies status 0 (no HTTP response) as network', () => { + expect(classifyError(0, 'ECONNREFUSED').errorType).toBe('network'); + }); + + it('classifies unmapped 5xx as unknown', () => { + expect(classifyError(500, 'boom').errorType).toBe('unknown'); + }); + + it('falls back to a default message when none is provided', () => { + expect(classifyError(500, '').message).toBe('Unknown error occurred'); + }); +}); diff --git a/packages/api/src/client.ts b/packages/api/src/client.ts index 80116b0..3254775 100644 --- a/packages/api/src/client.ts +++ b/packages/api/src/client.ts @@ -1,5 +1,6 @@ import axios, { AxiosInstance } from 'axios'; import { FMPConfig, APIResponse } from 'fmp-node-types'; +import { classifyError } from './utils/error-classifier'; /** * Utility function to unwrap single objects from arrays @@ -99,12 +100,7 @@ export class FMPClient { status: response.status, }; } catch (error: any) { - return { - success: false, - data: null, - error: error.message || 'Unknown error occurred', - status: error.response?.status || 500, - }; + return this.buildErrorResponse(error); } } @@ -130,15 +126,39 @@ export class FMPClient { status: response.status, }; } catch (error: any) { - return { - success: false, - data: null, - error: error.message || 'Unknown error occurred', - status: error.response?.status || 500, - }; + return this.buildErrorResponse(error); } } + /** + * Build a classified error response from a caught axios error. + * + * FMP returns the real reason in the response *body* (e.g. + * `{ "Error Message": "Special Endpoint : ..." }`), not in `error.message` + * (which is axios's generic "Request failed with status code N"). We prefer + * the body, then classify into a typed `errorType`. + */ + private buildErrorResponse(error: any): APIResponse { + const body = error?.response?.data; + const fmpMessage = + body && typeof body === 'object' + ? (body['Error Message'] ?? body.message ?? null) + : typeof body === 'string' + ? body + : null; + // status 0 signals "no HTTP response" (network error / timeout) to the classifier. + const status = error?.response?.status ?? 0; + const { errorType, message } = classifyError(status, fmpMessage || error?.message || ''); + + return { + success: false, + data: null, + error: message, + errorType, + status: status || 500, + }; + } + private getClientForVersion(version: 'v3' | 'v4' | 'stable'): AxiosInstance { switch (version) { case 'v3': diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 16df88f..2ee5488 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -46,3 +46,8 @@ export { } from './utils/constants'; export { FMPValidation } from './utils/validation'; + +// Export error classification utilities +export { classifyError } from './utils/error-classifier'; +export type { ClassifiedError } from './utils/error-classifier'; +export { FMPError } from './shared'; diff --git a/packages/api/src/live/validate.ts b/packages/api/src/live/validate.ts index e7beb03..80e72c6 100644 --- a/packages/api/src/live/validate.ts +++ b/packages/api/src/live/validate.ts @@ -13,6 +13,7 @@ // sub-objects are not reported in this iteration. import { z } from 'zod'; +import { classifyError } from '../utils/error-classifier'; export type Outcome = 'PASS' | 'FAIL' | 'SKIP' | 'DRIFT'; @@ -33,37 +34,26 @@ export interface Classification { const OUTCOME_RANK: Record = { PASS: 0, DRIFT: 1, SKIP: 2, FAIL: 3 }; -const PLAN_LOCKED_PATTERNS = [ - /exclusive endpoint/i, - /not available under your current/i, - /premium/i, - /special endpoint/i, - /upgrade your plan/i, -]; - /** * Classify a transport-level outcome. Returns null when the request succeeded - * (so the caller proceeds to shape validation). + * (so the caller proceeds to shape validation). Delegates the failure + * categorization to the shared `classifyError` so the live tool and the + * production client agree on what counts as plan-locked / rate-limited. */ export function classifyTransport(res: TransportResult): Classification | null { if (res.success) return null; - const error = res.error ?? ''; + const { errorType } = classifyError(res.status, res.error ?? ''); - if (res.status === 429) { - return { outcome: 'SKIP', detail: `quota/rate limit (429): ${error}`.trim(), stopRun: true }; + if (errorType === 'rate-limit') { + return { outcome: 'SKIP', detail: `quota/rate limit (${res.status}): ${res.error ?? ''}`.trim(), stopRun: true }; } - const planLocked = - res.status === 402 || - res.status === 403 || - PLAN_LOCKED_PATTERNS.some((re) => re.test(error)); - - if (planLocked) { - return { outcome: 'SKIP', detail: `plan-locked (${res.status}): ${error}`.trim() }; + if (errorType === 'plan-restricted') { + return { outcome: 'SKIP', detail: `plan-locked (${res.status}): ${res.error ?? ''}`.trim() }; } - return { outcome: 'FAIL', detail: `request failed (${res.status}): ${error || 'unknown error'}` }; + return { outcome: 'FAIL', detail: `request failed (${res.status}): ${res.error || 'unknown error'}` }; } /** Classify a single object value against an (object) schema. */ diff --git a/packages/api/src/utils/error-classifier.ts b/packages/api/src/utils/error-classifier.ts new file mode 100644 index 0000000..e81b5eb --- /dev/null +++ b/packages/api/src/utils/error-classifier.ts @@ -0,0 +1,61 @@ +// Classifies a failed FMP request into a typed category + human-readable message. +// +// FMP signals plan/subscription restrictions in two ways: HTTP 402/403, and/or a +// message body like "Special Endpoint : This endpoint is only for users with..." +// or "Exclusive Endpoint". This classifier folds both signals into a single +// `FMPErrorType` so callers (and AI tools) can react to the *kind* of failure. + +import type { FMPErrorType } from 'fmp-node-types'; + +/** Message patterns FMP uses for plan/subscription-locked endpoints. */ +const PLAN_LOCKED_PATTERNS = [ + /exclusive endpoint/i, + /not available under your current/i, + /premium/i, + /special endpoint/i, + /upgrade your plan/i, +]; + +export interface ClassifiedError { + errorType: FMPErrorType; + message: string; +} + +/** + * Classify a failed request. + * + * @param status HTTP status code; use `0` when no HTTP response was received + * (network error / timeout), which maps to `network`. + * @param rawMessage The best available message — prefer FMP's response body over + * axios's generic "Request failed with status code N". + */ +export function classifyError(status: number, rawMessage: string): ClassifiedError { + const msg = rawMessage?.trim() || 'Unknown error occurred'; + const matchesPlanPattern = PLAN_LOCKED_PATTERNS.some(re => re.test(msg)); + + if (status === 429) { + return { errorType: 'rate-limit', message: `FMP rate limit reached: ${msg}` }; + } + if (status === 401) { + return { + errorType: 'auth', + message: `FMP authentication failed (check your FMP_API_KEY): ${msg}`, + }; + } + if (status === 402 || status === 403 || matchesPlanPattern) { + return { + errorType: 'plan-restricted', + message: `This endpoint is not available on your current FMP plan. (${status}: ${msg})`, + }; + } + if (status === 404) { + return { errorType: 'not-found', message: `Not found: ${msg}` }; + } + if (status === 400) { + return { errorType: 'bad-request', message: `Invalid request: ${msg}` }; + } + if (status === 0) { + return { errorType: 'network', message: `Network error: ${msg}` }; + } + return { errorType: 'unknown', message: msg }; +} diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index ed8ef0a..499b6eb 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -1,5 +1,25 @@ # fmp-ai-tools +## 0.2.0-beta.4 + +### Patch Changes + +- Tools no longer throw out of `execute`. A thrown error — most importantly a missing/invalid `FMP_API_KEY`, which throws from the `FMP` constructor before any request is made — is now caught at the tool boundary and returned to the model as the same structured `{ error, type, message, status }` shape (with `type: "auth"` for key problems). Previously the raw exception reached the AI SDK and the agent received only a vague failure with no reason. + +## 0.2.0-beta.3 + +### Minor Changes + +- Typed error classification for FMP failures, surfaced through the AI tools. + - **fmp-node-types**: `APIResponse` gains an optional `errorType` (`plan-restricted | rate-limit | auth | not-found | bad-request | network | unknown`). + - **fmp-node-api**: the client now reads FMP's real error message from the response body and classifies failures (new `classifyError` export). Plan/subscription-restricted endpoints (402/403 or "Exclusive/Special Endpoint") are reported as `plan-restricted` instead of a generic error. + - **fmp-ai-tools**: every tool now returns a structured error (`{ error, type, message, status }`) to the model on failure instead of `null`, so an agent can explain _why_ a call failed — e.g. that the data requires a higher FMP plan. + +### Patch Changes + +- Updated dependencies + - fmp-node-api@0.2.0-beta.1 + ## 0.2.0-beta.2 ### Patch Changes diff --git a/packages/tools/README.md b/packages/tools/README.md index c62b761..85b91b4 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -302,12 +302,25 @@ Each tool accepts specific parameters. Here are some common ones: ## Error Handling -The tools handle API errors gracefully and will return informative error messages if: +When a request fails, a tool returns a structured JSON error (instead of `null`) so the model can explain *why* to the user: -- The API key is invalid or missing -- The requested data is not available -- Rate limits are exceeded -- Invalid parameters are provided +```json +{ "error": true, "type": "plan-restricted", "message": "This endpoint is not available on your current FMP plan. (403: ...)", "status": 403 } +``` + +The `type` field classifies the failure so your agent can react appropriately: + +| `type` | Meaning | +| ----------------- | ------------------------------------------------------------ | +| `plan-restricted` | Endpoint isn't included in your FMP subscription (402/403) | +| `rate-limit` | FMP quota / rate limit reached (429) | +| `auth` | Invalid or missing `FMP_API_KEY` (401) | +| `not-found` | Resource does not exist (404) | +| `bad-request` | Invalid parameters (400) | +| `network` | No response from FMP (timeout / offline) | +| `unknown` | Anything else | + +This is especially useful on lower FMP tiers: an agent calling an endpoint your plan doesn't cover now gets a clear `plan-restricted` message it can relay, rather than empty data. (Direct `fmp-node-api` callers get the same classification via `response.errorType`.) ## Testing Tools diff --git a/packages/tools/package.json b/packages/tools/package.json index 049a20a..bd48e04 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.2.0-beta.2", + "version": "0.2.0-beta.4", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/tools/src/__tests__/providers/openai/quote.test.ts b/packages/tools/src/__tests__/providers/openai/quote.test.ts index 0aa6224..7cfca89 100644 --- a/packages/tools/src/__tests__/providers/openai/quote.test.ts +++ b/packages/tools/src/__tests__/providers/openai/quote.test.ts @@ -23,4 +23,21 @@ describe('getStockQuote (minimal coverage)', () => { expect(mockQuote.getQuote).toHaveBeenCalledWith('AAPL'); expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL', price: 123.45 }]); }); + + it('surfaces a plan-restricted error instead of null', async () => { + mockQuote.getQuote.mockResolvedValueOnce({ + success: false, + data: null, + error: 'This endpoint is not available on your current FMP plan. (403: ...)', + errorType: 'plan-restricted', + status: 403, + }); + + const result = await (getStockQuote as any).execute({ symbol: 'AAPL' }); + const parsed = JSON.parse(result); + + expect(parsed.error).toBe(true); + expect(parsed.type).toBe('plan-restricted'); + expect(parsed.status).toBe(403); + }); }); diff --git a/packages/tools/src/__tests__/utils/format-response.test.ts b/packages/tools/src/__tests__/utils/format-response.test.ts new file mode 100644 index 0000000..4f2b378 --- /dev/null +++ b/packages/tools/src/__tests__/utils/format-response.test.ts @@ -0,0 +1,72 @@ +import { toToolResponse, toToolError } from '@/utils/format-response'; + +describe('toToolResponse', () => { + it('returns pretty JSON of data on success', () => { + const out = toToolResponse({ + success: true, + data: [{ symbol: 'AAPL', price: 1 }], + error: null, + status: 200, + }); + expect(JSON.parse(out)).toEqual([{ symbol: 'AAPL', price: 1 }]); + }); + + it('returns a "no data" note when a successful payload is empty', () => { + const out = toToolResponse({ success: true, data: [], error: null, status: 200 }); + expect(JSON.parse(out)).toEqual({ data: [], note: expect.stringContaining('No data') }); + }); + + it('surfaces a structured error with the classified type on failure', () => { + const out = toToolResponse({ + success: false, + data: null, + error: 'This endpoint is not available on your current FMP plan. (403: ...)', + errorType: 'plan-restricted', + status: 403, + }); + expect(JSON.parse(out)).toEqual({ + error: true, + type: 'plan-restricted', + message: expect.stringContaining('not available on your current FMP plan'), + status: 403, + }); + }); + + it('defaults type to unknown when errorType is absent', () => { + const out = toToolResponse({ success: false, data: null, error: 'boom', status: 500 }); + expect(JSON.parse(out).type).toBe('unknown'); + }); + + it('treats a response without an explicit failure as success', () => { + // Defensive: partial responses (no `success` field) should still return data. + const out = toToolResponse({ data: { ok: true } } as never); + expect(JSON.parse(out)).toEqual({ ok: true }); + }); +}); + +describe('toToolError', () => { + it('classifies a missing/invalid FMP API key as auth', () => { + const out = toToolError( + new Error( + 'FMP API key is required. Please provide it in the config or set the FMP_API_KEY environment variable.', + ), + ); + const parsed = JSON.parse(out); + expect(parsed).toEqual({ + error: true, + type: 'auth', + message: expect.stringContaining('API key'), + status: 0, + }); + }); + + it('falls back to unknown for other thrown errors', () => { + const parsed = JSON.parse(toToolError(new Error('something exploded'))); + expect(parsed.type).toBe('unknown'); + expect(parsed.error).toBe(true); + }); + + it('handles non-Error throwables', () => { + expect(JSON.parse(toToolError('boom')).message).toBe('boom'); + }); +}); 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 65c74f9..6152fbe 100644 --- a/packages/tools/src/__tests__/utils/openai-tool-wrapper.test.ts +++ b/packages/tools/src/__tests__/utils/openai-tool-wrapper.test.ts @@ -36,4 +36,22 @@ describe('createOpenAITool', () => { expect(spy).toHaveBeenCalledWith({ symbol: 'AAPL' }); expect(result).toBe('got AAPL'); }); + + it('catches a thrown execute and returns a structured error instead of throwing', async () => { + const tool = createOpenAITool({ + name: 'throwTool', + description: 'Throws', + inputSchema: z.object({ symbol: z.string() }), + execute: async () => { + throw new Error('FMP API key is required. Set the FMP_API_KEY environment variable.'); + }, + }); + + const result = await (tool as any).execute({ symbol: 'AAPL' }); + const parsed = JSON.parse(result); + + expect(parsed.error).toBe(true); + expect(parsed.type).toBe('auth'); + expect(parsed.message).toContain('API key'); + }); }); diff --git a/packages/tools/src/providers/openai/calendar.ts b/packages/tools/src/providers/openai/calendar.ts index a6bd7b3..a14d33d 100644 --- a/packages/tools/src/providers/openai/calendar.ts +++ b/packages/tools/src/providers/openai/calendar.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; // Common input schema for calendar date range const calendarInputSchema = z.object({ @@ -18,7 +19,7 @@ export const getEarningsCalendar = createOpenAITool({ from: from ?? undefined, to: to ?? undefined, }); - return JSON.stringify(earningsCalendar.data, null, 2); + return toToolResponse(earningsCalendar); }, }); @@ -32,6 +33,6 @@ export const getEconomicCalendar = createOpenAITool({ from: from ?? undefined, to: to ?? undefined, }); - return JSON.stringify(economicCalendar.data, null, 2); + return toToolResponse(economicCalendar); }, }); diff --git a/packages/tools/src/providers/openai/company.ts b/packages/tools/src/providers/openai/company.ts index f37c034..fc4a1f3 100644 --- a/packages/tools/src/providers/openai/company.ts +++ b/packages/tools/src/providers/openai/company.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; export const getCompanyProfile = createOpenAITool({ name: 'getCompanyProfile', @@ -14,7 +15,7 @@ export const getCompanyProfile = createOpenAITool({ execute: async ({ symbol }) => { const fmp = getFMPClient(); const companyProfile = await fmp.company.getCompanyProfile(symbol); - return JSON.stringify(companyProfile.data, null, 2); + return toToolResponse(companyProfile); }, }); @@ -27,7 +28,7 @@ export const getCompanySharesFloat = createOpenAITool({ execute: async ({ symbol }) => { const fmp = getFMPClient(); const companySharesFloat = await fmp.company.getSharesFloat(symbol); - return JSON.stringify(companySharesFloat.data, null, 2); + return toToolResponse(companySharesFloat); }, }); @@ -40,6 +41,6 @@ export const getCompanyExecutiveCompensation = createOpenAITool({ execute: async ({ symbol }) => { const fmp = getFMPClient(); const companyExecutiveCompensation = await fmp.company.getExecutiveCompensation(symbol); - return JSON.stringify(companyExecutiveCompensation.data, null, 2); + return toToolResponse(companyExecutiveCompensation); }, }); diff --git a/packages/tools/src/providers/openai/economic.ts b/packages/tools/src/providers/openai/economic.ts index 23825b4..a6ae083 100644 --- a/packages/tools/src/providers/openai/economic.ts +++ b/packages/tools/src/providers/openai/economic.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; // Input schema for treasury rates with date range const treasuryRatesInputSchema = z.object({ @@ -51,7 +52,7 @@ export const getTreasuryRates = createOpenAITool({ from: from ?? undefined, to: to ?? undefined, }); - return JSON.stringify(treasuryRates.data, null, 2); + return toToolResponse(treasuryRates); }, }); @@ -66,6 +67,6 @@ export const getEconomicIndicators = createOpenAITool({ from: from ?? undefined, to: to ?? undefined, }); - return JSON.stringify(economicIndicators.data, null, 2); + return toToolResponse(economicIndicators); }, }); diff --git a/packages/tools/src/providers/openai/etf.ts b/packages/tools/src/providers/openai/etf.ts index 79539bd..d84b2ba 100644 --- a/packages/tools/src/providers/openai/etf.ts +++ b/packages/tools/src/providers/openai/etf.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; // Input schema for ETF holdings with optional date const etfHoldingsInputSchema = z.object({ @@ -30,7 +31,7 @@ export const getETFHoldings = createOpenAITool({ } const etfHoldings = await fmp.etf.getHoldings(params); - return JSON.stringify(etfHoldings.data, null, 2); + return toToolResponse(etfHoldings); }, }); @@ -41,6 +42,6 @@ export const getETFProfile = createOpenAITool({ execute: async ({ symbol }) => { const fmp = getFMPClient(); const etfProfile = await fmp.etf.getProfile(symbol); - return JSON.stringify(etfProfile.data, null, 2); + return toToolResponse(etfProfile); }, }); diff --git a/packages/tools/src/providers/openai/financial.ts b/packages/tools/src/providers/openai/financial.ts index b4973aa..daa2531 100644 --- a/packages/tools/src/providers/openai/financial.ts +++ b/packages/tools/src/providers/openai/financial.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; export const getBalanceSheet = createOpenAITool({ name: 'getBalanceSheet', @@ -20,7 +21,7 @@ export const getBalanceSheet = createOpenAITool({ period, limit: Number(limit), }); - return JSON.stringify(balanceSheet.data, null, 2); + return toToolResponse(balanceSheet); }, }); @@ -42,7 +43,7 @@ export const getIncomeStatement = createOpenAITool({ period, limit: Number(limit), }); - return JSON.stringify(incomeStatement.data, null, 2); + return toToolResponse(incomeStatement); }, }); @@ -65,7 +66,7 @@ export const getCashFlowStatement = createOpenAITool({ period, limit: Number(limit), }); - return JSON.stringify(cashFlowStatement.data, null, 2); + return toToolResponse(cashFlowStatement); }, }); @@ -83,7 +84,7 @@ export const getKeyMetrics = createOpenAITool({ 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); + return toToolResponse(keyMetrics); }, }); @@ -106,7 +107,7 @@ export const getFinancialRatios = createOpenAITool({ period, limit: Number(limit), }); - return JSON.stringify(financialRatios.data, null, 2); + return toToolResponse(financialRatios); }, }); @@ -128,7 +129,7 @@ export const getEnterpriseValue = createOpenAITool({ period, limit: Number(limit), }); - return JSON.stringify(enterpriseValue.data, null, 2); + return toToolResponse(enterpriseValue); }, }); @@ -150,7 +151,7 @@ export const getCashflowGrowth = createOpenAITool({ period, limit: Number(limit), }); - return JSON.stringify(cashflowGrowth.data, null, 2); + return toToolResponse(cashflowGrowth); }, }); @@ -172,7 +173,7 @@ export const getIncomeGrowth = createOpenAITool({ period, limit: Number(limit), }); - return JSON.stringify(incomeGrowth.data, null, 2); + return toToolResponse(incomeGrowth); }, }); @@ -194,7 +195,7 @@ export const getBalanceSheetGrowth = createOpenAITool({ period, limit: Number(limit), }); - return JSON.stringify(balanceSheetGrowth.data, null, 2); + return toToolResponse(balanceSheetGrowth); }, }); @@ -216,7 +217,7 @@ export const getFinancialGrowth = createOpenAITool({ period, limit: Number(limit), }); - return JSON.stringify(financialGrowth.data, null, 2); + return toToolResponse(financialGrowth); }, }); @@ -233,6 +234,6 @@ export const getEarningsHistorical = createOpenAITool({ symbol, limit: Number(limit), }); - return JSON.stringify(earningsHistorical.data, null, 2); + return toToolResponse(earningsHistorical); }, }); diff --git a/packages/tools/src/providers/openai/insider.ts b/packages/tools/src/providers/openai/insider.ts index a56f00b..e1072f6 100644 --- a/packages/tools/src/providers/openai/insider.ts +++ b/packages/tools/src/providers/openai/insider.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; // Input schema for insider trading with symbol and optional page const insiderTradingInputSchema = z.object({ @@ -24,6 +25,6 @@ export const getInsiderTrading = createOpenAITool({ execute: async ({ symbol, page }) => { const fmp = getFMPClient(); const insiderTrading = await fmp.insider.getInsiderTradesBySymbol(symbol, page); - return JSON.stringify(insiderTrading.data, null, 2); + return toToolResponse(insiderTrading); }, }); diff --git a/packages/tools/src/providers/openai/institutional.ts b/packages/tools/src/providers/openai/institutional.ts index 30ea3ea..99549b8 100644 --- a/packages/tools/src/providers/openai/institutional.ts +++ b/packages/tools/src/providers/openai/institutional.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; // Input schema for institutional holders with symbol const institutionalHoldersInputSchema = z.object({ @@ -20,6 +21,6 @@ export const getInstitutionalHolders = createOpenAITool({ const institutionalHolders = await fmp.institutional.getInstitutionalHolders({ symbol }); // Return formatted JSON string - return JSON.stringify(institutionalHolders.data, null, 2); + return toToolResponse(institutionalHolders); }, }); diff --git a/packages/tools/src/providers/openai/market.ts b/packages/tools/src/providers/openai/market.ts index 398ca42..5e703e3 100644 --- a/packages/tools/src/providers/openai/market.ts +++ b/packages/tools/src/providers/openai/market.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; // Empty schema for tools that don't require parameters const emptyInputSchema = z.object({}); @@ -12,7 +13,7 @@ export const getMarketPerformance = createOpenAITool({ execute: async () => { const fmp = getFMPClient(); const marketPerformance = await fmp.market.getMarketPerformance(); - return JSON.stringify(marketPerformance.data, null, 2); + return toToolResponse(marketPerformance); }, }); @@ -23,7 +24,7 @@ export const getSectorPerformance = createOpenAITool({ execute: async () => { const fmp = getFMPClient(); const sectorPerformance = await fmp.market.getSectorPerformance(); - return JSON.stringify(sectorPerformance.data, null, 2); + return toToolResponse(sectorPerformance); }, }); @@ -34,7 +35,7 @@ export const getGainers = createOpenAITool({ execute: async () => { const fmp = getFMPClient(); const gainers = await fmp.market.getGainers(); - return JSON.stringify(gainers.data, null, 2); + return toToolResponse(gainers); }, }); @@ -45,7 +46,7 @@ export const getLosers = createOpenAITool({ execute: async () => { const fmp = getFMPClient(); const losers = await fmp.market.getLosers(); - return JSON.stringify(losers.data, null, 2); + return toToolResponse(losers); }, }); @@ -56,6 +57,6 @@ export const getMostActive = createOpenAITool({ execute: async () => { const fmp = getFMPClient(); const mostActive = await fmp.market.getMostActive(); - return JSON.stringify(mostActive.data, null, 2); + return toToolResponse(mostActive); }, }); diff --git a/packages/tools/src/providers/openai/quote.ts b/packages/tools/src/providers/openai/quote.ts index 504b440..9337ea0 100644 --- a/packages/tools/src/providers/openai/quote.ts +++ b/packages/tools/src/providers/openai/quote.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; // Input schema for stock quote with symbol const stockQuoteInputSchema = z.object({ @@ -18,6 +19,6 @@ export const getStockQuote = createOpenAITool({ execute: async ({ symbol }) => { const fmp = getFMPClient(); const stockQuote = await fmp.quote.getQuote(symbol); - return JSON.stringify(stockQuote.data, null, 2); + return toToolResponse(stockQuote); }, }); diff --git a/packages/tools/src/providers/openai/senate-house.ts b/packages/tools/src/providers/openai/senate-house.ts index d47c050..16d7c02 100644 --- a/packages/tools/src/providers/openai/senate-house.ts +++ b/packages/tools/src/providers/openai/senate-house.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; // Input schema for symbol-based trading data const symbolInputSchema = z.object({ @@ -32,7 +33,7 @@ export const getSenateTrading = createOpenAITool({ execute: async ({ symbol }) => { const fmp = getFMPClient(); const senateTrading = await fmp.senateHouse.getSenateTrading({ symbol }); - return JSON.stringify(senateTrading.data, null, 2); + return toToolResponse(senateTrading); }, }); @@ -44,7 +45,7 @@ export const getHouseTrading = createOpenAITool({ execute: async ({ symbol }) => { const fmp = getFMPClient(); const houseTrading = await fmp.senateHouse.getHouseTrading({ symbol }); - return JSON.stringify(houseTrading.data, null, 2); + return toToolResponse(houseTrading); }, }); @@ -56,7 +57,7 @@ export const getSenateTradingByName = createOpenAITool({ execute: async ({ name }) => { const fmp = getFMPClient(); const senateTradingByName = await fmp.senateHouse.getSenateTradingByName({ name }); - return JSON.stringify(senateTradingByName.data, null, 2); + return toToolResponse(senateTradingByName); }, }); @@ -68,7 +69,7 @@ export const getHouseTradingByName = createOpenAITool({ execute: async ({ name }) => { const fmp = getFMPClient(); const houseTradingByName = await fmp.senateHouse.getHouseTradingByName({ name }); - return JSON.stringify(houseTradingByName.data, null, 2); + return toToolResponse(houseTradingByName); }, }); @@ -80,7 +81,7 @@ export const getSenateTradingRSSFeed = createOpenAITool({ execute: async ({ page }) => { const fmp = getFMPClient(); const senateTradingRSSFeed = await fmp.senateHouse.getSenateTradingRSSFeed({ page }); - return JSON.stringify(senateTradingRSSFeed.data, null, 2); + return toToolResponse(senateTradingRSSFeed); }, }); @@ -92,6 +93,6 @@ export const getHouseTradingRSSFeed = createOpenAITool({ execute: async ({ page }) => { const fmp = getFMPClient(); const houseTradingRSSFeed = await fmp.senateHouse.getHouseTradingRSSFeed({ page }); - return JSON.stringify(houseTradingRSSFeed.data, null, 2); + return toToolResponse(houseTradingRSSFeed); }, }); diff --git a/packages/tools/src/providers/openai/stock.ts b/packages/tools/src/providers/openai/stock.ts index 9257ae3..e11089e 100644 --- a/packages/tools/src/providers/openai/stock.ts +++ b/packages/tools/src/providers/openai/stock.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; // Input schema for symbol-based stock operations const symbolInputSchema = z.object({ @@ -17,7 +18,7 @@ export const getMarketCap = createOpenAITool({ execute: async ({ symbol }) => { const fmp = getFMPClient(); const marketCap = await fmp.stock.getMarketCap(symbol); - return JSON.stringify(marketCap.data, null, 2); + return toToolResponse(marketCap); }, }); @@ -28,7 +29,7 @@ export const getStockSplits = createOpenAITool({ execute: async ({ symbol }) => { const fmp = getFMPClient(); const stockSplits = await fmp.stock.getStockSplits(symbol); - return JSON.stringify(stockSplits.data, null, 2); + return toToolResponse(stockSplits); }, }); @@ -39,7 +40,7 @@ export const getDividendHistory = createOpenAITool({ execute: async ({ symbol }) => { const fmp = getFMPClient(); const dividendHistory = await fmp.stock.getDividendHistory(symbol); - return JSON.stringify(dividendHistory.data, null, 2); + return toToolResponse(dividendHistory); }, }); diff --git a/packages/tools/src/providers/vercel-ai/calendar.ts b/packages/tools/src/providers/vercel-ai/calendar.ts index 56f0a38..6fdfce6 100644 --- a/packages/tools/src/providers/vercel-ai/calendar.ts +++ b/packages/tools/src/providers/vercel-ai/calendar.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createTool } from '@/utils/aisdk-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; export const calendarTools = { getEarningsCalendar: createTool({ @@ -13,7 +14,7 @@ export const calendarTools = { execute: async ({ from, to }) => { const fmp = getFMPClient(); const earningsCalendar = await fmp.calendar.getEarningsCalendar({ from, to }); - const response = JSON.stringify(earningsCalendar.data, null, 2); + const response = toToolResponse(earningsCalendar); return response; }, }), @@ -28,7 +29,7 @@ export const calendarTools = { execute: async ({ from, to }) => { const fmp = getFMPClient(); const economicCalendar = await fmp.calendar.getEconomicsCalendar({ from, to }); - const response = JSON.stringify(economicCalendar.data, null, 2); + const response = toToolResponse(economicCalendar); return response; }, }), diff --git a/packages/tools/src/providers/vercel-ai/company.ts b/packages/tools/src/providers/vercel-ai/company.ts index 9b6f5d0..cf813d5 100644 --- a/packages/tools/src/providers/vercel-ai/company.ts +++ b/packages/tools/src/providers/vercel-ai/company.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createTool } from '@/utils/aisdk-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; export const companyTools = { getCompanyProfile: createTool({ @@ -12,7 +13,7 @@ export const companyTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const companyProfile = await fmp.company.getCompanyProfile(symbol); - const response = JSON.stringify(companyProfile.data, null, 2); + const response = toToolResponse(companyProfile); return response; }, }), @@ -25,7 +26,7 @@ export const companyTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const companySharesFloat = await fmp.company.getSharesFloat(symbol); - const response = JSON.stringify(companySharesFloat.data, null, 2); + const response = toToolResponse(companySharesFloat); return response; }, }), @@ -39,7 +40,7 @@ export const companyTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const companyExecutiveCompensation = await fmp.company.getExecutiveCompensation(symbol); - const response = JSON.stringify(companyExecutiveCompensation.data, null, 2); + const response = toToolResponse(companyExecutiveCompensation); return response; }, }), diff --git a/packages/tools/src/providers/vercel-ai/economic.ts b/packages/tools/src/providers/vercel-ai/economic.ts index 035f8a6..2bb1917 100644 --- a/packages/tools/src/providers/vercel-ai/economic.ts +++ b/packages/tools/src/providers/vercel-ai/economic.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; import { createTool } from '@/utils/aisdk-tool-wrapper'; export const economicTools = { @@ -13,7 +14,7 @@ export const economicTools = { execute: async ({ from, to }) => { const fmp = getFMPClient(); const treasuryRates = await fmp.economic.getTreasuryRates({ from, to }); - const response = JSON.stringify(treasuryRates.data, null, 2); + const response = toToolResponse(treasuryRates); return response; }, }), @@ -55,7 +56,7 @@ export const economicTools = { execute: async ({ name, from, to }) => { const fmp = getFMPClient(); const economicIndicators = await fmp.economic.getEconomicIndicators({ name, from, to }); - const response = JSON.stringify(economicIndicators.data, null, 2); + const response = toToolResponse(economicIndicators); return response; }, }), diff --git a/packages/tools/src/providers/vercel-ai/etf.ts b/packages/tools/src/providers/vercel-ai/etf.ts index d2dac1b..f522aa4 100644 --- a/packages/tools/src/providers/vercel-ai/etf.ts +++ b/packages/tools/src/providers/vercel-ai/etf.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; import { createTool } from '@/utils/aisdk-tool-wrapper'; export const etfTools = { @@ -17,7 +18,7 @@ export const etfTools = { params.date = date; } const etfHoldings = await fmp.etf.getHoldings(params); - const response = JSON.stringify(etfHoldings.data, null, 2); + const response = toToolResponse(etfHoldings); return response; }, }), @@ -31,7 +32,7 @@ export const etfTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const etfProfile = await fmp.etf.getProfile(symbol); - const response = JSON.stringify(etfProfile.data, null, 2); + const response = toToolResponse(etfProfile); return response; }, }), diff --git a/packages/tools/src/providers/vercel-ai/financial.ts b/packages/tools/src/providers/vercel-ai/financial.ts index a2e3617..93d5570 100644 --- a/packages/tools/src/providers/vercel-ai/financial.ts +++ b/packages/tools/src/providers/vercel-ai/financial.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; import { createTool } from '@/utils/aisdk-tool-wrapper'; export const financialTools = { @@ -17,7 +18,7 @@ export const financialTools = { execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); const balanceSheet = await fmp.financial.getBalanceSheet({ symbol, period, limit }); - const response = JSON.stringify(balanceSheet.data, null, 2); + const response = toToolResponse(balanceSheet); return response; }, }), @@ -36,7 +37,7 @@ export const financialTools = { execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); const incomeStatement = await fmp.financial.getIncomeStatement({ symbol, period, limit }); - const response = JSON.stringify(incomeStatement.data, null, 2); + const response = toToolResponse(incomeStatement); return response; }, }), @@ -55,7 +56,7 @@ export const financialTools = { execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); const cashFlowStatement = await fmp.financial.getCashFlowStatement({ symbol, period, limit }); - const response = JSON.stringify(cashFlowStatement.data, null, 2); + const response = toToolResponse(cashFlowStatement); return response; }, }), @@ -74,7 +75,7 @@ export const financialTools = { 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); + const response = toToolResponse(keyMetrics); return response; }, }), @@ -93,7 +94,7 @@ export const financialTools = { execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); const financialRatios = await fmp.financial.getFinancialRatios({ symbol, period, limit }); - const response = JSON.stringify(financialRatios.data, null, 2); + const response = toToolResponse(financialRatios); return response; }, }), @@ -112,7 +113,7 @@ export const financialTools = { 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); + const response = toToolResponse(enterpriseValue); return response; }, }), @@ -131,7 +132,7 @@ export const financialTools = { 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); + const response = toToolResponse(cashflowGrowth); return response; }, }), @@ -150,7 +151,7 @@ export const financialTools = { 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); + const response = toToolResponse(incomeGrowth); return response; }, }), @@ -173,7 +174,7 @@ export const financialTools = { period, limit, }); - const response = JSON.stringify(balanceSheetGrowth.data, null, 2); + const response = toToolResponse(balanceSheetGrowth); return response; }, }), @@ -192,7 +193,7 @@ export const financialTools = { 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); + const response = toToolResponse(financialGrowth); return response; }, }), @@ -207,7 +208,7 @@ export const financialTools = { execute: async ({ symbol, limit }) => { const fmp = getFMPClient(); const earningsHistorical = await fmp.financial.getEarningsHistorical({ symbol, limit }); - const response = JSON.stringify(earningsHistorical.data, null, 2); + const response = toToolResponse(earningsHistorical); return response; }, }), diff --git a/packages/tools/src/providers/vercel-ai/insider.ts b/packages/tools/src/providers/vercel-ai/insider.ts index 9dbd461..86364db 100644 --- a/packages/tools/src/providers/vercel-ai/insider.ts +++ b/packages/tools/src/providers/vercel-ai/insider.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { createTool } from '@/utils/aisdk-tool-wrapper'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; export const insiderTools = { getInsiderTrading: createTool({ @@ -13,7 +14,7 @@ export const insiderTools = { execute: async ({ symbol, page }) => { const fmp = getFMPClient(); const insiderTrading = await fmp.insider.getInsiderTradesBySymbol(symbol, page); - const response = JSON.stringify(insiderTrading.data, null, 2); + const response = toToolResponse(insiderTrading); return response; }, }), diff --git a/packages/tools/src/providers/vercel-ai/institutional.ts b/packages/tools/src/providers/vercel-ai/institutional.ts index a4af128..269c816 100644 --- a/packages/tools/src/providers/vercel-ai/institutional.ts +++ b/packages/tools/src/providers/vercel-ai/institutional.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; import { createTool } from '@/utils/aisdk-tool-wrapper'; export const institutionalTools = { @@ -12,7 +13,7 @@ export const institutionalTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const institutionalHolders = await fmp.institutional.getInstitutionalHolders({ symbol }); - const response = JSON.stringify(institutionalHolders.data, null, 2); + const response = toToolResponse(institutionalHolders); return response; }, }), diff --git a/packages/tools/src/providers/vercel-ai/market.ts b/packages/tools/src/providers/vercel-ai/market.ts index 7efe869..72b2846 100644 --- a/packages/tools/src/providers/vercel-ai/market.ts +++ b/packages/tools/src/providers/vercel-ai/market.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; import { createTool } from '@/utils/aisdk-tool-wrapper'; export const marketTools = { @@ -10,7 +11,7 @@ export const marketTools = { execute: async () => { const fmp = getFMPClient(); const marketPerformance = await fmp.market.getMarketPerformance(); - const response = JSON.stringify(marketPerformance.data, null, 2); + const response = toToolResponse(marketPerformance); return response; }, }), @@ -22,7 +23,7 @@ export const marketTools = { execute: async () => { const fmp = getFMPClient(); const sectorPerformance = await fmp.market.getSectorPerformance(); - const response = JSON.stringify(sectorPerformance.data, null, 2); + const response = toToolResponse(sectorPerformance); return response; }, }), @@ -34,7 +35,7 @@ export const marketTools = { execute: async () => { const fmp = getFMPClient(); const gainers = await fmp.market.getGainers(); - const response = JSON.stringify(gainers.data, null, 2); + const response = toToolResponse(gainers); return response; }, }), @@ -46,7 +47,7 @@ export const marketTools = { execute: async () => { const fmp = getFMPClient(); const losers = await fmp.market.getLosers(); - const response = JSON.stringify(losers.data, null, 2); + const response = toToolResponse(losers); return response; }, }), @@ -58,7 +59,7 @@ export const marketTools = { execute: async () => { const fmp = getFMPClient(); const mostActive = await fmp.market.getMostActive(); - const response = JSON.stringify(mostActive.data, null, 2); + const response = toToolResponse(mostActive); return response; }, }), diff --git a/packages/tools/src/providers/vercel-ai/quote.ts b/packages/tools/src/providers/vercel-ai/quote.ts index 557094d..6709ef7 100644 --- a/packages/tools/src/providers/vercel-ai/quote.ts +++ b/packages/tools/src/providers/vercel-ai/quote.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; import { createTool } from '@/utils/aisdk-tool-wrapper'; export const quoteTools = { @@ -12,7 +13,7 @@ export const quoteTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const stockQuote = await fmp.quote.getQuote(symbol); - const response = JSON.stringify(stockQuote.data, null, 2); + const response = toToolResponse(stockQuote); return response; }, }), diff --git a/packages/tools/src/providers/vercel-ai/senate-house.ts b/packages/tools/src/providers/vercel-ai/senate-house.ts index 2d63c9b..8aec846 100644 --- a/packages/tools/src/providers/vercel-ai/senate-house.ts +++ b/packages/tools/src/providers/vercel-ai/senate-house.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; import { createTool } from '@/utils/aisdk-tool-wrapper'; export const senateHouseTools = { @@ -12,7 +13,7 @@ export const senateHouseTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const senateTrading = await fmp.senateHouse.getSenateTrading({ symbol }); - const response = JSON.stringify(senateTrading.data, null, 2); + const response = toToolResponse(senateTrading); return response; }, }), @@ -26,7 +27,7 @@ export const senateHouseTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const houseTrading = await fmp.senateHouse.getHouseTrading({ symbol }); - const response = JSON.stringify(houseTrading.data, null, 2); + const response = toToolResponse(houseTrading); return response; }, }), @@ -40,7 +41,7 @@ export const senateHouseTools = { execute: async ({ name }) => { const fmp = getFMPClient(); const senateTradingByName = await fmp.senateHouse.getSenateTradingByName({ name }); - const response = JSON.stringify(senateTradingByName.data, null, 2); + const response = toToolResponse(senateTradingByName); return response; }, }), @@ -54,7 +55,7 @@ export const senateHouseTools = { execute: async ({ name }) => { const fmp = getFMPClient(); const houseTradingByName = await fmp.senateHouse.getHouseTradingByName({ name }); - const response = JSON.stringify(houseTradingByName.data, null, 2); + const response = toToolResponse(houseTradingByName); return response; }, }), @@ -68,7 +69,7 @@ export const senateHouseTools = { execute: async ({ page = 0 }) => { const fmp = getFMPClient(); const senateTradingRSSFeed = await fmp.senateHouse.getSenateTradingRSSFeed({ page }); - const response = JSON.stringify(senateTradingRSSFeed.data, null, 2); + const response = toToolResponse(senateTradingRSSFeed); return response; }, }), @@ -82,7 +83,7 @@ export const senateHouseTools = { execute: async ({ page = 0 }) => { const fmp = getFMPClient(); const houseTradingRSSFeed = await fmp.senateHouse.getHouseTradingRSSFeed({ page }); - const response = JSON.stringify(houseTradingRSSFeed.data, null, 2); + const response = toToolResponse(houseTradingRSSFeed); return response; }, }), diff --git a/packages/tools/src/providers/vercel-ai/stock.ts b/packages/tools/src/providers/vercel-ai/stock.ts index 1d9f6e5..be7f13f 100644 --- a/packages/tools/src/providers/vercel-ai/stock.ts +++ b/packages/tools/src/providers/vercel-ai/stock.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; import { createTool } from '@/utils/aisdk-tool-wrapper'; export const stockTools = { @@ -12,7 +13,7 @@ export const stockTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const marketCap = await fmp.stock.getMarketCap(symbol); - const response = JSON.stringify(marketCap.data, null, 2); + const response = toToolResponse(marketCap); return response; }, }), @@ -26,7 +27,7 @@ export const stockTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const stockSplits = await fmp.stock.getStockSplits(symbol); - const response = JSON.stringify(stockSplits.data, null, 2); + const response = toToolResponse(stockSplits); return response; }, }), @@ -40,7 +41,7 @@ export const stockTools = { execute: async ({ symbol }) => { const fmp = getFMPClient(); const dividendHistory = await fmp.stock.getDividendHistory(symbol); - const response = JSON.stringify(dividendHistory.data, null, 2); + const response = toToolResponse(dividendHistory); return response; }, }), diff --git a/packages/tools/src/utils/aisdk-tool-wrapper.ts b/packages/tools/src/utils/aisdk-tool-wrapper.ts index 5032232..60e0c42 100644 --- a/packages/tools/src/utils/aisdk-tool-wrapper.ts +++ b/packages/tools/src/utils/aisdk-tool-wrapper.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { tool } from 'ai'; import { logApiExecutionWithTiming } from './logger'; +import { toToolError } from './format-response'; interface AISDKToolConfig { name: string; @@ -17,7 +18,13 @@ export const createTool = (config: AISDKToolConfig) => { // generic Zod schema; the real Zod schema is still passed at runtime. inputSchema: inputSchema as any, execute: async (input: any) => { - return await logApiExecutionWithTiming(name, input, () => execute(input)); + try { + return await logApiExecutionWithTiming(name, input, () => execute(input)); + } catch (error) { + // Never throw out of a tool — return a structured error the model can + // relay (e.g. a missing FMP_API_KEY, which throws from `new FMP()`). + return toToolError(error); + } }, }); }; diff --git a/packages/tools/src/utils/format-response.ts b/packages/tools/src/utils/format-response.ts new file mode 100644 index 0000000..11ee174 --- /dev/null +++ b/packages/tools/src/utils/format-response.ts @@ -0,0 +1,50 @@ +import type { APIResponse, FMPErrorType } from 'fmp-node-api'; + +/** + * Format an `fmp-node-api` response into the string an AI tool returns to the model. + * + * On success, returns the data as pretty JSON (or a clear "no data" note when the + * payload is empty). On failure, returns a structured error object — including the + * classified `type` (e.g. `plan-restricted`, `rate-limit`, `auth`) — so the model + * can explain *why* the call failed instead of receiving a bare `null`. + */ +export function toToolResponse(res: APIResponse): string { + // Only an explicit failure surfaces an error; the client always sets `success`. + if (res.success === false) { + return JSON.stringify({ + error: true, + type: res.errorType ?? 'unknown', + message: res.error ?? 'The request failed.', + status: res.status, + }); + } + + if (isEmpty(res.data)) { + return JSON.stringify({ data: [], note: 'No data was returned for this request.' }); + } + return JSON.stringify(res.data, null, 2); +} + +/** + * Format a *thrown* error into the same structured shape `toToolResponse` uses. + * + * Some failures throw instead of returning an `APIResponse` — most notably a + * missing/invalid `FMP_API_KEY`, which throws from the `FMP` constructor before + * any request is made. Catching these at the tool boundary lets the model relay + * the real reason instead of receiving a bare exception. + */ +export function toToolError(error: unknown): string { + const message = error instanceof Error ? error.message : String(error); + let type: FMPErrorType = 'unknown'; + if (/api[\s_-]?key/i.test(message)) { + type = 'auth'; + } + return JSON.stringify({ error: true, type, message, status: 0 }); +} + +function isEmpty(data: unknown): boolean { + if (data == null) return true; + if (Array.isArray(data)) return data.length === 0; + if (typeof data === 'object') return Object.keys(data as object).length === 0; + return false; +} diff --git a/packages/tools/src/utils/openai-tool-wrapper.ts b/packages/tools/src/utils/openai-tool-wrapper.ts index 5a85011..a25e515 100644 --- a/packages/tools/src/utils/openai-tool-wrapper.ts +++ b/packages/tools/src/utils/openai-tool-wrapper.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { tool } from '@openai/agents'; import { logApiExecutionWithTiming } from './logger'; +import { toToolError } from './format-response'; export interface OpenAIToolConfig> { name: string; @@ -22,8 +23,14 @@ export function createOpenAITool>(config: OpenAIToolC description, parameters: inputSchema as z.ZodObject, execute: async (input: unknown) => { - const args = inputSchema.parse(input) as z.infer; - return await logApiExecutionWithTiming(name, args, () => execute(args)); + try { + const args = inputSchema.parse(input) as z.infer; + return await logApiExecutionWithTiming(name, args, () => execute(args)); + } catch (error) { + // Never throw out of a tool — return a structured error the model can + // relay (e.g. a missing FMP_API_KEY, which throws from `new FMP()`). + return toToolError(error); + } }, }); } diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index cd0276b..c6f5b3f 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,14 @@ # fmp-node-types +## 0.2.0-beta.1 + +### Minor Changes + +- Typed error classification for FMP failures, surfaced through the AI tools. + - **fmp-node-types**: `APIResponse` gains an optional `errorType` (`plan-restricted | rate-limit | auth | not-found | bad-request | network | unknown`). + - **fmp-node-api**: the client now reads FMP's real error message from the response body and classifies failures (new `classifyError` export). Plan/subscription-restricted endpoints (402/403 or "Exclusive/Special Endpoint") are reported as `plan-restricted` instead of a generic error. + - **fmp-ai-tools**: every tool now returns a structured error (`{ error, type, message, status }`) to the model on failure instead of `null`, so an agent can explain _why_ a call failed — e.g. that the data requires a higher FMP plan. + ## 0.2.0-beta.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index 302211e..b2f6d66 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-types", - "version": "0.2.0-beta.0", + "version": "0.2.0-beta.1", "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/common.ts b/packages/types/src/common.ts index 9a100c9..3b7d5de 100644 --- a/packages/types/src/common.ts +++ b/packages/types/src/common.ts @@ -1,10 +1,23 @@ // Common types used across all FMP API endpoints +// Categorizes a failed request so callers (and AI tools) can react to the +// *kind* of failure rather than parsing the message. Only set when success is false. +export type FMPErrorType = + | 'plan-restricted' // 402/403 or "Exclusive/Special Endpoint" — not included in the API plan + | 'rate-limit' // 429 — quota/rate limit reached + | 'auth' // 401 — invalid or missing API key + | 'not-found' // 404 — resource does not exist + | 'bad-request' // 400 — invalid parameters + | 'network' // no HTTP response (timeout / DNS / offline) + | 'unknown'; // anything else + // Base API response wrapper - consistent structure with nullable fields export interface APIResponse { success: boolean; data: T | null; error: string | null; + /** Categorizes the failure. Present only when `success` is false. */ + errorType?: FMPErrorType; status: number; } From 1736ba8f58cad509be664f71e06cdc7032c2ebb2 Mon Sep 17 00:00:00 2001 From: e-roy Date: Tue, 26 May 2026 07:54:54 -0400 Subject: [PATCH 06/13] unified tools --- .changeset/pre.json | 1 + .changeset/tools-shared-definitions.md | 5 + .cursor/rules/file-structure.mdc | 20 +- CLAUDE.md | 10 +- CONTRIBUTING.md | 23 ++ packages/tools/CHANGELOG.md | 6 + packages/tools/README.md | 21 ++ packages/tools/package.json | 2 +- packages/tools/src/__tests__/core.test.ts | 16 +- .../__tests__/definitions/consistency.test.ts | 35 +++ .../__tests__/definitions/definitions.test.ts | 21 ++ .../providers/openai/calendar.test.ts | 2 +- .../providers/openai/company.test.ts | 2 +- .../providers/openai/economic.test.ts | 2 +- .../__tests__/providers/openai/etf.test.ts | 2 +- .../providers/openai/financial.test.ts | 2 +- .../providers/openai/insider.test.ts | 2 +- .../providers/openai/institutional.test.ts | 2 +- .../__tests__/providers/openai/market.test.ts | 2 +- .../__tests__/providers/openai/quote.test.ts | 2 +- .../providers/openai/senate-house.test.ts | 2 +- .../__tests__/providers/openai/stock.test.ts | 2 +- .../providers/vercel-ai/calendar.test.ts | 2 +- .../providers/vercel-ai/company.test.ts | 2 +- .../providers/vercel-ai/economic.test.ts | 2 +- .../__tests__/providers/vercel-ai/etf.test.ts | 2 +- .../providers/vercel-ai/financial.test.ts | 2 +- .../providers/vercel-ai/insider.test.ts | 2 +- .../providers/vercel-ai/institutional.test.ts | 2 +- .../providers/vercel-ai/market.test.ts | 2 +- .../providers/vercel-ai/quote.test.ts | 2 +- .../providers/vercel-ai/senate-house.test.ts | 2 +- .../providers/vercel-ai/stock.test.ts | 2 +- packages/tools/src/definitions/calendar.ts | 36 +++ packages/tools/src/definitions/company.ts | 35 +++ packages/tools/src/definitions/economic.ts | 65 +++++ packages/tools/src/definitions/etf.ts | 42 +++ packages/tools/src/definitions/financial.ts | 100 ++++++++ packages/tools/src/definitions/index.ts | 43 ++++ packages/tools/src/definitions/insider.ts | 26 ++ .../tools/src/definitions/institutional.ts | 20 ++ packages/tools/src/definitions/market.ts | 39 +++ packages/tools/src/definitions/quote.ts | 19 ++ .../tools/src/definitions/senate-house.ts | 84 ++++++ packages/tools/src/definitions/stock.ts | 34 +++ packages/tools/src/definitions/types.ts | 31 +++ .../tools/src/providers/openai/calendar.ts | 38 --- .../tools/src/providers/openai/company.ts | 46 ---- .../tools/src/providers/openai/economic.ts | 72 ------ packages/tools/src/providers/openai/etf.ts | 47 ---- .../tools/src/providers/openai/financial.ts | 239 ------------------ packages/tools/src/providers/openai/index.ts | 235 ++++++----------- .../tools/src/providers/openai/insider.ts | 30 --- .../src/providers/openai/institutional.ts | 26 -- packages/tools/src/providers/openai/market.ts | 62 ----- packages/tools/src/providers/openai/quote.ts | 24 -- .../src/providers/openai/senate-house.ts | 98 ------- packages/tools/src/providers/openai/stock.ts | 47 ---- .../tools/src/providers/vercel-ai/calendar.ts | 36 --- .../tools/src/providers/vercel-ai/company.ts | 47 ---- .../tools/src/providers/vercel-ai/economic.ts | 63 ----- packages/tools/src/providers/vercel-ai/etf.ts | 39 --- .../src/providers/vercel-ai/financial.ts | 215 ---------------- .../tools/src/providers/vercel-ai/index.ts | 102 ++++---- .../tools/src/providers/vercel-ai/insider.ts | 21 -- .../src/providers/vercel-ai/institutional.ts | 20 -- .../tools/src/providers/vercel-ai/market.ts | 66 ----- .../tools/src/providers/vercel-ai/quote.ts | 20 -- .../src/providers/vercel-ai/senate-house.ts | 90 ------- .../tools/src/providers/vercel-ai/stock.ts | 48 ---- 70 files changed, 864 insertions(+), 1645 deletions(-) create mode 100644 .changeset/tools-shared-definitions.md create mode 100644 packages/tools/src/__tests__/definitions/consistency.test.ts create mode 100644 packages/tools/src/__tests__/definitions/definitions.test.ts create mode 100644 packages/tools/src/definitions/calendar.ts create mode 100644 packages/tools/src/definitions/company.ts create mode 100644 packages/tools/src/definitions/economic.ts create mode 100644 packages/tools/src/definitions/etf.ts create mode 100644 packages/tools/src/definitions/financial.ts create mode 100644 packages/tools/src/definitions/index.ts create mode 100644 packages/tools/src/definitions/insider.ts create mode 100644 packages/tools/src/definitions/institutional.ts create mode 100644 packages/tools/src/definitions/market.ts create mode 100644 packages/tools/src/definitions/quote.ts create mode 100644 packages/tools/src/definitions/senate-house.ts create mode 100644 packages/tools/src/definitions/stock.ts create mode 100644 packages/tools/src/definitions/types.ts delete mode 100644 packages/tools/src/providers/openai/calendar.ts delete mode 100644 packages/tools/src/providers/openai/company.ts delete mode 100644 packages/tools/src/providers/openai/economic.ts delete mode 100644 packages/tools/src/providers/openai/etf.ts delete mode 100644 packages/tools/src/providers/openai/financial.ts delete mode 100644 packages/tools/src/providers/openai/insider.ts delete mode 100644 packages/tools/src/providers/openai/institutional.ts delete mode 100644 packages/tools/src/providers/openai/market.ts delete mode 100644 packages/tools/src/providers/openai/quote.ts delete mode 100644 packages/tools/src/providers/openai/senate-house.ts delete mode 100644 packages/tools/src/providers/openai/stock.ts delete mode 100644 packages/tools/src/providers/vercel-ai/calendar.ts delete mode 100644 packages/tools/src/providers/vercel-ai/company.ts delete mode 100644 packages/tools/src/providers/vercel-ai/economic.ts delete mode 100644 packages/tools/src/providers/vercel-ai/etf.ts delete mode 100644 packages/tools/src/providers/vercel-ai/financial.ts delete mode 100644 packages/tools/src/providers/vercel-ai/insider.ts delete mode 100644 packages/tools/src/providers/vercel-ai/institutional.ts delete mode 100644 packages/tools/src/providers/vercel-ai/market.ts delete mode 100644 packages/tools/src/providers/vercel-ai/quote.ts delete mode 100644 packages/tools/src/providers/vercel-ai/senate-house.ts delete mode 100644 packages/tools/src/providers/vercel-ai/stock.ts diff --git a/.changeset/pre.json b/.changeset/pre.json index 0f1e396..f08d4d1 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -14,6 +14,7 @@ "loosen-tool-dep-ranges", "openai-bundler-safe", "tools-catch-thrown-errors", + "tools-shared-definitions", "zod4-v6-beta" ] } diff --git a/.changeset/tools-shared-definitions.md b/.changeset/tools-shared-definitions.md new file mode 100644 index 0000000..6b68b6e --- /dev/null +++ b/.changeset/tools-shared-definitions.md @@ -0,0 +1,5 @@ +--- +"fmp-ai-tools": patch +--- + +Internal refactor: each tool is now defined once in a shared `definitions/` layer, with the Vercel AI and OpenAI wrappers acting as thin per-provider adapters. The public API (tool names, subpath exports `fmp-ai-tools/vercel-ai` and `fmp-ai-tools/openai`, `fmpTools`, category groups, and individual tools) is unchanged. Minor drift between the two providers was canonicalized: numeric params use `z.number()` (OpenAI no longer string-coerces), and descriptions/validation use the richer variant. Adding a new provider is now a single adapter file. diff --git a/.cursor/rules/file-structure.mdc b/.cursor/rules/file-structure.mdc index f77fd36..95de2d2 100644 --- a/.cursor/rules/file-structure.mdc +++ b/.cursor/rules/file-structure.mdc @@ -123,10 +123,11 @@ fmp-node-wrapper/ - **`packages/tools/`**: AI tools for FMP API integrations (`fmp-ai-tools`) - AI tool implementations compatible with Vercel AI SDK, OpenAI, and more - - Provider-specific implementations (one file per endpoint category, aggregated in `index.ts`): - - `providers/vercel-ai/` - Vercel AI SDK tool providers - - `providers/openai/` - OpenAI tool providers - - Tool wrapper utilities and type definitions + - `definitions/` - each tool defined once (provider-agnostic `FMPToolDefinition`), one file per endpoint category, aggregated in `definitions/index.ts` + - Per-provider adapters wrap a definition into an SDK-specific tool (`utils/aisdk-tool-wrapper.ts`, `utils/openai-tool-wrapper.ts`) + - `providers/` - each provider's `index.ts` builds its public shape from the shared definitions via its adapter: + - `providers/vercel-ai/` - Vercel AI SDK (`ToolSet` object) + - `providers/openai/` - OpenAI Agents (`Tool[]` arrays) - Built with tsup for multiple output formats - Depends on `fmp-node-api` as a workspace dependency @@ -259,8 +260,9 @@ The API is organized into logical endpoint categories: The AI tools package provides integrations for various AI platforms: -- **Vercel AI SDK**: Tool providers exported from `fmp-ai-tools/vercel-ai` -- **OpenAI**: Tool providers exported from `fmp-ai-tools/openai` -- **Provider-specific implementations**: Organized by AI platform under `src/providers/` -- **Tool wrappers**: Utilities for creating and managing AI tools (`aisdk-tool-wrapper.ts`, `openai-tool-wrapper.ts`) -- **Type definitions**: Shared types for tool implementations +- **Shared definitions**: Each tool is defined once under `src/definitions/` (provider-agnostic `FMPToolDefinition`: name, description, Zod input schema, execute), one file per endpoint category. +- **Per-provider adapters**: Turn a definition into an SDK-specific tool (`utils/aisdk-tool-wrapper.ts` for Vercel AI, `utils/openai-tool-wrapper.ts` for OpenAI). +- **Providers**: Each `src/providers//index.ts` builds that provider's public shape from the shared definitions and exposes it on a subpath: + - **Vercel AI SDK**: `fmp-ai-tools/vercel-ai` (a `ToolSet` object) + - **OpenAI Agents**: `fmp-ai-tools/openai` (`Tool[]` arrays) +- **Adding a tool**: add one definition under `src/definitions/`, then a one-line export to each provider's `index.ts`. diff --git a/CLAUDE.md b/CLAUDE.md index 6bddf1e..cb20f8c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,15 @@ Inside `packages/api` and `packages/tools`, source uses the `@/` alias for `src/ ### AI tools (`packages/tools`) -Tools wrap the `fmp-node-api` client for LLM use. `getFMPClient()` (`src/client.ts`) just does `new FMP()` (env-var key). Tools are organized by provider under `src/providers/` (`vercel-ai/`, `openai/`), one file per endpoint category, aggregated in each provider's `index.ts`. The package exports per-provider subpaths: `fmp-ai-tools/vercel-ai` and `fmp-ai-tools/openai`. `fmpTools` is the combined `ToolSet` for the Vercel AI SDK. Provider files use `createTool` wrappers (`src/utils/aisdk-tool-wrapper.ts`, `openai-tool-wrapper.ts`) with Zod input schemas. +Tools wrap the `fmp-node-api` client for LLM use. `getFMPClient()` (`src/client.ts`) just does `new FMP()` (env-var key). + +**Each tool is defined once** in `src/definitions/.ts` as a provider-agnostic `FMPToolDefinition` (`{ name, description, inputSchema (Zod), execute }`); `execute` calls the `fmp-node-api` method and returns `toToolResponse(...)`. `src/definitions/index.ts` aggregates the per-category arrays into `allDefinitions`. + +**Per-provider adapters** turn a definition into an SDK-specific tool: `createTool` (`src/utils/aisdk-tool-wrapper.ts`) for the Vercel AI SDK and `createOpenAITool` (`src/utils/openai-tool-wrapper.ts`) for OpenAI Agents. Both add logging + error catching (`toToolError`); the OpenAI adapter also `inputSchema.parse()`s input (the Vercel SDK does that itself). + +Each provider's `index.ts` (`src/providers/vercel-ai/`, `src/providers/openai/`) maps the shared definitions through its adapter and rebuilds that provider's public shape — Vercel exposes a `ToolSet` object (`fmpTools`) + category objects; OpenAI exposes `Tool[]` arrays. Both also re-export every tool individually. The package exports per-provider subpaths `fmp-ai-tools/vercel-ai` and `fmp-ai-tools/openai`. + +To add a tool: add one `defineTool({...})` to the relevant `src/definitions/.ts`, then add a one-line individual export to each provider's `index.ts` (it flows into the category group and `fmpTools` automatically). Adding a new provider is one adapter + one `providers//index.ts`. ## Build & publish diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 068e3c5..bd49e2a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -176,6 +176,29 @@ describe('getNewEndpoint', () => { }); ``` +### Adding a New AI Tool (`fmp-ai-tools`) + +Tools are defined once, provider-agnostically, and adapted to each AI SDK (Vercel AI, OpenAI Agents). To add a tool: + +1. **Define the tool** in the relevant `packages/tools/src/definitions/.ts` (create the category file and register it in `definitions/index.ts` if new): + + ```typescript + defineTool({ + name: 'getNewTool', + description: 'Clear, model-facing description of what this returns', + inputSchema: z.object({ + symbol: z.string().min(1).describe('The stock symbol'), + }), + execute: async ({ symbol }) => toToolResponse(await getFMPClient().category.getNew(symbol)), + }); + ``` + +2. **Export it from each provider** — add one line to `packages/tools/src/providers/vercel-ai/index.ts` and `packages/tools/src/providers/openai/index.ts`. It flows into the category group and `fmpTools` automatically. +3. **Add a test** and run `pnpm --filter fmp-ai-tools test`. +4. **Update docs** — the Available Tools lists in `packages/tools/README.md` and `apps/docs`. + +Conventions: numeric params use `z.number()`; optional dates use `.optional().nullable()` and are coerced with `?? undefined`; `symbol`/`name` use `.min(1)`. Adding a brand-new provider is a single adapter (`src/utils/-tool-wrapper.ts`) plus a `src/providers//index.ts`. + ## Testing ### Test Structure diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index 499b6eb..9451f1e 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -1,5 +1,11 @@ # fmp-ai-tools +## 0.2.0-beta.5 + +### Patch Changes + +- Internal refactor: each tool is now defined once in a shared `definitions/` layer, with the Vercel AI and OpenAI wrappers acting as thin per-provider adapters. The public API (tool names, subpath exports `fmp-ai-tools/vercel-ai` and `fmp-ai-tools/openai`, `fmpTools`, category groups, and individual tools) is unchanged. Minor drift between the two providers was canonicalized: numeric params use `z.number()` (OpenAI no longer string-coerces), and descriptions/validation use the richer variant. Adding a new provider is now a single adapter file. + ## 0.2.0-beta.4 ### Patch Changes diff --git a/packages/tools/README.md b/packages/tools/README.md index 85b91b4..7afd8d7 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -348,6 +348,27 @@ const quote = await fmp.quote.getQuote('AAPL'); const profile = await fmp.company.getCompanyProfile('AAPL'); ``` +## Architecture + +Each tool is defined **once**, provider-agnostically, then adapted to each AI SDK: + +``` +src/definitions/.ts one FMPToolDefinition per tool: { name, description, inputSchema (Zod), execute } +src/utils/aisdk-tool-wrapper.ts createTool() → Vercel AI SDK tool +src/utils/openai-tool-wrapper.ts createOpenAITool() → OpenAI Agents tool +src/providers//index.ts builds that provider's public shape from the shared definitions +``` + +A definition's `execute` calls the `fmp-node-api` method and returns `toToolResponse(...)`, which formats the result (or a structured error) for the model. The adapters add logging and error catching; the OpenAI adapter also validates input against the Zod schema. + +### Adding a tool + +1. Add a `defineTool({ name, description, inputSchema, execute })` to the relevant `src/definitions/.ts` (or create a new category file and register it in `src/definitions/index.ts`). +2. Add a one-line individual export to each provider's `index.ts` (`src/providers/vercel-ai/index.ts` and `src/providers/openai/index.ts`). It flows into the category group and `fmpTools` automatically. +3. Add a test and run `pnpm --filter fmp-ai-tools test`. + +Adding a brand-new provider is a single adapter (`src/utils/-tool-wrapper.ts`) plus a `src/providers//index.ts`. + ## License MIT diff --git a/packages/tools/package.json b/packages/tools/package.json index bd48e04..e4aa83d 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.2.0-beta.4", + "version": "0.2.0-beta.5", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/tools/src/__tests__/core.test.ts b/packages/tools/src/__tests__/core.test.ts index 2400807..d030707 100644 --- a/packages/tools/src/__tests__/core.test.ts +++ b/packages/tools/src/__tests__/core.test.ts @@ -13,28 +13,34 @@ describe('FMP Tools', () => { describe('Vercel AI SDK Tools', () => { it('should have getStockQuote tool', () => { expect(fmpTools.getStockQuote).toBeDefined(); - expect(fmpTools.getStockQuote.description).toBe('Get the stock quote for a company'); + expect(fmpTools.getStockQuote.description).toBe( + 'Get the real-time stock quote for a company including price, volume, and market data', + ); }); it('should have getCompanyProfile tool', () => { expect(fmpTools.getCompanyProfile).toBeDefined(); - expect(fmpTools.getCompanyProfile.description).toBe('Get the company profile'); + expect(fmpTools.getCompanyProfile.description).toBe('Get the company profile for a company'); }); it('should have getBalanceSheet tool', () => { expect(fmpTools.getBalanceSheet).toBeDefined(); - expect(fmpTools.getBalanceSheet.description).toBe('Get balance sheet for a company'); + expect(fmpTools.getBalanceSheet.description).toBe( + 'Get balance sheet for a company showing assets, liabilities, and equity', + ); }); it('should have getIncomeStatement tool', () => { expect(fmpTools.getIncomeStatement).toBeDefined(); - expect(fmpTools.getIncomeStatement.description).toBe('Get income statement for a company'); + expect(fmpTools.getIncomeStatement.description).toBe( + 'Get income statement for a company showing revenue, expenses, and profit', + ); }); it('should have getCashFlowStatement tool', () => { expect(fmpTools.getCashFlowStatement).toBeDefined(); expect(fmpTools.getCashFlowStatement.description).toBe( - 'Get cash flow statement for a company', + 'Get cash flow statement for a company showing operating, investing, and financing cash flows', ); }); diff --git a/packages/tools/src/__tests__/definitions/consistency.test.ts b/packages/tools/src/__tests__/definitions/consistency.test.ts new file mode 100644 index 0000000..9de0c7d --- /dev/null +++ b/packages/tools/src/__tests__/definitions/consistency.test.ts @@ -0,0 +1,35 @@ +import { allDefinitions } from '@/definitions'; +import { fmpTools as vercelTools } from '@/providers/vercel-ai'; +import { fmpTools as openaiTools } from '@/providers/openai'; + +// Guards against the two providers drifting apart: both must expose exactly the +// tools declared once in the shared definitions, with matching descriptions. +describe('cross-provider consistency', () => { + const names = allDefinitions.map(d => d.name); + + it('has 37 uniquely-named definitions', () => { + expect(names.length).toBe(37); + expect(new Set(names).size).toBe(37); + }); + + it('Vercel AI exposes exactly the defined tools', () => { + expect(new Set(Object.keys(vercelTools))).toEqual(new Set(names)); + }); + + it('OpenAI exposes exactly the defined tools', () => { + const openaiNames = (openaiTools as Array<{ name: string }>).map(t => t.name); + expect(new Set(openaiNames)).toEqual(new Set(names)); + }); + + it('descriptions match across both providers and the definition', () => { + const openaiByName = new Map( + (openaiTools as Array<{ name: string; description: string }>).map(t => [t.name, t.description]), + ); + for (const def of allDefinitions) { + expect((vercelTools as Record)[def.name].description).toBe( + def.description, + ); + expect(openaiByName.get(def.name)).toBe(def.description); + } + }); +}); diff --git a/packages/tools/src/__tests__/definitions/definitions.test.ts b/packages/tools/src/__tests__/definitions/definitions.test.ts new file mode 100644 index 0000000..ffad38a --- /dev/null +++ b/packages/tools/src/__tests__/definitions/definitions.test.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +import { allDefinitions, financialDefinitions } from '@/definitions'; + +describe('tool definitions', () => { + it('every definition has a non-empty name, description, and Zod object schema', () => { + for (const def of allDefinitions) { + expect(typeof def.name).toBe('string'); + expect(def.name.length).toBeGreaterThan(0); + expect(def.description.length).toBeGreaterThan(0); + expect(def.inputSchema).toBeInstanceOf(z.ZodObject); + expect(typeof def.execute).toBe('function'); + } + }); + + it('numeric params are numbers with numeric defaults (not string-coerced)', () => { + const balanceSheet = financialDefinitions.find(d => d.name === 'getBalanceSheet')!; + const parsed = balanceSheet.inputSchema.parse({ symbol: 'AAPL' }); + expect(parsed).toEqual({ symbol: 'AAPL', period: 'annual', limit: 5 }); + expect(typeof parsed.limit).toBe('number'); + }); +}); diff --git a/packages/tools/src/__tests__/providers/openai/calendar.test.ts b/packages/tools/src/__tests__/providers/openai/calendar.test.ts index c891cc5..0d51f9a 100644 --- a/packages/tools/src/__tests__/providers/openai/calendar.test.ts +++ b/packages/tools/src/__tests__/providers/openai/calendar.test.ts @@ -1,4 +1,4 @@ -import { getEarningsCalendar, getEconomicCalendar } from '@/providers/openai/calendar'; +import { getEarningsCalendar, getEconomicCalendar } from '@/providers/openai'; const mockCalendar = { getEarningsCalendar: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/openai/company.test.ts b/packages/tools/src/__tests__/providers/openai/company.test.ts index 96a0104..bc12679 100644 --- a/packages/tools/src/__tests__/providers/openai/company.test.ts +++ b/packages/tools/src/__tests__/providers/openai/company.test.ts @@ -2,7 +2,7 @@ import { getCompanyProfile, getCompanySharesFloat, getCompanyExecutiveCompensation, -} from '@/providers/openai/company'; +} from '@/providers/openai'; const mockCompany = { getCompanyProfile: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/openai/economic.test.ts b/packages/tools/src/__tests__/providers/openai/economic.test.ts index 7ca676b..62ed006 100644 --- a/packages/tools/src/__tests__/providers/openai/economic.test.ts +++ b/packages/tools/src/__tests__/providers/openai/economic.test.ts @@ -1,4 +1,4 @@ -import { getTreasuryRates, getEconomicIndicators } from '@/providers/openai/economic'; +import { getTreasuryRates, getEconomicIndicators } from '@/providers/openai'; const mockEconomic = { getTreasuryRates: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/openai/etf.test.ts b/packages/tools/src/__tests__/providers/openai/etf.test.ts index df57b6e..57bbfca 100644 --- a/packages/tools/src/__tests__/providers/openai/etf.test.ts +++ b/packages/tools/src/__tests__/providers/openai/etf.test.ts @@ -1,4 +1,4 @@ -import { getETFHoldings, getETFProfile } from '@/providers/openai/etf'; +import { getETFHoldings, getETFProfile } from '@/providers/openai'; const mockETF = { getHoldings: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/openai/financial.test.ts b/packages/tools/src/__tests__/providers/openai/financial.test.ts index 78e9d6c..88ca93d 100644 --- a/packages/tools/src/__tests__/providers/openai/financial.test.ts +++ b/packages/tools/src/__tests__/providers/openai/financial.test.ts @@ -10,7 +10,7 @@ import { getBalanceSheetGrowth, getFinancialGrowth, getEarningsHistorical, -} from '@/providers/openai/financial'; +} from '@/providers/openai'; const mockFinancial = { getBalanceSheet: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/openai/insider.test.ts b/packages/tools/src/__tests__/providers/openai/insider.test.ts index eed0f64..c446777 100644 --- a/packages/tools/src/__tests__/providers/openai/insider.test.ts +++ b/packages/tools/src/__tests__/providers/openai/insider.test.ts @@ -1,4 +1,4 @@ -import { getInsiderTrading } from '@/providers/openai/insider'; +import { getInsiderTrading } from '@/providers/openai'; const mockInsider = { getInsiderTradesBySymbol: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/openai/institutional.test.ts b/packages/tools/src/__tests__/providers/openai/institutional.test.ts index ca38c74..c9f0cae 100644 --- a/packages/tools/src/__tests__/providers/openai/institutional.test.ts +++ b/packages/tools/src/__tests__/providers/openai/institutional.test.ts @@ -1,4 +1,4 @@ -import { getInstitutionalHolders } from '@/providers/openai/institutional'; +import { getInstitutionalHolders } from '@/providers/openai'; const mockInstitutional = { getInstitutionalHolders: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/openai/market.test.ts b/packages/tools/src/__tests__/providers/openai/market.test.ts index 76a4d89..a158651 100644 --- a/packages/tools/src/__tests__/providers/openai/market.test.ts +++ b/packages/tools/src/__tests__/providers/openai/market.test.ts @@ -4,7 +4,7 @@ import { getGainers, getLosers, getMostActive, -} from '@/providers/openai/market'; +} from '@/providers/openai'; const mockMarket = { getMarketPerformance: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/openai/quote.test.ts b/packages/tools/src/__tests__/providers/openai/quote.test.ts index 7cfca89..aec6919 100644 --- a/packages/tools/src/__tests__/providers/openai/quote.test.ts +++ b/packages/tools/src/__tests__/providers/openai/quote.test.ts @@ -1,4 +1,4 @@ -import { getStockQuote } from '@/providers/openai/quote'; +import { getStockQuote } from '@/providers/openai'; const mockQuote = { getQuote: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/openai/senate-house.test.ts b/packages/tools/src/__tests__/providers/openai/senate-house.test.ts index d74d9c8..110e1ff 100644 --- a/packages/tools/src/__tests__/providers/openai/senate-house.test.ts +++ b/packages/tools/src/__tests__/providers/openai/senate-house.test.ts @@ -5,7 +5,7 @@ import { getHouseTradingByName, getSenateTradingRSSFeed, getHouseTradingRSSFeed, -} from '@/providers/openai/senate-house'; +} from '@/providers/openai'; const mockSenateHouse = { getSenateTrading: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/openai/stock.test.ts b/packages/tools/src/__tests__/providers/openai/stock.test.ts index ab8f4e7..718c8c6 100644 --- a/packages/tools/src/__tests__/providers/openai/stock.test.ts +++ b/packages/tools/src/__tests__/providers/openai/stock.test.ts @@ -1,4 +1,4 @@ -import { getMarketCap, getStockSplits, getDividendHistory } from '@/providers/openai/stock'; +import { getMarketCap, getStockSplits, getDividendHistory } from '@/providers/openai'; const mockStock = { getMarketCap: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/calendar.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/calendar.test.ts index 34b44fa..9dd7381 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/calendar.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/calendar.test.ts @@ -1,4 +1,4 @@ -import { calendarTools } from '@/providers/vercel-ai/calendar'; +import { calendarTools } from '@/providers/vercel-ai'; const mockCalendar = { getEarningsCalendar: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/company.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/company.test.ts index 0342645..d593bf0 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/company.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/company.test.ts @@ -1,4 +1,4 @@ -import { companyTools } from '@/providers/vercel-ai/company'; +import { companyTools } from '@/providers/vercel-ai'; const mockCompany = { getCompanyProfile: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/economic.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/economic.test.ts index ef00fd6..8b2aa80 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/economic.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/economic.test.ts @@ -1,4 +1,4 @@ -import { economicTools } from '@/providers/vercel-ai/economic'; +import { economicTools } from '@/providers/vercel-ai'; const mockEconomic = { getTreasuryRates: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/etf.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/etf.test.ts index baf0846..8262400 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/etf.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/etf.test.ts @@ -1,4 +1,4 @@ -import { etfTools } from '@/providers/vercel-ai/etf'; +import { etfTools } from '@/providers/vercel-ai'; const mockETF = { getHoldings: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/financial.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/financial.test.ts index ccb6e27..d64888d 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/financial.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/financial.test.ts @@ -1,4 +1,4 @@ -import { financialTools } from '@/providers/vercel-ai/financial'; +import { financialTools } from '@/providers/vercel-ai'; const mockFinancial = { getBalanceSheet: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/insider.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/insider.test.ts index e0b5650..1a97d2f 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/insider.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/insider.test.ts @@ -1,4 +1,4 @@ -import { insiderTools } from '@/providers/vercel-ai/insider'; +import { insiderTools } from '@/providers/vercel-ai'; const mockInsider = { getInsiderTradesBySymbol: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/institutional.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/institutional.test.ts index 1558ba3..73e28a7 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/institutional.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/institutional.test.ts @@ -1,4 +1,4 @@ -import { institutionalTools } from '@/providers/vercel-ai/institutional'; +import { institutionalTools } from '@/providers/vercel-ai'; const mockInstitutional = { getInstitutionalHolders: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/market.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/market.test.ts index 1bad555..5cbc87d 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/market.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/market.test.ts @@ -1,4 +1,4 @@ -import { marketTools } from '@/providers/vercel-ai/market'; +import { marketTools } from '@/providers/vercel-ai'; const mockMarket = { getMarketPerformance: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/quote.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/quote.test.ts index d686e9e..037ef4d 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/quote.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/quote.test.ts @@ -1,4 +1,4 @@ -import { quoteTools } from '@/providers/vercel-ai/quote'; +import { quoteTools } from '@/providers/vercel-ai'; const mockQuote = { getQuote: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/senate-house.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/senate-house.test.ts index 16d8b3f..bfbdcfe 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/senate-house.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/senate-house.test.ts @@ -1,4 +1,4 @@ -import { senateHouseTools } from '@/providers/vercel-ai/senate-house'; +import { senateHouseTools } from '@/providers/vercel-ai'; const mockSenateHouse = { getSenateTrading: jest.fn(), diff --git a/packages/tools/src/__tests__/providers/vercel-ai/stock.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/stock.test.ts index dfe14c8..843a96b 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/stock.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/stock.test.ts @@ -1,4 +1,4 @@ -import { stockTools } from '@/providers/vercel-ai/stock'; +import { stockTools } from '@/providers/vercel-ai'; const mockStock = { getMarketCap: jest.fn(), diff --git a/packages/tools/src/definitions/calendar.ts b/packages/tools/src/definitions/calendar.ts new file mode 100644 index 0000000..d0103b4 --- /dev/null +++ b/packages/tools/src/definitions/calendar.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +const dateRangeSchema = z.object({ + from: z.string().optional().nullable().describe('Start date in YYYY-MM-DD format (optional)'), + to: z.string().optional().nullable().describe('End date in YYYY-MM-DD format (optional)'), +}); + +export const calendarDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getEarningsCalendar', + description: 'Get earnings calendar showing upcoming and recent earnings announcements', + inputSchema: dateRangeSchema, + execute: async ({ from, to }) => + toToolResponse( + await getFMPClient().calendar.getEarningsCalendar({ + from: from ?? undefined, + to: to ?? undefined, + }), + ), + }), + defineTool({ + name: 'getEconomicCalendar', + description: 'Get economic calendar showing upcoming and recent economic events and indicators', + inputSchema: dateRangeSchema, + execute: async ({ from, to }) => + toToolResponse( + await getFMPClient().calendar.getEconomicsCalendar({ + from: from ?? undefined, + to: to ?? undefined, + }), + ), + }), +]; diff --git a/packages/tools/src/definitions/company.ts b/packages/tools/src/definitions/company.ts new file mode 100644 index 0000000..075da9b --- /dev/null +++ b/packages/tools/src/definitions/company.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +const symbolSchema = z.object({ + symbol: z + .string() + .min(1, 'Stock symbol is required') + .describe('The stock symbol (e.g., AAPL, MSFT, GOOGL)'), +}); + +export const companyDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getCompanyProfile', + description: 'Get the company profile for a company', + inputSchema: symbolSchema, + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().company.getCompanyProfile(symbol)), + }), + defineTool({ + name: 'getCompanySharesFloat', + description: 'Get the company shares float', + inputSchema: symbolSchema, + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().company.getSharesFloat(symbol)), + }), + defineTool({ + name: 'getCompanyExecutiveCompensation', + description: 'Get the company executive compensation', + inputSchema: symbolSchema, + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().company.getExecutiveCompensation(symbol)), + }), +]; diff --git a/packages/tools/src/definitions/economic.ts b/packages/tools/src/definitions/economic.ts new file mode 100644 index 0000000..575edf6 --- /dev/null +++ b/packages/tools/src/definitions/economic.ts @@ -0,0 +1,65 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +const economicIndicatorNames = [ + 'GDP', + 'realGDP', + 'nominalPotentialGDP', + 'realGDPPerCapita', + 'federalFunds', + 'CPI', + 'inflationRate', + 'inflation', + 'retailSales', + 'consumerSentiment', + 'durableGoods', + 'unemploymentRate', + 'totalNonfarmPayroll', + 'initialClaims', + 'industrialProductionTotalIndex', + 'newPrivatelyOwnedHousingUnitsStartedTotalUnits', + 'totalVehicleSales', + 'retailMoneyFunds', + 'smoothedUSRecessionProbabilities', + '3MonthOr90DayRatesAndYieldsCertificatesOfDeposit', + 'commercialBankInterestRateOnCreditCardPlansAllAccounts', + '30YearFixedRateMortgageAverage', + '15YearFixedRateMortgageAverage', +] as const; + +export const economicDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getTreasuryRates', + description: 'Get treasury rates for different maturities over time', + inputSchema: z.object({ + from: z.string().optional().nullable().describe('Start date in YYYY-MM-DD format (optional)'), + to: z.string().optional().nullable().describe('End date in YYYY-MM-DD format (optional)'), + }), + execute: async ({ from, to }) => + toToolResponse( + await getFMPClient().economic.getTreasuryRates({ + from: from ?? undefined, + to: to ?? undefined, + }), + ), + }), + defineTool({ + name: 'getEconomicIndicators', + description: 'Get economic indicators like GDP, unemployment rate, inflation, etc.', + inputSchema: z.object({ + name: z.enum(economicIndicatorNames).describe('The name of the economic indicator'), + from: z.string().optional().nullable().describe('Start date in YYYY-MM-DD format (optional)'), + to: z.string().optional().nullable().describe('End date in YYYY-MM-DD format (optional)'), + }), + execute: async ({ name, from, to }) => + toToolResponse( + await getFMPClient().economic.getEconomicIndicators({ + name, + from: from ?? undefined, + to: to ?? undefined, + }), + ), + }), +]; diff --git a/packages/tools/src/definitions/etf.ts b/packages/tools/src/definitions/etf.ts new file mode 100644 index 0000000..b038545 --- /dev/null +++ b/packages/tools/src/definitions/etf.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +export const etfDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getETFHoldings', + description: + 'Get ETF holdings for a specific ETF symbol, showing the underlying assets and their weights', + inputSchema: z.object({ + symbol: z + .string() + .min(1, 'ETF symbol is required') + .describe('ETF symbol (e.g., SPY, QQQ, VTI)'), + date: z + .string() + .optional() + .nullable() + .describe('Date for holdings in YYYY-MM-DD format (optional)'), + }), + execute: async ({ symbol, date }) => { + // `date` is optional; the holdings params type treats it as required, so widen. + const params: any = { symbol }; + if (date) { + params.date = date; + } + return toToolResponse(await getFMPClient().etf.getHoldings(params)); + }, + }), + defineTool({ + name: 'getETFProfile', + description: 'Get ETF profile information including fund details, expense ratio, and key metrics', + inputSchema: z.object({ + symbol: z + .string() + .min(1, 'ETF symbol is required') + .describe('ETF symbol (e.g., SPY, QQQ, VTI)'), + }), + execute: async ({ symbol }) => toToolResponse(await getFMPClient().etf.getProfile(symbol)), + }), +]; diff --git a/packages/tools/src/definitions/financial.ts b/packages/tools/src/definitions/financial.ts new file mode 100644 index 0000000..3d8ac26 --- /dev/null +++ b/packages/tools/src/definitions/financial.ts @@ -0,0 +1,100 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +const period = z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'); + +const symbol = (what: string) => z.string().min(1).describe(`The stock symbol to get ${what} for`); + +const limit = z.number().default(5).describe('The number of periods to retrieve'); + +/** Build a standard statement tool: symbol + period + limit -> fmp.financial[method]. */ +const statementTool = ( + name: string, + description: string, + what: string, + call: (args: { symbol: string; period: 'annual' | 'quarter'; limit: number }) => Promise, +) => + defineTool({ + name, + description, + inputSchema: z.object({ symbol: symbol(what), period, limit }), + execute: async ({ symbol, period, limit }) => toToolResponse(await call({ symbol, period, limit })), + }); + +export const financialDefinitions: FMPToolDefinition[] = [ + statementTool( + 'getBalanceSheet', + 'Get balance sheet for a company showing assets, liabilities, and equity', + 'balance sheet', + args => getFMPClient().financial.getBalanceSheet(args), + ), + statementTool( + 'getIncomeStatement', + 'Get income statement for a company showing revenue, expenses, and profit', + 'income statement', + args => getFMPClient().financial.getIncomeStatement(args), + ), + statementTool( + 'getCashFlowStatement', + 'Get cash flow statement for a company showing operating, investing, and financing cash flows', + 'cash flow statement', + args => getFMPClient().financial.getCashFlowStatement(args), + ), + statementTool( + 'getKeyMetrics', + 'Get key metrics for a company', + 'key metrics', + args => getFMPClient().financial.getKeyMetrics(args), + ), + statementTool( + 'getFinancialRatios', + 'Get financial ratios for a company including profitability, liquidity, and efficiency metrics', + 'financial ratios', + args => getFMPClient().financial.getFinancialRatios(args), + ), + statementTool( + 'getEnterpriseValue', + 'Get enterprise value for a company', + 'enterprise value', + args => getFMPClient().financial.getEnterpriseValue(args), + ), + statementTool( + 'getCashflowGrowth', + 'Get cashflow growth for a company', + 'cashflow growth', + args => getFMPClient().financial.getCashflowGrowth(args), + ), + statementTool( + 'getIncomeGrowth', + 'Get income growth for a company', + 'income growth', + args => getFMPClient().financial.getIncomeGrowth(args), + ), + statementTool( + 'getBalanceSheetGrowth', + 'Get balance sheet growth for a company', + 'balance sheet growth', + args => getFMPClient().financial.getBalanceSheetGrowth(args), + ), + statementTool( + 'getFinancialGrowth', + 'Get financial growth for a company', + 'financial growth', + args => getFMPClient().financial.getFinancialGrowth(args), + ), + defineTool({ + name: 'getEarningsHistorical', + description: 'Get earnings historical for a company', + inputSchema: z.object({ + symbol: symbol('earnings historical'), + limit: z.number().default(10).describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, limit }) => + toToolResponse(await getFMPClient().financial.getEarningsHistorical({ symbol, limit })), + }), +]; diff --git a/packages/tools/src/definitions/index.ts b/packages/tools/src/definitions/index.ts new file mode 100644 index 0000000..58a9a47 --- /dev/null +++ b/packages/tools/src/definitions/index.ts @@ -0,0 +1,43 @@ +import { quoteDefinitions } from './quote'; +import { companyDefinitions } from './company'; +import { financialDefinitions } from './financial'; +import { calendarDefinitions } from './calendar'; +import { economicDefinitions } from './economic'; +import { etfDefinitions } from './etf'; +import { insiderDefinitions } from './insider'; +import { institutionalDefinitions } from './institutional'; +import { marketDefinitions } from './market'; +import { senateHouseDefinitions } from './senate-house'; +import { stockDefinitions } from './stock'; +import type { FMPToolDefinition } from './types'; + +export * from './types'; + +export { + quoteDefinitions, + companyDefinitions, + financialDefinitions, + calendarDefinitions, + economicDefinitions, + etfDefinitions, + insiderDefinitions, + institutionalDefinitions, + marketDefinitions, + senateHouseDefinitions, + stockDefinitions, +}; + +/** Every tool definition, in a stable order shared by all providers. */ +export const allDefinitions: FMPToolDefinition[] = [ + ...quoteDefinitions, + ...companyDefinitions, + ...financialDefinitions, + ...calendarDefinitions, + ...economicDefinitions, + ...etfDefinitions, + ...insiderDefinitions, + ...institutionalDefinitions, + ...marketDefinitions, + ...senateHouseDefinitions, + ...stockDefinitions, +]; diff --git a/packages/tools/src/definitions/insider.ts b/packages/tools/src/definitions/insider.ts new file mode 100644 index 0000000..ffbfba8 --- /dev/null +++ b/packages/tools/src/definitions/insider.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +export const insiderDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getInsiderTrading', + description: + 'Get insider trading data for a specific stock symbol showing buy/sell transactions by company insiders', + inputSchema: z.object({ + symbol: z + .string() + .min(1, 'Stock symbol is required') + .describe('Stock symbol (e.g., AAPL, MSFT, GOOGL)'), + page: z + .number() + .int() + .min(0) + .default(0) + .describe('Page number for pagination (optional, defaults to 0)'), + }), + execute: async ({ symbol, page }) => + toToolResponse(await getFMPClient().insider.getInsiderTradesBySymbol(symbol, page)), + }), +]; diff --git a/packages/tools/src/definitions/institutional.ts b/packages/tools/src/definitions/institutional.ts new file mode 100644 index 0000000..945edd3 --- /dev/null +++ b/packages/tools/src/definitions/institutional.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +export const institutionalDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getInstitutionalHolders', + description: + 'Get institutional holders for a specific stock symbol showing which institutions own shares and their holdings', + inputSchema: z.object({ + symbol: z + .string() + .min(1, 'Stock symbol is required') + .describe('Stock symbol (e.g., AAPL, MSFT, GOOGL)'), + }), + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().institutional.getInstitutionalHolders({ symbol })), + }), +]; diff --git a/packages/tools/src/definitions/market.ts b/packages/tools/src/definitions/market.ts new file mode 100644 index 0000000..2edfbf8 --- /dev/null +++ b/packages/tools/src/definitions/market.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +const empty = z.object({}); + +export const marketDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getMarketPerformance', + description: 'Get overall market performance data including major indices performance', + inputSchema: empty, + execute: async () => toToolResponse(await getFMPClient().market.getMarketPerformance()), + }), + defineTool({ + name: 'getSectorPerformance', + description: 'Get sector performance data showing how different market sectors are performing', + inputSchema: empty, + execute: async () => toToolResponse(await getFMPClient().market.getSectorPerformance()), + }), + defineTool({ + name: 'getGainers', + description: 'Get top gaining stocks showing the best performing stocks of the day', + inputSchema: empty, + execute: async () => toToolResponse(await getFMPClient().market.getGainers()), + }), + defineTool({ + name: 'getLosers', + description: 'Get top losing stocks showing the worst performing stocks of the day', + inputSchema: empty, + execute: async () => toToolResponse(await getFMPClient().market.getLosers()), + }), + defineTool({ + name: 'getMostActive', + description: 'Get most active stocks showing stocks with the highest trading volume', + inputSchema: empty, + execute: async () => toToolResponse(await getFMPClient().market.getMostActive()), + }), +]; diff --git a/packages/tools/src/definitions/quote.ts b/packages/tools/src/definitions/quote.ts new file mode 100644 index 0000000..47f8f66 --- /dev/null +++ b/packages/tools/src/definitions/quote.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +export const quoteDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getStockQuote', + description: + 'Get the real-time stock quote for a company including price, volume, and market data', + inputSchema: z.object({ + symbol: z + .string() + .min(1, 'Stock symbol is required') + .describe('The symbol of the company to get the stock quote for'), + }), + execute: async ({ symbol }) => toToolResponse(await getFMPClient().quote.getQuote(symbol)), + }), +]; diff --git a/packages/tools/src/definitions/senate-house.ts b/packages/tools/src/definitions/senate-house.ts new file mode 100644 index 0000000..d5c626a --- /dev/null +++ b/packages/tools/src/definitions/senate-house.ts @@ -0,0 +1,84 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +const symbolSchema = z.object({ + symbol: z + .string() + .min(1, 'Stock symbol is required') + .describe('Stock symbol (e.g., AAPL, MSFT, GOOGL)'), +}); + +const rssFeedSchema = z.object({ + page: z + .number() + .int() + .min(0) + .default(0) + .describe('Page number for pagination (optional, defaults to 0)'), +}); + +export const senateHouseDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getSenateTrading', + description: 'Get senate trading data for a specific stock symbol showing senator transactions', + inputSchema: symbolSchema, + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().senateHouse.getSenateTrading({ symbol })), + }), + defineTool({ + name: 'getHouseTrading', + description: + 'Get house trading data for a specific stock symbol showing representative transactions', + inputSchema: symbolSchema, + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().senateHouse.getHouseTrading({ symbol })), + }), + defineTool({ + name: 'getSenateTradingByName', + description: + 'Get senate trading data for a specific senator by name showing their trading activity', + inputSchema: z.object({ + name: z + .string() + .min(1, 'Name is required') + .describe('The name of the senator to get trading data for'), + }), + execute: async ({ name }) => + toToolResponse(await getFMPClient().senateHouse.getSenateTradingByName({ name })), + }), + defineTool({ + name: 'getHouseTradingByName', + description: + 'Get house trading data for a specific representative by name showing their trading activity', + inputSchema: z.object({ + name: z + .string() + .min(1, 'Name is required') + .describe('The name of the representative to get trading data for'), + }), + execute: async ({ name }) => + toToolResponse(await getFMPClient().senateHouse.getHouseTradingByName({ name })), + }), + defineTool({ + name: 'getSenateTradingRSSFeed', + description: + 'Get senate trading data through RSS feed with pagination showing recent senate transactions', + inputSchema: rssFeedSchema, + // JS default mirrors the Zod default for the Vercel adapter, which (unlike the + // OpenAI adapter) does not parse input before calling execute. + execute: async ({ page = 0 }) => + toToolResponse(await getFMPClient().senateHouse.getSenateTradingRSSFeed({ page })), + }), + defineTool({ + name: 'getHouseTradingRSSFeed', + description: + 'Get house trading data through RSS feed with pagination showing recent house transactions', + inputSchema: rssFeedSchema, + // JS default mirrors the Zod default for the Vercel adapter, which (unlike the + // OpenAI adapter) does not parse input before calling execute. + execute: async ({ page = 0 }) => + toToolResponse(await getFMPClient().senateHouse.getHouseTradingRSSFeed({ page })), + }), +]; diff --git a/packages/tools/src/definitions/stock.ts b/packages/tools/src/definitions/stock.ts new file mode 100644 index 0000000..cc3d800 --- /dev/null +++ b/packages/tools/src/definitions/stock.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +const symbolSchema = z.object({ + symbol: z + .string() + .min(1, 'Stock symbol is required') + .describe('The stock symbol (e.g., AAPL, MSFT, GOOGL)'), +}); + +export const stockDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getMarketCap', + description: 'Get market capitalization for a company showing current market value', + inputSchema: symbolSchema, + execute: async ({ symbol }) => toToolResponse(await getFMPClient().stock.getMarketCap(symbol)), + }), + defineTool({ + name: 'getStockSplits', + description: 'Get stock splits history for a company showing all historical stock split events', + inputSchema: symbolSchema, + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().stock.getStockSplits(symbol)), + }), + defineTool({ + name: 'getDividendHistory', + description: 'Get dividend history for a company showing all historical dividend payments', + inputSchema: symbolSchema, + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().stock.getDividendHistory(symbol)), + }), +]; diff --git a/packages/tools/src/definitions/types.ts b/packages/tools/src/definitions/types.ts new file mode 100644 index 0000000..894fbac --- /dev/null +++ b/packages/tools/src/definitions/types.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +/** + * Provider-agnostic definition of an FMP tool: its name, model-facing description, + * Zod input schema, and the execute body (which returns the string the model receives, + * already run through `toToolResponse`). + * + * Per-provider adapters (`createTool` for Vercel AI, `createOpenAITool` for OpenAI Agents) + * turn a definition into an SDK-specific tool. Defining each tool once here keeps the + * providers in sync and makes adding a new provider a single adapter file. + */ +export interface FMPToolDefinition { + name: string; + description: string; + inputSchema: z.ZodObject; + execute: (args: any) => Promise; +} + +/** + * Identity helper that preserves per-tool inference of `execute`'s args from the schema + * at the definition site, while widening the result to `FMPToolDefinition` so definitions + * can be collected into a single array. + */ +export function defineTool>(def: { + name: string; + description: string; + inputSchema: T; + execute: (args: z.infer) => Promise; +}): FMPToolDefinition { + return def as FMPToolDefinition; +} diff --git a/packages/tools/src/providers/openai/calendar.ts b/packages/tools/src/providers/openai/calendar.ts deleted file mode 100644 index a14d33d..0000000 --- a/packages/tools/src/providers/openai/calendar.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -// Common input schema for calendar date range -const calendarInputSchema = z.object({ - from: z.string().optional().nullable().describe('Start date in YYYY-MM-DD format (optional)'), - to: z.string().optional().nullable().describe('End date in YYYY-MM-DD format (optional)'), -}); - -export const getEarningsCalendar = createOpenAITool({ - name: 'getEarningsCalendar', - description: 'Get earnings calendar showing upcoming and recent earnings announcements', - inputSchema: calendarInputSchema, - execute: async ({ from, to }) => { - const fmp = getFMPClient(); - const earningsCalendar = await fmp.calendar.getEarningsCalendar({ - from: from ?? undefined, - to: to ?? undefined, - }); - return toToolResponse(earningsCalendar); - }, -}); - -export const getEconomicCalendar = createOpenAITool({ - name: 'getEconomicCalendar', - description: 'Get economic calendar showing upcoming and recent economic events and indicators', - inputSchema: calendarInputSchema, - execute: async ({ from, to }) => { - const fmp = getFMPClient(); - const economicCalendar = await fmp.calendar.getEconomicsCalendar({ - from: from ?? undefined, - to: to ?? undefined, - }); - return toToolResponse(economicCalendar); - }, -}); diff --git a/packages/tools/src/providers/openai/company.ts b/packages/tools/src/providers/openai/company.ts deleted file mode 100644 index fc4a1f3..0000000 --- a/packages/tools/src/providers/openai/company.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -export const getCompanyProfile = createOpenAITool({ - name: 'getCompanyProfile', - description: 'Get the company profile for a company', - inputSchema: z.object({ - symbol: z - .string() - .min(1, 'Stock symbol is required') - .describe('The stock symbol (e.g., AAPL, MSFT, GOOGL)'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const companyProfile = await fmp.company.getCompanyProfile(symbol); - return toToolResponse(companyProfile); - }, -}); - -export const getCompanySharesFloat = createOpenAITool({ - name: 'getCompanySharesFloat', - description: 'Get the company shares float', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol (e.g., AAPL, MSFT, GOOGL)'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const companySharesFloat = await fmp.company.getSharesFloat(symbol); - return toToolResponse(companySharesFloat); - }, -}); - -export const getCompanyExecutiveCompensation = createOpenAITool({ - name: 'getCompanyExecutiveCompensation', - description: 'Get the company executive compensation', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol (e.g., AAPL, MSFT, GOOGL)'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const companyExecutiveCompensation = await fmp.company.getExecutiveCompensation(symbol); - return toToolResponse(companyExecutiveCompensation); - }, -}); diff --git a/packages/tools/src/providers/openai/economic.ts b/packages/tools/src/providers/openai/economic.ts deleted file mode 100644 index a6ae083..0000000 --- a/packages/tools/src/providers/openai/economic.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -// Input schema for treasury rates with date range -const treasuryRatesInputSchema = z.object({ - from: z.string().optional().nullable().describe('Start date in YYYY-MM-DD format (optional)'), - to: z.string().optional().nullable().describe('End date in YYYY-MM-DD format (optional)'), -}); - -// Economic indicators enum and schema -const economicIndicatorNames = [ - 'GDP', - 'realGDP', - 'nominalPotentialGDP', - 'realGDPPerCapita', - 'federalFunds', - 'CPI', - 'inflationRate', - 'inflation', - 'retailSales', - 'consumerSentiment', - 'durableGoods', - 'unemploymentRate', - 'totalNonfarmPayroll', - 'initialClaims', - 'industrialProductionTotalIndex', - 'newPrivatelyOwnedHousingUnitsStartedTotalUnits', - 'totalVehicleSales', - 'retailMoneyFunds', - 'smoothedUSRecessionProbabilities', - '3MonthOr90DayRatesAndYieldsCertificatesOfDeposit', - 'commercialBankInterestRateOnCreditCardPlansAllAccounts', - '30YearFixedRateMortgageAverage', - '15YearFixedRateMortgageAverage', -] as const; - -const economicIndicatorsInputSchema = z.object({ - name: z.enum(economicIndicatorNames).describe('The name of the economic indicator'), - from: z.string().optional().nullable().describe('Start date in YYYY-MM-DD format (optional)'), - to: z.string().optional().nullable().describe('End date in YYYY-MM-DD format (optional)'), -}); - -export const getTreasuryRates = createOpenAITool({ - name: 'getTreasuryRates', - description: 'Get treasury rates for different maturities over time', - inputSchema: treasuryRatesInputSchema, - execute: async ({ from, to }) => { - const fmp = getFMPClient(); - const treasuryRates = await fmp.economic.getTreasuryRates({ - from: from ?? undefined, - to: to ?? undefined, - }); - return toToolResponse(treasuryRates); - }, -}); - -export const getEconomicIndicators = createOpenAITool({ - name: 'getEconomicIndicators', - description: 'Get economic indicators like GDP, unemployment rate, inflation, etc.', - inputSchema: economicIndicatorsInputSchema, - execute: async ({ name, from, to }) => { - const fmp = getFMPClient(); - const economicIndicators = await fmp.economic.getEconomicIndicators({ - name, - from: from ?? undefined, - to: to ?? undefined, - }); - return toToolResponse(economicIndicators); - }, -}); diff --git a/packages/tools/src/providers/openai/etf.ts b/packages/tools/src/providers/openai/etf.ts deleted file mode 100644 index d84b2ba..0000000 --- a/packages/tools/src/providers/openai/etf.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -// Input schema for ETF holdings with optional date -const etfHoldingsInputSchema = z.object({ - symbol: z.string().min(1, 'ETF symbol is required').describe('ETF symbol (e.g., SPY, QQQ, VTI)'), - date: z - .string() - .optional() - .nullable() - .describe('Date for holdings in YYYY-MM-DD format (optional)'), -}); - -// Input schema for ETF profile -const etfProfileInputSchema = z.object({ - symbol: z.string().min(1, 'ETF symbol is required').describe('ETF symbol (e.g., SPY, QQQ, VTI)'), -}); - -export const getETFHoldings = createOpenAITool({ - name: 'getETFHoldings', - description: - 'Get ETF holdings for a specific ETF symbol, showing the underlying assets and their weights', - inputSchema: etfHoldingsInputSchema, - execute: async ({ symbol, date }) => { - const fmp = getFMPClient(); - const params: any = { symbol }; - if (date) { - params.date = date; - } - - const etfHoldings = await fmp.etf.getHoldings(params); - return toToolResponse(etfHoldings); - }, -}); - -export const getETFProfile = createOpenAITool({ - name: 'getETFProfile', - description: 'Get ETF profile information including fund details, expense ratio, and key metrics', - inputSchema: etfProfileInputSchema, - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const etfProfile = await fmp.etf.getProfile(symbol); - return toToolResponse(etfProfile); - }, -}); diff --git a/packages/tools/src/providers/openai/financial.ts b/packages/tools/src/providers/openai/financial.ts deleted file mode 100644 index daa2531..0000000 --- a/packages/tools/src/providers/openai/financial.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -export const getBalanceSheet = createOpenAITool({ - name: 'getBalanceSheet', - description: 'Get balance sheet for a company showing assets, liabilities, and equity', - 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, - limit: Number(limit), - }); - return toToolResponse(balanceSheet); - }, -}); - -export const getIncomeStatement = createOpenAITool({ - name: 'getIncomeStatement', - description: 'Get income statement for a company showing revenue, expenses, and profit', - 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, - limit: Number(limit), - }); - return toToolResponse(incomeStatement); - }, -}); - -export const getCashFlowStatement = createOpenAITool({ - name: 'getCashFlowStatement', - description: - 'Get cash flow statement for a company showing operating, investing, and financing cash flows', - 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, - limit: Number(limit), - }); - return toToolResponse(cashFlowStatement); - }, -}); - -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 toToolResponse(keyMetrics); - }, -}); - -export const getFinancialRatios = createOpenAITool({ - name: 'getFinancialRatios', - description: - 'Get financial ratios for a company including profitability, liquidity, and efficiency metrics', - 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, - limit: Number(limit), - }); - return toToolResponse(financialRatios); - }, -}); - -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 toToolResponse(enterpriseValue); - }, -}); - -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 toToolResponse(cashflowGrowth); - }, -}); - -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 toToolResponse(incomeGrowth); - }, -}); - -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 toToolResponse(balanceSheetGrowth); - }, -}); - -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 toToolResponse(financialGrowth); - }, -}); - -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 toToolResponse(earningsHistorical); - }, -}); diff --git a/packages/tools/src/providers/openai/index.ts b/packages/tools/src/providers/openai/index.ts index 74a7bd4..58c1c22 100644 --- a/packages/tools/src/providers/openai/index.ts +++ b/packages/tools/src/providers/openai/index.ts @@ -1,165 +1,80 @@ import type { Tool } from '@openai/agents'; +import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { - getCompanyProfile, - getCompanySharesFloat, - getCompanyExecutiveCompensation, -} from './company'; -import { getEarningsCalendar, getEconomicCalendar } from './calendar'; -import { getTreasuryRates, getEconomicIndicators } from './economic'; -import { getETFHoldings, getETFProfile } from './etf'; -import { - getBalanceSheet, - getIncomeStatement, - getCashFlowStatement, - getKeyMetrics, - getFinancialRatios, - getEnterpriseValue, - getCashflowGrowth, - getIncomeGrowth, - getBalanceSheetGrowth, - getFinancialGrowth, - getEarningsHistorical, -} from './financial'; -import { getInsiderTrading } from './insider'; -import { getInstitutionalHolders } from './institutional'; -import { - getMarketPerformance, - getSectorPerformance, - getGainers, - getLosers, - getMostActive, -} from './market'; -import { getStockQuote } from './quote'; -import { - getSenateTrading, - getHouseTrading, - getSenateTradingByName, - getHouseTradingByName, - getSenateTradingRSSFeed, - getHouseTradingRSSFeed, -} from './senate-house'; -import { getMarketCap, getStockSplits, getDividendHistory } from './stock'; + allDefinitions, + quoteDefinitions, + companyDefinitions, + financialDefinitions, + calendarDefinitions, + economicDefinitions, + etfDefinitions, + insiderDefinitions, + institutionalDefinitions, + marketDefinitions, + senateHouseDefinitions, + stockDefinitions, + type FMPToolDefinition, +} from '@/definitions'; + +// One OpenAI Agents tool instance per definition, addressable by name. The same +// instances are reused for the individual exports and the category arrays below. +const byName: Record> = Object.fromEntries( + allDefinitions.map(def => [def.name, createOpenAITool(def) as Tool]), +); + +const pick = (defs: FMPToolDefinition[]): Tool[] => defs.map(def => byName[def.name]); -// Export individual tools for OpenAI agents -export { - getCompanyProfile, - getCompanySharesFloat, - getCompanyExecutiveCompensation, - getEarningsCalendar, - getEconomicCalendar, - getTreasuryRates, - getEconomicIndicators, - getETFHoldings, - getETFProfile, - getBalanceSheet, - getIncomeStatement, - getCashFlowStatement, - getKeyMetrics, - getFinancialRatios, - getEnterpriseValue, - getCashflowGrowth, - getIncomeGrowth, - getBalanceSheetGrowth, - getFinancialGrowth, - getEarningsHistorical, - getInsiderTrading, - getInstitutionalHolders, - getMarketPerformance, - getSectorPerformance, - getGainers, - getLosers, - getMostActive, - getStockQuote, - getSenateTrading, - getHouseTrading, - getSenateTradingByName, - getHouseTradingByName, - getSenateTradingRSSFeed, - getHouseTradingRSSFeed, - getMarketCap, - getStockSplits, - getDividendHistory, -}; +// Individual tools for direct import +export const getStockQuote = byName.getStockQuote; +export const getCompanyProfile = byName.getCompanyProfile; +export const getCompanySharesFloat = byName.getCompanySharesFloat; +export const getCompanyExecutiveCompensation = byName.getCompanyExecutiveCompensation; +export const getEarningsCalendar = byName.getEarningsCalendar; +export const getEconomicCalendar = byName.getEconomicCalendar; +export const getTreasuryRates = byName.getTreasuryRates; +export const getEconomicIndicators = byName.getEconomicIndicators; +export const getETFHoldings = byName.getETFHoldings; +export const getETFProfile = byName.getETFProfile; +export const getBalanceSheet = byName.getBalanceSheet; +export const getIncomeStatement = byName.getIncomeStatement; +export const getCashFlowStatement = byName.getCashFlowStatement; +export const getKeyMetrics = byName.getKeyMetrics; +export const getFinancialRatios = byName.getFinancialRatios; +export const getEnterpriseValue = byName.getEnterpriseValue; +export const getCashflowGrowth = byName.getCashflowGrowth; +export const getIncomeGrowth = byName.getIncomeGrowth; +export const getBalanceSheetGrowth = byName.getBalanceSheetGrowth; +export const getFinancialGrowth = byName.getFinancialGrowth; +export const getEarningsHistorical = byName.getEarningsHistorical; +export const getInsiderTrading = byName.getInsiderTrading; +export const getInstitutionalHolders = byName.getInstitutionalHolders; +export const getMarketPerformance = byName.getMarketPerformance; +export const getSectorPerformance = byName.getSectorPerformance; +export const getGainers = byName.getGainers; +export const getLosers = byName.getLosers; +export const getMostActive = byName.getMostActive; +export const getSenateTrading = byName.getSenateTrading; +export const getHouseTrading = byName.getHouseTrading; +export const getSenateTradingByName = byName.getSenateTradingByName; +export const getHouseTradingByName = byName.getHouseTradingByName; +export const getSenateTradingRSSFeed = byName.getSenateTradingRSSFeed; +export const getHouseTradingRSSFeed = byName.getHouseTradingRSSFeed; +export const getMarketCap = byName.getMarketCap; +export const getStockSplits = byName.getStockSplits; +export const getDividendHistory = byName.getDividendHistory; -// Export tool groups as arrays for OpenAI Agents -export const companyTools = [ - getCompanyProfile, - getCompanySharesFloat, - getCompanyExecutiveCompensation, -] as Tool[]; -export const calendarTools = [getEarningsCalendar, getEconomicCalendar] as Tool[]; -export const economicTools = [getTreasuryRates, getEconomicIndicators] as Tool[]; -export const etfTools = [getETFHoldings, getETFProfile] as Tool[]; -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[]; -export const marketTools = [ - getMarketPerformance, - getSectorPerformance, - getGainers, - getLosers, - getMostActive, -] as Tool[]; -export const quoteTools = [getStockQuote] as Tool[]; -export const senateHouseTools = [ - getSenateTrading, - getHouseTrading, - getSenateTradingByName, - getHouseTradingByName, - getSenateTradingRSSFeed, - getHouseTradingRSSFeed, -] as Tool[]; -export const stockTools = [getMarketCap, getStockSplits, getDividendHistory] as Tool[]; +// Tool groups as arrays for OpenAI Agents +export const quoteTools = pick(quoteDefinitions); +export const companyTools = pick(companyDefinitions); +export const financialTools = pick(financialDefinitions); +export const calendarTools = pick(calendarDefinitions); +export const economicTools = pick(economicDefinitions); +export const etfTools = pick(etfDefinitions); +export const insiderTools = pick(insiderDefinitions); +export const institutionalTools = pick(institutionalDefinitions); +export const marketTools = pick(marketDefinitions); +export const senateHouseTools = pick(senateHouseDefinitions); +export const stockTools = pick(stockDefinitions); -// Combine all tools into a single array for convenience -export const fmpTools: Tool[] = [ - getCompanyProfile, - getCompanySharesFloat, - getCompanyExecutiveCompensation, - getEarningsCalendar, - getEconomicCalendar, - getTreasuryRates, - getEconomicIndicators, - getETFHoldings, - getETFProfile, - getBalanceSheet, - getIncomeStatement, - getCashFlowStatement, - getKeyMetrics, - getFinancialRatios, - getEnterpriseValue, - getCashflowGrowth, - getIncomeGrowth, - getBalanceSheetGrowth, - getFinancialGrowth, - getEarningsHistorical, - getInsiderTrading, - getInstitutionalHolders, - getMarketPerformance, - getSectorPerformance, - getGainers, - getLosers, - getMostActive, - getStockQuote, - getSenateTrading, - getHouseTrading, - getSenateTradingByName, - getHouseTradingByName, - getSenateTradingRSSFeed, - getHouseTradingRSSFeed, - getMarketCap, - getStockSplits, - getDividendHistory, -]; +// Combine all tools into a single array +export const fmpTools: Tool[] = allDefinitions.map(def => byName[def.name]); diff --git a/packages/tools/src/providers/openai/insider.ts b/packages/tools/src/providers/openai/insider.ts deleted file mode 100644 index e1072f6..0000000 --- a/packages/tools/src/providers/openai/insider.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -// Input schema for insider trading with symbol and optional page -const insiderTradingInputSchema = z.object({ - symbol: z - .string() - .min(1, 'Stock symbol is required') - .describe('Stock symbol (e.g., AAPL, MSFT, GOOGL)'), - page: z - .number() - .int() - .min(0) - .default(0) - .describe('Page number for pagination (optional, defaults to 0)'), -}); - -export const getInsiderTrading = createOpenAITool({ - name: 'getInsiderTrading', - description: - 'Get insider trading data for a specific stock symbol showing buy/sell transactions by company insiders', - inputSchema: insiderTradingInputSchema, - execute: async ({ symbol, page }) => { - const fmp = getFMPClient(); - const insiderTrading = await fmp.insider.getInsiderTradesBySymbol(symbol, page); - return toToolResponse(insiderTrading); - }, -}); diff --git a/packages/tools/src/providers/openai/institutional.ts b/packages/tools/src/providers/openai/institutional.ts deleted file mode 100644 index 99549b8..0000000 --- a/packages/tools/src/providers/openai/institutional.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -// Input schema for institutional holders with symbol -const institutionalHoldersInputSchema = z.object({ - symbol: z - .string() - .min(1, 'Stock symbol is required') - .describe('Stock symbol (e.g., AAPL, MSFT, GOOGL)'), -}); - -export const getInstitutionalHolders = createOpenAITool({ - name: 'getInstitutionalHolders', - description: - 'Get institutional holders for a specific stock symbol showing which institutions own shares and their holdings', - inputSchema: institutionalHoldersInputSchema, - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const institutionalHolders = await fmp.institutional.getInstitutionalHolders({ symbol }); - - // Return formatted JSON string - return toToolResponse(institutionalHolders); - }, -}); diff --git a/packages/tools/src/providers/openai/market.ts b/packages/tools/src/providers/openai/market.ts deleted file mode 100644 index 5e703e3..0000000 --- a/packages/tools/src/providers/openai/market.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -// Empty schema for tools that don't require parameters -const emptyInputSchema = z.object({}); - -export const getMarketPerformance = createOpenAITool({ - name: 'getMarketPerformance', - description: 'Get overall market performance data including major indices performance', - inputSchema: emptyInputSchema, - execute: async () => { - const fmp = getFMPClient(); - const marketPerformance = await fmp.market.getMarketPerformance(); - return toToolResponse(marketPerformance); - }, -}); - -export const getSectorPerformance = createOpenAITool({ - name: 'getSectorPerformance', - description: 'Get sector performance data showing how different market sectors are performing', - inputSchema: emptyInputSchema, - execute: async () => { - const fmp = getFMPClient(); - const sectorPerformance = await fmp.market.getSectorPerformance(); - return toToolResponse(sectorPerformance); - }, -}); - -export const getGainers = createOpenAITool({ - name: 'getGainers', - description: 'Get top gaining stocks showing the best performing stocks of the day', - inputSchema: emptyInputSchema, - execute: async () => { - const fmp = getFMPClient(); - const gainers = await fmp.market.getGainers(); - return toToolResponse(gainers); - }, -}); - -export const getLosers = createOpenAITool({ - name: 'getLosers', - description: 'Get top losing stocks showing the worst performing stocks of the day', - inputSchema: emptyInputSchema, - execute: async () => { - const fmp = getFMPClient(); - const losers = await fmp.market.getLosers(); - return toToolResponse(losers); - }, -}); - -export const getMostActive = createOpenAITool({ - name: 'getMostActive', - description: 'Get most active stocks showing stocks with the highest trading volume', - inputSchema: emptyInputSchema, - execute: async () => { - const fmp = getFMPClient(); - const mostActive = await fmp.market.getMostActive(); - return toToolResponse(mostActive); - }, -}); diff --git a/packages/tools/src/providers/openai/quote.ts b/packages/tools/src/providers/openai/quote.ts deleted file mode 100644 index 9337ea0..0000000 --- a/packages/tools/src/providers/openai/quote.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -// Input schema for stock quote with symbol -const stockQuoteInputSchema = z.object({ - symbol: z - .string() - .min(1, 'Stock symbol is required') - .describe('The symbol of the company to get the stock quote for'), -}); - -export const getStockQuote = createOpenAITool({ - name: 'getStockQuote', - description: - 'Get the real-time stock quote for a company including price, volume, and market data', - inputSchema: stockQuoteInputSchema, - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const stockQuote = await fmp.quote.getQuote(symbol); - return toToolResponse(stockQuote); - }, -}); diff --git a/packages/tools/src/providers/openai/senate-house.ts b/packages/tools/src/providers/openai/senate-house.ts deleted file mode 100644 index 16d7c02..0000000 --- a/packages/tools/src/providers/openai/senate-house.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -// Input schema for symbol-based trading data -const symbolInputSchema = z.object({ - symbol: z - .string() - .min(1, 'Stock symbol is required') - .describe('Stock symbol (e.g., AAPL, MSFT, GOOGL)'), -}); - -// Input schema for name-based trading data -const nameInputSchema = z.object({ - name: z.string().min(1, 'Name is required').describe('The name of the senator or representative'), -}); - -// Input schema for RSS feed with pagination -const rssFeedInputSchema = z.object({ - page: z - .number() - .int() - .min(0) - .default(0) - .describe('Page number for pagination (optional, defaults to 0)'), -}); - -export const getSenateTrading = createOpenAITool({ - name: 'getSenateTrading', - description: 'Get senate trading data for a specific stock symbol showing senator transactions', - inputSchema: symbolInputSchema, - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const senateTrading = await fmp.senateHouse.getSenateTrading({ symbol }); - return toToolResponse(senateTrading); - }, -}); - -export const getHouseTrading = createOpenAITool({ - name: 'getHouseTrading', - description: - 'Get house trading data for a specific stock symbol showing representative transactions', - inputSchema: symbolInputSchema, - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const houseTrading = await fmp.senateHouse.getHouseTrading({ symbol }); - return toToolResponse(houseTrading); - }, -}); - -export const getSenateTradingByName = createOpenAITool({ - name: 'getSenateTradingByName', - description: - 'Get senate trading data for a specific senator by name showing their trading activity', - inputSchema: nameInputSchema, - execute: async ({ name }) => { - const fmp = getFMPClient(); - const senateTradingByName = await fmp.senateHouse.getSenateTradingByName({ name }); - return toToolResponse(senateTradingByName); - }, -}); - -export const getHouseTradingByName = createOpenAITool({ - name: 'getHouseTradingByName', - description: - 'Get house trading data for a specific representative by name showing their trading activity', - inputSchema: nameInputSchema, - execute: async ({ name }) => { - const fmp = getFMPClient(); - const houseTradingByName = await fmp.senateHouse.getHouseTradingByName({ name }); - return toToolResponse(houseTradingByName); - }, -}); - -export const getSenateTradingRSSFeed = createOpenAITool({ - name: 'getSenateTradingRSSFeed', - description: - 'Get senate trading data through RSS feed with pagination showing recent senate transactions', - inputSchema: rssFeedInputSchema, - execute: async ({ page }) => { - const fmp = getFMPClient(); - const senateTradingRSSFeed = await fmp.senateHouse.getSenateTradingRSSFeed({ page }); - return toToolResponse(senateTradingRSSFeed); - }, -}); - -export const getHouseTradingRSSFeed = createOpenAITool({ - name: 'getHouseTradingRSSFeed', - description: - 'Get house trading data through RSS feed with pagination showing recent house transactions', - inputSchema: rssFeedInputSchema, - execute: async ({ page }) => { - const fmp = getFMPClient(); - const houseTradingRSSFeed = await fmp.senateHouse.getHouseTradingRSSFeed({ page }); - return toToolResponse(houseTradingRSSFeed); - }, -}); diff --git a/packages/tools/src/providers/openai/stock.ts b/packages/tools/src/providers/openai/stock.ts deleted file mode 100644 index e11089e..0000000 --- a/packages/tools/src/providers/openai/stock.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -// Input schema for symbol-based stock operations -const symbolInputSchema = z.object({ - symbol: z - .string() - .min(1, 'Stock symbol is required') - .describe('The stock symbol (e.g., AAPL, MSFT, GOOGL)'), -}); - -export const getMarketCap = createOpenAITool({ - name: 'getMarketCap', - description: 'Get market capitalization for a company showing current market value', - inputSchema: symbolInputSchema, - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const marketCap = await fmp.stock.getMarketCap(symbol); - return toToolResponse(marketCap); - }, -}); - -export const getStockSplits = createOpenAITool({ - name: 'getStockSplits', - description: 'Get stock splits history for a company showing all historical stock split events', - inputSchema: symbolInputSchema, - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const stockSplits = await fmp.stock.getStockSplits(symbol); - return toToolResponse(stockSplits); - }, -}); - -export const getDividendHistory = createOpenAITool({ - name: 'getDividendHistory', - description: 'Get dividend history for a company showing all historical dividend payments', - inputSchema: symbolInputSchema, - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const dividendHistory = await fmp.stock.getDividendHistory(symbol); - return toToolResponse(dividendHistory); - }, -}); - -export const stockTools = [getMarketCap, getStockSplits, getDividendHistory]; diff --git a/packages/tools/src/providers/vercel-ai/calendar.ts b/packages/tools/src/providers/vercel-ai/calendar.ts deleted file mode 100644 index 6fdfce6..0000000 --- a/packages/tools/src/providers/vercel-ai/calendar.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { z } from 'zod'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -export const calendarTools = { - getEarningsCalendar: createTool({ - name: 'getEarningsCalendar', - description: 'Get earnings calendar', - inputSchema: z.object({ - from: z.string().optional().describe('Start date in YYYY-MM-DD format'), - to: z.string().optional().describe('End date in YYYY-MM-DD format'), - }), - execute: async ({ from, to }) => { - const fmp = getFMPClient(); - const earningsCalendar = await fmp.calendar.getEarningsCalendar({ from, to }); - const response = toToolResponse(earningsCalendar); - return response; - }, - }), - - getEconomicCalendar: createTool({ - name: 'getEconomicCalendar', - description: 'Get economic calendar', - inputSchema: z.object({ - from: z.string().optional().describe('Start date in YYYY-MM-DD format'), - to: z.string().optional().describe('End date in YYYY-MM-DD format'), - }), - execute: async ({ from, to }) => { - const fmp = getFMPClient(); - const economicCalendar = await fmp.calendar.getEconomicsCalendar({ from, to }); - const response = toToolResponse(economicCalendar); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/company.ts b/packages/tools/src/providers/vercel-ai/company.ts deleted file mode 100644 index cf813d5..0000000 --- a/packages/tools/src/providers/vercel-ai/company.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { z } from 'zod'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -export const companyTools = { - getCompanyProfile: createTool({ - name: 'getCompanyProfile', - description: 'Get the company profile', - inputSchema: z.object({ - symbol: z.string().describe('The stock ticker symbol (e.g., AAPL)'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const companyProfile = await fmp.company.getCompanyProfile(symbol); - const response = toToolResponse(companyProfile); - return response; - }, - }), - getCompanySharesFloat: createTool({ - name: 'getCompanySharesFloat', - description: 'Get the company shares float', - inputSchema: z.object({ - symbol: z.string().describe('The stock ticker symbol (e.g., AAPL)'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const companySharesFloat = await fmp.company.getSharesFloat(symbol); - const response = toToolResponse(companySharesFloat); - return response; - }, - }), - - getCompanyExecutiveCompensation: createTool({ - name: 'getCompanyExecutiveCompensation', - description: 'Get the company executive compensation', - inputSchema: z.object({ - symbol: z.string().describe('The stock ticker symbol (e.g., AAPL)'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const companyExecutiveCompensation = await fmp.company.getExecutiveCompensation(symbol); - const response = toToolResponse(companyExecutiveCompensation); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/economic.ts b/packages/tools/src/providers/vercel-ai/economic.ts deleted file mode 100644 index 2bb1917..0000000 --- a/packages/tools/src/providers/vercel-ai/economic.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; - -export const economicTools = { - getTreasuryRates: createTool({ - name: 'getTreasuryRates', - description: 'Get treasury rates', - inputSchema: z.object({ - from: z.string().optional().describe('Start date in YYYY-MM-DD format'), - to: z.string().optional().describe('End date in YYYY-MM-DD format'), - }), - execute: async ({ from, to }) => { - const fmp = getFMPClient(); - const treasuryRates = await fmp.economic.getTreasuryRates({ from, to }); - const response = toToolResponse(treasuryRates); - return response; - }, - }), - - getEconomicIndicators: createTool({ - name: 'getEconomicIndicators', - description: 'Get economic indicators', - inputSchema: z.object({ - name: z - .enum([ - 'GDP', - 'realGDP', - 'nominalPotentialGDP', - 'realGDPPerCapita', - 'federalFunds', - 'CPI', - 'inflationRate', - 'inflation', - 'retailSales', - 'consumerSentiment', - 'durableGoods', - 'unemploymentRate', - 'totalNonfarmPayroll', - 'initialClaims', - 'industrialProductionTotalIndex', - 'newPrivatelyOwnedHousingUnitsStartedTotalUnits', - 'totalVehicleSales', - 'retailMoneyFunds', - 'smoothedUSRecessionProbabilities', - '3MonthOr90DayRatesAndYieldsCertificatesOfDeposit', - 'commercialBankInterestRateOnCreditCardPlansAllAccounts', - '30YearFixedRateMortgageAverage', - '15YearFixedRateMortgageAverage', - ]) - .describe('The name of the economic indicator'), - from: z.string().optional().describe('Start date in YYYY-MM-DD format'), - to: z.string().optional().describe('End date in YYYY-MM-DD format'), - }), - execute: async ({ name, from, to }) => { - const fmp = getFMPClient(); - const economicIndicators = await fmp.economic.getEconomicIndicators({ name, from, to }); - const response = toToolResponse(economicIndicators); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/etf.ts b/packages/tools/src/providers/vercel-ai/etf.ts deleted file mode 100644 index f522aa4..0000000 --- a/packages/tools/src/providers/vercel-ai/etf.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; - -export const etfTools = { - getETFHoldings: createTool({ - name: 'getETFHoldings', - description: 'Get ETF holdings for a specific ETF symbol', - inputSchema: z.object({ - symbol: z.string().describe('ETF symbol (e.g., SPY, QQQ, VTI)'), - date: z.string().optional().describe('Date for holdings (YYYY-MM-DD format)'), - }), - execute: async ({ symbol, date }) => { - const fmp = getFMPClient(); - const params: any = { symbol }; - if (date) { - params.date = date; - } - const etfHoldings = await fmp.etf.getHoldings(params); - const response = toToolResponse(etfHoldings); - return response; - }, - }), - - getETFProfile: createTool({ - name: 'getETFProfile', - description: 'Get ETF profile information', - inputSchema: z.object({ - symbol: z.string().describe('ETF symbol (e.g., SPY, QQQ, VTI)'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const etfProfile = await fmp.etf.getProfile(symbol); - const response = toToolResponse(etfProfile); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/financial.ts b/packages/tools/src/providers/vercel-ai/financial.ts deleted file mode 100644 index 93d5570..0000000 --- a/packages/tools/src/providers/vercel-ai/financial.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; - -export const financialTools = { - getBalanceSheet: createTool({ - name: 'getBalanceSheet', - description: 'Get balance sheet for a company', - 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.number().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, limit }); - const response = toToolResponse(balanceSheet); - return response; - }, - }), - - getIncomeStatement: createTool({ - name: 'getIncomeStatement', - description: 'Get income statement for a company', - 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.number().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, limit }); - const response = toToolResponse(incomeStatement); - return response; - }, - }), - - getCashFlowStatement: createTool({ - name: 'getCashFlowStatement', - description: 'Get cash flow statement for a company', - 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.number().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, limit }); - const response = toToolResponse(cashFlowStatement); - 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 = toToolResponse(keyMetrics); - return response; - }, - }), - - getFinancialRatios: createTool({ - name: 'getFinancialRatios', - description: 'Get financial ratios for a company', - 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.number().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, limit }); - const response = toToolResponse(financialRatios); - 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 = toToolResponse(enterpriseValue); - 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 = toToolResponse(cashflowGrowth); - 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 = toToolResponse(incomeGrowth); - 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 = toToolResponse(balanceSheetGrowth); - 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 = toToolResponse(financialGrowth); - 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 = toToolResponse(earningsHistorical); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/index.ts b/packages/tools/src/providers/vercel-ai/index.ts index ddb83ec..3163b28 100644 --- a/packages/tools/src/providers/vercel-ai/index.ts +++ b/packages/tools/src/providers/vercel-ai/index.ts @@ -1,26 +1,59 @@ import { ToolSet } from 'ai'; -import { quoteTools } from './quote'; -import { companyTools } from './company'; -import { financialTools } from './financial'; -import { calendarTools } from './calendar'; -import { economicTools } from './economic'; -import { etfTools } from './etf'; -import { insiderTools } from './insider'; -import { institutionalTools } from './institutional'; -import { marketTools } from './market'; -import { senateHouseTools } from './senate-house'; -import { stockTools } from './stock'; +import { createTool } from '@/utils/aisdk-tool-wrapper'; +import { + quoteDefinitions, + companyDefinitions, + financialDefinitions, + calendarDefinitions, + economicDefinitions, + etfDefinitions, + insiderDefinitions, + institutionalDefinitions, + marketDefinitions, + senateHouseDefinitions, + stockDefinitions, + type FMPToolDefinition, +} from '@/definitions'; -// Export individual tools for Vercel AI +// Build a Vercel AI `ToolSet` (object keyed by tool name) from shared definitions. +const toToolSet = (defs: FMPToolDefinition[]): ToolSet => + Object.fromEntries(defs.map(def => [def.name, createTool(def)])); + +// Tool groups by category +export const quoteTools = toToolSet(quoteDefinitions); +export const companyTools = toToolSet(companyDefinitions); +export const financialTools = toToolSet(financialDefinitions); +export const calendarTools = toToolSet(calendarDefinitions); +export const economicTools = toToolSet(economicDefinitions); +export const etfTools = toToolSet(etfDefinitions); +export const insiderTools = toToolSet(insiderDefinitions); +export const institutionalTools = toToolSet(institutionalDefinitions); +export const marketTools = toToolSet(marketDefinitions); +export const senateHouseTools = toToolSet(senateHouseDefinitions); +export const stockTools = toToolSet(stockDefinitions); + +// Combine all tools into a single ToolSet +export const fmpTools: ToolSet = { + ...quoteTools, + ...companyTools, + ...financialTools, + ...calendarTools, + ...economicTools, + ...etfTools, + ...insiderTools, + ...institutionalTools, + ...marketTools, + ...senateHouseTools, + ...stockTools, +}; + +// Individual tools for direct import +export const { getStockQuote } = quoteTools; export const { getCompanyProfile, getCompanySharesFloat, getCompanyExecutiveCompensation } = companyTools; - export const { getEarningsCalendar, getEconomicCalendar } = calendarTools; - export const { getTreasuryRates, getEconomicIndicators } = economicTools; - export const { getETFHoldings, getETFProfile } = etfTools; - export const { getBalanceSheet, getIncomeStatement, @@ -34,16 +67,10 @@ export const { getFinancialGrowth, getEarningsHistorical, } = financialTools; - export const { getInsiderTrading } = insiderTools; - export const { getInstitutionalHolders } = institutionalTools; - export const { getMarketPerformance, getSectorPerformance, getGainers, getLosers, getMostActive } = marketTools; - -export const { getStockQuote } = quoteTools; - export const { getSenateTrading, getHouseTrading, @@ -52,35 +79,4 @@ export const { getSenateTradingRSSFeed, getHouseTradingRSSFeed, } = senateHouseTools; - export const { getMarketCap, getStockSplits, getDividendHistory } = stockTools; - -// Combine all tools into a single object for AI SDK v2 -export const fmpTools: ToolSet = { - ...quoteTools, - ...companyTools, - ...financialTools, - ...calendarTools, - ...economicTools, - ...etfTools, - ...insiderTools, - ...institutionalTools, - ...marketTools, - ...senateHouseTools, - ...stockTools, -}; - -// Re-export individual tool groups -export { - quoteTools, - companyTools, - financialTools, - calendarTools, - economicTools, - etfTools, - insiderTools, - institutionalTools, - marketTools, - senateHouseTools, - stockTools, -}; diff --git a/packages/tools/src/providers/vercel-ai/insider.ts b/packages/tools/src/providers/vercel-ai/insider.ts deleted file mode 100644 index 86364db..0000000 --- a/packages/tools/src/providers/vercel-ai/insider.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from 'zod'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; - -export const insiderTools = { - getInsiderTrading: createTool({ - name: 'getInsiderTrading', - description: 'Get insider trading data for a specific stock symbol', - inputSchema: z.object({ - symbol: z.string().describe('Stock symbol (e.g., AAPL, MSFT, GOOGL)'), - page: z.number().default(0).describe('Page number for pagination'), - }), - execute: async ({ symbol, page }) => { - const fmp = getFMPClient(); - const insiderTrading = await fmp.insider.getInsiderTradesBySymbol(symbol, page); - const response = toToolResponse(insiderTrading); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/institutional.ts b/packages/tools/src/providers/vercel-ai/institutional.ts deleted file mode 100644 index 269c816..0000000 --- a/packages/tools/src/providers/vercel-ai/institutional.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; - -export const institutionalTools = { - getInstitutionalHolders: createTool({ - name: 'getInstitutionalHolders', - description: 'Get institutional holders for a specific stock symbol', - inputSchema: z.object({ - symbol: z.string().describe('Stock symbol (e.g., AAPL, MSFT, GOOGL)'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const institutionalHolders = await fmp.institutional.getInstitutionalHolders({ symbol }); - const response = toToolResponse(institutionalHolders); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/market.ts b/packages/tools/src/providers/vercel-ai/market.ts deleted file mode 100644 index 72b2846..0000000 --- a/packages/tools/src/providers/vercel-ai/market.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; - -export const marketTools = { - getMarketPerformance: createTool({ - name: 'getMarketPerformance', - description: 'Get overall market performance data', - inputSchema: z.object({}), - execute: async () => { - const fmp = getFMPClient(); - const marketPerformance = await fmp.market.getMarketPerformance(); - const response = toToolResponse(marketPerformance); - return response; - }, - }), - - getSectorPerformance: createTool({ - name: 'getSectorPerformance', - description: 'Get sector performance data', - inputSchema: z.object({}), - execute: async () => { - const fmp = getFMPClient(); - const sectorPerformance = await fmp.market.getSectorPerformance(); - const response = toToolResponse(sectorPerformance); - return response; - }, - }), - - getGainers: createTool({ - name: 'getGainers', - description: 'Get top gaining stocks', - inputSchema: z.object({}), - execute: async () => { - const fmp = getFMPClient(); - const gainers = await fmp.market.getGainers(); - const response = toToolResponse(gainers); - return response; - }, - }), - - getLosers: createTool({ - name: 'getLosers', - description: 'Get top losing stocks', - inputSchema: z.object({}), - execute: async () => { - const fmp = getFMPClient(); - const losers = await fmp.market.getLosers(); - const response = toToolResponse(losers); - return response; - }, - }), - - getMostActive: createTool({ - name: 'getMostActive', - description: 'Get most active stocks', - inputSchema: z.object({}), - execute: async () => { - const fmp = getFMPClient(); - const mostActive = await fmp.market.getMostActive(); - const response = toToolResponse(mostActive); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/quote.ts b/packages/tools/src/providers/vercel-ai/quote.ts deleted file mode 100644 index 6709ef7..0000000 --- a/packages/tools/src/providers/vercel-ai/quote.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; - -export const quoteTools = { - getStockQuote: createTool({ - name: 'getStockQuote', - description: 'Get the stock quote for a company', - inputSchema: z.object({ - symbol: z.string().describe('The symbol of the company to get the stock quote for'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const stockQuote = await fmp.quote.getQuote(symbol); - const response = toToolResponse(stockQuote); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/senate-house.ts b/packages/tools/src/providers/vercel-ai/senate-house.ts deleted file mode 100644 index 8aec846..0000000 --- a/packages/tools/src/providers/vercel-ai/senate-house.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; - -export const senateHouseTools = { - getSenateTrading: createTool({ - name: 'getSenateTrading', - description: 'Get senate trading data for a specific stock symbol', - inputSchema: z.object({ - symbol: z.string().describe('Stock symbol (e.g., AAPL, MSFT, GOOGL)'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const senateTrading = await fmp.senateHouse.getSenateTrading({ symbol }); - const response = toToolResponse(senateTrading); - return response; - }, - }), - - getHouseTrading: createTool({ - name: 'getHouseTrading', - description: 'Get house trading data for a specific stock symbol', - inputSchema: z.object({ - symbol: z.string().describe('Stock symbol (e.g., AAPL, MSFT, GOOGL)'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const houseTrading = await fmp.senateHouse.getHouseTrading({ symbol }); - const response = toToolResponse(houseTrading); - return response; - }, - }), - - getSenateTradingByName: createTool({ - name: 'getSenateTradingByName', - description: 'Get senate trading data for a specific senator by name', - inputSchema: z.object({ - name: z.string().describe('The name of the senator to get trading data for'), - }), - execute: async ({ name }) => { - const fmp = getFMPClient(); - const senateTradingByName = await fmp.senateHouse.getSenateTradingByName({ name }); - const response = toToolResponse(senateTradingByName); - return response; - }, - }), - - getHouseTradingByName: createTool({ - name: 'getHouseTradingByName', - description: 'Get house trading data for a specific representative by name', - inputSchema: z.object({ - name: z.string().describe('The name of the representative to get trading data for'), - }), - execute: async ({ name }) => { - const fmp = getFMPClient(); - const houseTradingByName = await fmp.senateHouse.getHouseTradingByName({ name }); - const response = toToolResponse(houseTradingByName); - return response; - }, - }), - - getSenateTradingRSSFeed: createTool({ - name: 'getSenateTradingRSSFeed', - description: 'Get senate trading data through RSS feed with pagination', - inputSchema: z.object({ - page: z.number().default(0).describe('Page number for pagination'), - }), - execute: async ({ page = 0 }) => { - const fmp = getFMPClient(); - const senateTradingRSSFeed = await fmp.senateHouse.getSenateTradingRSSFeed({ page }); - const response = toToolResponse(senateTradingRSSFeed); - return response; - }, - }), - - getHouseTradingRSSFeed: createTool({ - name: 'getHouseTradingRSSFeed', - description: 'Get house trading data through RSS feed with pagination', - inputSchema: z.object({ - page: z.number().default(0).describe('Page number for pagination'), - }), - execute: async ({ page = 0 }) => { - const fmp = getFMPClient(); - const houseTradingRSSFeed = await fmp.senateHouse.getHouseTradingRSSFeed({ page }); - const response = toToolResponse(houseTradingRSSFeed); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/stock.ts b/packages/tools/src/providers/vercel-ai/stock.ts deleted file mode 100644 index be7f13f..0000000 --- a/packages/tools/src/providers/vercel-ai/stock.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -import { toToolResponse } from '@/utils/format-response'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; - -export const stockTools = { - getMarketCap: createTool({ - name: 'getMarketCap', - description: 'Get market capitalization for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get market cap for'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const marketCap = await fmp.stock.getMarketCap(symbol); - const response = toToolResponse(marketCap); - return response; - }, - }), - - getStockSplits: createTool({ - name: 'getStockSplits', - description: 'Get stock splits history for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get stock splits for'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const stockSplits = await fmp.stock.getStockSplits(symbol); - const response = toToolResponse(stockSplits); - return response; - }, - }), - - getDividendHistory: createTool({ - name: 'getDividendHistory', - description: 'Get dividend history for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get dividend history for'), - }), - execute: async ({ symbol }) => { - const fmp = getFMPClient(); - const dividendHistory = await fmp.stock.getDividendHistory(symbol); - const response = toToolResponse(dividendHistory); - return response; - }, - }), -}; From 02603273fa015c13a1473a5b1d0b4d7ca1f73a6d Mon Sep 17 00:00:00 2001 From: e-roy Date: Tue, 26 May 2026 15:00:35 -0400 Subject: [PATCH 07/13] add screen, search, news and price history tools --- .changeset/pre.json | 2 + .changeset/search-endpoint.md | 6 ++ .changeset/tools-config-and-coverage.md | 8 +++ .../src/app/docs/tools/categories/page.mdx | 36 ++++++++++ apps/docs/src/app/docs/tools/openai/page.mdx | 12 ++++ .../src/app/docs/tools/vercel-ai/page.mdx | 12 ++++ packages/api/CHANGELOG.md | 6 ++ packages/api/package.json | 2 +- packages/api/scripts/live/manifest.ts | 6 ++ .../src/__tests__/endpoints/search.test.ts | 57 ++++++++++++++++ packages/api/src/endpoints/search.ts | 38 +++++++++++ packages/api/src/fmp.ts | 3 + packages/api/src/index.ts | 1 + packages/tools/CHANGELOG.md | 13 ++++ packages/tools/README.md | 25 +++++++ packages/tools/package.json | 2 +- packages/tools/src/__tests__/client.test.ts | 41 ++++++++++-- .../__tests__/definitions/consistency.test.ts | 6 +- .../providers/vercel-ai/news.test.ts | 45 +++++++++++++ .../providers/vercel-ai/quote.test.ts | 45 +++++++++++++ .../providers/vercel-ai/screener.test.ts | 34 ++++++++++ .../providers/vercel-ai/search.test.ts | 30 +++++++++ packages/tools/src/client.ts | 37 +++++++++-- packages/tools/src/definitions/index.ts | 9 +++ packages/tools/src/definitions/news.ts | 56 ++++++++++++++++ packages/tools/src/definitions/quote.ts | 65 +++++++++++++++++++ packages/tools/src/definitions/screener.ts | 39 +++++++++++ packages/tools/src/definitions/search.ts | 30 +++++++++ packages/tools/src/providers/openai/index.ts | 16 +++++ .../tools/src/providers/vercel-ai/index.ts | 18 ++++- packages/types/CHANGELOG.md | 6 ++ packages/types/package.json | 2 +- packages/types/src/index.ts | 3 + packages/types/src/search.ts | 14 ++++ 34 files changed, 706 insertions(+), 19 deletions(-) create mode 100644 .changeset/search-endpoint.md create mode 100644 .changeset/tools-config-and-coverage.md create mode 100644 packages/api/src/__tests__/endpoints/search.test.ts create mode 100644 packages/api/src/endpoints/search.ts create mode 100644 packages/tools/src/__tests__/providers/vercel-ai/news.test.ts create mode 100644 packages/tools/src/__tests__/providers/vercel-ai/screener.test.ts create mode 100644 packages/tools/src/__tests__/providers/vercel-ai/search.test.ts create mode 100644 packages/tools/src/definitions/news.ts create mode 100644 packages/tools/src/definitions/screener.ts create mode 100644 packages/tools/src/definitions/search.ts create mode 100644 packages/types/src/search.ts diff --git a/.changeset/pre.json b/.changeset/pre.json index f08d4d1..1962011 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -13,7 +13,9 @@ "error-handling", "loosen-tool-dep-ranges", "openai-bundler-safe", + "search-endpoint", "tools-catch-thrown-errors", + "tools-config-and-coverage", "tools-shared-definitions", "zod4-v6-beta" ] diff --git a/.changeset/search-endpoint.md b/.changeset/search-endpoint.md new file mode 100644 index 0000000..9d9c607 --- /dev/null +++ b/.changeset/search-endpoint.md @@ -0,0 +1,6 @@ +--- +"fmp-node-types": minor +"fmp-node-api": minor +--- + +Add a `/search` endpoint: `fmp.search.search({ query, limit?, exchange? })` resolves a company name or partial ticker to matching symbols, returning the new `SearchResult` type. Wired into the live-API shape-check manifest. diff --git a/.changeset/tools-config-and-coverage.md b/.changeset/tools-config-and-coverage.md new file mode 100644 index 0000000..7a4ef40 --- /dev/null +++ b/.changeset/tools-config-and-coverage.md @@ -0,0 +1,8 @@ +--- +"fmp-ai-tools": minor +--- + +Add client configuration and new tools. + +- **Client**: the FMP client is now memoized (no longer reconstructed on every tool call), and a new `configureFMPClient({ apiKey, timeout })` (exported from both entry points) lets consumers configure it instead of relying solely on the `FMP_API_KEY` environment variable. +- **New tools**: `getHistoricalPrice` and `getIntraday` (price history), `getStockNews` and `getStockNewsBySymbol` (news), `screenStocks` (screener), and `searchSymbol` (symbol search). High-volume tools apply a default result `limit` to keep outputs context-friendly. diff --git a/apps/docs/src/app/docs/tools/categories/page.mdx b/apps/docs/src/app/docs/tools/categories/page.mdx index 1ebf611..af93b84 100644 --- a/apps/docs/src/app/docs/tools/categories/page.mdx +++ b/apps/docs/src/app/docs/tools/categories/page.mdx @@ -14,11 +14,14 @@ Real-time and historical price data for stocks and other securities. **Tools:** - `getStockQuote` - Get real-time stock quote for a company +- `getHistoricalPrice` - Get historical daily prices (most recent `limit` days, default 30) +- `getIntraday` - Get intraday price bars at a given interval (most recent `limit` bars, default 50) **Use Cases:** - Real-time price monitoring - Portfolio tracking - Price alerts and notifications +- Historical trend and performance analysis ### 🏢 Company Tools @@ -179,6 +182,39 @@ Stock-specific data like market cap, splits, and dividends. - Dividend analysis - Historical stock data research +### 📰 News Tools + +Latest stock market news, general or filtered by symbol. + +**Tools:** +- `getStockNews` - Get the latest general stock market news (most recent `limit`, default 20) +- `getStockNewsBySymbol` - Get the latest news for one or more symbols (default `limit` 20) + +**Use Cases:** +- Market sentiment and event tracking +- Company-specific news monitoring + +### 🔎 Screener Tools + +Find stocks matching financial criteria. + +**Tools:** +- `screenStocks` - Screen for stocks by market cap, price, sector, exchange, etc. (default `limit` 50) + +**Use Cases:** +- Idea generation and stock discovery +- Building filtered watchlists + +### 🔍 Search Tools + +Resolve names and partial tickers to symbols. + +**Tools:** +- `searchSymbol` - Resolve a company name or partial ticker to matching symbols (e.g., "Apple" → AAPL) + +**Use Cases:** +- Turning a company name into a ticker before calling other tools + ## Tool Selection Guide ### For Financial Analysis Applications diff --git a/apps/docs/src/app/docs/tools/openai/page.mdx b/apps/docs/src/app/docs/tools/openai/page.mdx index 4e0bcd1..03d9e69 100644 --- a/apps/docs/src/app/docs/tools/openai/page.mdx +++ b/apps/docs/src/app/docs/tools/openai/page.mdx @@ -173,6 +173,8 @@ FMP Tools provides comprehensive financial tools organized into categories: ### 📊 Quote Tools - `getStockQuote` - Real-time stock quotes and price data +- `getHistoricalPrice` - Historical daily prices (most recent `limit` days, default 30) +- `getIntraday` - Intraday price bars at a given interval (most recent `limit` bars, default 50) ### 🏢 Company Tools - `getCompanyProfile` - Comprehensive company profiles and information @@ -227,6 +229,16 @@ FMP Tools provides comprehensive financial tools organized into categories: - `getStockSplits` - Historical stock splits for a company - `getDividendHistory` - Dividend history and payments for a company +### 📰 News Tools +- `getStockNews` - Latest general stock market news (default `limit` 20) +- `getStockNewsBySymbol` - Latest news for one or more symbols (default `limit` 20) + +### 🔎 Screener Tools +- `screenStocks` - Screen for stocks by market cap, price, sector, exchange, etc. (default `limit` 50) + +### 🔍 Search Tools +- `searchSymbol` - Resolve a company name or partial ticker to matching symbols + [**View detailed tool categories →**](/docs/tools/categories) ## Advanced Configuration 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 effb015..fe6aa7d 100644 --- a/apps/docs/src/app/docs/tools/vercel-ai/page.mdx +++ b/apps/docs/src/app/docs/tools/vercel-ai/page.mdx @@ -144,6 +144,8 @@ FMP Tools provides comprehensive financial tools organized into categories: ### 📊 Quote Tools - `getStockQuote` - Real-time stock quotes and price data +- `getHistoricalPrice` - Historical daily prices (most recent `limit` days, default 30) +- `getIntraday` - Intraday price bars at a given interval (most recent `limit` bars, default 50) ### 🏢 Company Tools - `getCompanyProfile` - Comprehensive company profiles and information @@ -198,6 +200,16 @@ FMP Tools provides comprehensive financial tools organized into categories: - `getStockSplits` - Historical stock splits for a company - `getDividendHistory` - Dividend history and payments for a company +### 📰 News Tools +- `getStockNews` - Latest general stock market news (default `limit` 20) +- `getStockNewsBySymbol` - Latest news for one or more symbols (default `limit` 20) + +### 🔎 Screener Tools +- `screenStocks` - Screen for stocks by market cap, price, sector, exchange, etc. (default `limit` 50) + +### 🔍 Search Tools +- `searchSymbol` - Resolve a company name or partial ticker to matching symbols + [**View detailed tool categories →**](/docs/tools/categories) ## Advanced Configuration diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 8c1cab0..27acb10 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,11 @@ # fmp-node-api +## 0.2.0-beta.2 + +### Minor Changes + +- Add a `/search` endpoint: `fmp.search.search({ query, limit?, exchange? })` resolves a company name or partial ticker to matching symbols, returning the new `SearchResult` type. Wired into the live-API shape-check manifest. + ## 0.2.0-beta.1 ### Minor Changes diff --git a/packages/api/package.json b/packages/api/package.json index ca7fa70..6a077fe 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-api", - "version": "0.2.0-beta.1", + "version": "0.2.0-beta.2", "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/live/manifest.ts b/packages/api/scripts/live/manifest.ts index bc2bf3d..2e048d2 100644 --- a/packages/api/scripts/live/manifest.ts +++ b/packages/api/scripts/live/manifest.ts @@ -92,6 +92,8 @@ import { AvailableSectorsSchema, AvailableIndustriesSchema, AvailableCountriesSchema, + // search + SearchResultSchema, // sec RSSFeedItemSchema, RSSFeedAllItemSchema, @@ -122,6 +124,7 @@ export type Category = | 'mutual-fund' | 'news' | 'screener' + | 'search' | 'sec' | 'senate-house'; @@ -256,6 +259,9 @@ export const manifest: LiveCase[] = [ { category: 'screener', name: 'getAvailableIndustries()', schema: AvailableIndustriesSchema, kind: 'array', call: (fmp) => fmp.screener.getAvailableIndustries() }, { category: 'screener', name: 'getAvailableCountries()', schema: AvailableCountriesSchema, kind: 'array', call: (fmp) => fmp.screener.getAvailableCountries() }, + // ---- search ---- + { category: 'search', name: 'search(AAPL)', schema: SearchResultSchema, kind: 'array', call: (fmp) => fmp.search.search({ query: 'AAPL', limit: 5 }) }, + // ---- sec ---- { category: 'sec', name: 'getRSSFeed()', schema: RSSFeedItemSchema, kind: 'array', call: (fmp) => fmp.sec.getRSSFeed({ limit: 5, type: '10-K', from: '2024-01-01', to: '2024-12-31', isDone: true }) }, { category: 'sec', name: 'getRSSFeedAll()', schema: RSSFeedAllItemSchema, kind: 'array', call: (fmp) => fmp.sec.getRSSFeedAll({ page: 0 }) }, diff --git a/packages/api/src/__tests__/endpoints/search.test.ts b/packages/api/src/__tests__/endpoints/search.test.ts new file mode 100644 index 0000000..2ed6bfa --- /dev/null +++ b/packages/api/src/__tests__/endpoints/search.test.ts @@ -0,0 +1,57 @@ +import { SearchEndpoints } from '../../endpoints/search'; +import { FMPClient } from '../../client'; + +// Mock the FMPClient +jest.mock('../../client'); + +describe('SearchEndpoints', () => { + let searchEndpoints: SearchEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + searchEndpoints = new SearchEndpoints(mockClient); + }); + + describe('search', () => { + it('should search using the /search endpoint (v3) with query params', async () => { + const mockResponse = { + success: true, + data: [ + { + symbol: 'AAPL', + name: 'Apple Inc.', + currency: 'USD', + stockExchange: 'NASDAQ Global Select', + exchangeShortName: 'NASDAQ', + }, + ], + error: null, + status: 200, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await searchEndpoints.search({ query: 'Apple', limit: 5, exchange: 'NASDAQ' }); + + expect(mockClient.get).toHaveBeenCalledWith('/search', 'v3', { + query: 'Apple', + limit: 5, + exchange: 'NASDAQ', + }); + expect(result).toEqual(mockResponse); + }); + + it('should pass undefined for optional params when omitted', async () => { + const mockResponse = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(mockResponse); + + await searchEndpoints.search({ query: 'TSLA' }); + + expect(mockClient.get).toHaveBeenCalledWith('/search', 'v3', { + query: 'TSLA', + limit: undefined, + exchange: undefined, + }); + }); + }); +}); diff --git a/packages/api/src/endpoints/search.ts b/packages/api/src/endpoints/search.ts new file mode 100644 index 0000000..487735c --- /dev/null +++ b/packages/api/src/endpoints/search.ts @@ -0,0 +1,38 @@ +// Search endpoints for FMP API + +import { FMPClient } from '@/client'; +import { APIResponse, SearchResult } from 'fmp-node-types'; + +export class SearchEndpoints { + constructor(private client: FMPClient) {} + + /** + * Search for stocks, ETFs, and other instruments by ticker symbol or company name. + * + * Resolves a free-text query (e.g. a company name or partial ticker) to matching + * symbols. Useful for turning a name like "Apple" into a ticker like "AAPL". + * + * @param params - Search parameters + * @param params.query - The search query (ticker or company name) + * @param params.limit - Maximum number of results to return (optional) + * @param params.exchange - Restrict results to a specific exchange (optional, e.g. "NASDAQ") + * + * @returns Promise resolving to an array of matching instruments + * + * @example + * ```typescript + * const results = await fmp.search.search({ query: 'Apple', limit: 5 }); + * const aapl = results.data?.find(r => r.symbol === 'AAPL'); + * ``` + * + * @see {@link https://site.financialmodelingprep.com/developer/docs#general-search|FMP Search Documentation} + */ + async search(params: { + query: string; + limit?: number; + exchange?: string; + }): Promise> { + const { query, limit, exchange } = params; + return this.client.get('/search', 'v3', { query, limit, exchange }); + } +} diff --git a/packages/api/src/fmp.ts b/packages/api/src/fmp.ts index 6613417..babc843 100644 --- a/packages/api/src/fmp.ts +++ b/packages/api/src/fmp.ts @@ -17,6 +17,7 @@ import { MutualFundEndpoints } from './endpoints/mutual-fund'; import { NewsEndpoints } from './endpoints/news'; import { QuoteEndpoints } from './endpoints/quote'; import { ScreenerEndpoints } from './endpoints/screener'; +import { SearchEndpoints } from './endpoints/search'; import { SECEndpoints } from './endpoints/sec'; import { SenateHouseEndpoints } from './endpoints/senate-house'; import { StockEndpoints } from './endpoints/stock'; @@ -65,6 +66,7 @@ export class FMP { public readonly news: NewsEndpoints; public readonly quote: QuoteEndpoints; public readonly screener: ScreenerEndpoints; + public readonly search: SearchEndpoints; public readonly sec: SECEndpoints; public readonly senateHouse: SenateHouseEndpoints; public readonly stock: StockEndpoints; @@ -99,6 +101,7 @@ export class FMP { this.news = new NewsEndpoints(client); this.quote = new QuoteEndpoints(client); this.screener = new ScreenerEndpoints(client); + this.search = new SearchEndpoints(client); this.sec = new SECEndpoints(client); this.senateHouse = new SenateHouseEndpoints(client); this.stock = new StockEndpoints(client); diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 2ee5488..9eab6e8 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -23,6 +23,7 @@ export { MutualFundEndpoints } from './endpoints/mutual-fund'; export { NewsEndpoints } from './endpoints/news'; export { QuoteEndpoints } from './endpoints/quote'; export { ScreenerEndpoints } from './endpoints/screener'; +export { SearchEndpoints } from './endpoints/search'; export { StockEndpoints } from './endpoints/stock'; export { SenateHouseEndpoints } from './endpoints/senate-house'; export { SECEndpoints } from './endpoints/sec'; diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index 9451f1e..99bfd3b 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -1,5 +1,18 @@ # fmp-ai-tools +## 0.2.0-beta.6 + +### Minor Changes + +- Add client configuration and new tools. + - **Client**: the FMP client is now memoized (no longer reconstructed on every tool call), and a new `configureFMPClient({ apiKey, timeout })` (exported from both entry points) lets consumers configure it instead of relying solely on the `FMP_API_KEY` environment variable. + - **New tools**: `getHistoricalPrice` and `getIntraday` (price history), `getStockNews` and `getStockNewsBySymbol` (news), `screenStocks` (screener), and `searchSymbol` (symbol search). High-volume tools apply a default result `limit` to keep outputs context-friendly. + +### Patch Changes + +- Updated dependencies + - fmp-node-api@0.2.0-beta.2 + ## 0.2.0-beta.5 ### Patch Changes diff --git a/packages/tools/README.md b/packages/tools/README.md index 7afd8d7..1dd84b7 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -96,6 +96,16 @@ Get your API key from [Financial Modeling Prep](https://site.financialmodelingpr The tools internally use the `fmp-node-api` library, which reads this environment variable to authenticate with the Financial Modeling Prep API. +### Configuring the client explicitly (optional) + +If you'd rather not use the environment variable, call `configureFMPClient` once at startup (available from either entry point). The client is memoized, so this only needs to happen once: + +```typescript +import { configureFMPClient } from 'fmp-ai-tools/vercel-ai'; // or 'fmp-ai-tools/openai' + +configureFMPClient({ apiKey: 'your_api_key_here', timeout: 15000 }); +``` + ### Debugging and Logging **⚠️ Development Only**: These logging features are intended for debugging and development, not production use. @@ -140,6 +150,8 @@ Logs: result summary and formatted JSON response data. ### Quote Tools - `getStockQuote` - Get real-time stock quote for a company +- `getHistoricalPrice` - Get historical daily prices (most recent `limit` days, default 30) +- `getIntraday` - Get intraday price bars at a given interval (most recent `limit` bars, default 50) ### Company Tools @@ -207,6 +219,19 @@ Logs: result summary and formatted JSON response data. - `getInsiderTrading` - Get insider trading data for a company +### News Tools + +- `getStockNews` - Get the latest general stock market news (most recent `limit`, default 20) +- `getStockNewsBySymbol` - Get the latest news for one or more symbols (default `limit` 20) + +### Screener Tools + +- `screenStocks` - Screen for stocks by market cap, price, sector, exchange, etc. (default `limit` 50) + +### Search Tools + +- `searchSymbol` - Resolve a company name or partial ticker to matching symbols (e.g., "Apple" → AAPL) + ## Using Individual Tools You can import and use specific tool categories or individual tools from either provider: diff --git a/packages/tools/package.json b/packages/tools/package.json index e4aa83d..eb9abcd 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.2.0-beta.5", + "version": "0.2.0-beta.6", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/tools/src/__tests__/client.test.ts b/packages/tools/src/__tests__/client.test.ts index bbd0f44..0cc4dcc 100644 --- a/packages/tools/src/__tests__/client.test.ts +++ b/packages/tools/src/__tests__/client.test.ts @@ -1,13 +1,42 @@ -jest.mock('fmp-node-api', () => ({ - FMP: jest.fn().mockImplementation(() => ({ __mockFmp: true })), -})); +const FMP = jest.fn().mockImplementation((config?: unknown) => ({ __mockFmp: true, config })); -import { getFMPClient } from '@/client'; +jest.mock('fmp-node-api', () => ({ FMP })); -describe('client.getFMPClient', () => { - it('should create an FMP client instance', () => { +import { getFMPClient, configureFMPClient, resetFMPClient } from '@/client'; + +describe('client', () => { + beforeEach(() => { + FMP.mockClear(); + resetFMPClient(); + }); + + it('creates an FMP client instance', () => { const client = getFMPClient(); expect(client).toBeDefined(); expect(typeof client).toBe('object'); }); + + it('memoizes the client across calls (constructs FMP once)', () => { + const a = getFMPClient(); + const b = getFMPClient(); + expect(a).toBe(b); + expect(FMP).toHaveBeenCalledTimes(1); + }); + + it('configureFMPClient passes config and rebuilds on next use', () => { + getFMPClient(); // build with default (undefined) config + expect(FMP).toHaveBeenLastCalledWith(undefined); + + configureFMPClient({ apiKey: 'test-key' }); + getFMPClient(); + expect(FMP).toHaveBeenLastCalledWith({ apiKey: 'test-key' }); + expect(FMP).toHaveBeenCalledTimes(2); + }); + + it('resetFMPClient clears the cache so a new instance is built', () => { + getFMPClient(); + resetFMPClient(); + getFMPClient(); + expect(FMP).toHaveBeenCalledTimes(2); + }); }); diff --git a/packages/tools/src/__tests__/definitions/consistency.test.ts b/packages/tools/src/__tests__/definitions/consistency.test.ts index 9de0c7d..c1f147c 100644 --- a/packages/tools/src/__tests__/definitions/consistency.test.ts +++ b/packages/tools/src/__tests__/definitions/consistency.test.ts @@ -7,9 +7,9 @@ import { fmpTools as openaiTools } from '@/providers/openai'; describe('cross-provider consistency', () => { const names = allDefinitions.map(d => d.name); - it('has 37 uniquely-named definitions', () => { - expect(names.length).toBe(37); - expect(new Set(names).size).toBe(37); + it('has 43 uniquely-named definitions', () => { + expect(names.length).toBe(43); + expect(new Set(names).size).toBe(43); }); it('Vercel AI exposes exactly the defined tools', () => { diff --git a/packages/tools/src/__tests__/providers/vercel-ai/news.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/news.test.ts new file mode 100644 index 0000000..81dac83 --- /dev/null +++ b/packages/tools/src/__tests__/providers/vercel-ai/news.test.ts @@ -0,0 +1,45 @@ +import { newsTools } from '@/providers/vercel-ai'; + +const mockNews = { + getStockNews: jest.fn(), + getStockNewsBySymbol: jest.fn(), +}; + +jest.mock('@/client', () => ({ + getFMPClient: jest.fn(() => ({ + news: mockNews, + })), +})); + +describe('Vercel AI News Tools (minimal)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('getStockNews passes the default limit and returns data', async () => { + mockNews.getStockNews.mockResolvedValueOnce({ data: [{ title: 'x' }] }); + + const result = await (newsTools.getStockNews.execute as any)({}); + + expect(mockNews.getStockNews).toHaveBeenCalledWith({ + from: undefined, + to: undefined, + limit: 20, + }); + expect(JSON.parse(result)).toEqual([{ title: 'x' }]); + }); + + it('getStockNewsBySymbol forwards symbols', async () => { + mockNews.getStockNewsBySymbol.mockResolvedValueOnce({ data: [{ title: 'y' }] }); + + const result = await (newsTools.getStockNewsBySymbol.execute as any)({ symbols: ['AAPL'] }); + + expect(mockNews.getStockNewsBySymbol).toHaveBeenCalledWith({ + symbols: ['AAPL'], + from: undefined, + to: undefined, + limit: 20, + }); + expect(JSON.parse(result)).toEqual([{ title: 'y' }]); + }); +}); diff --git a/packages/tools/src/__tests__/providers/vercel-ai/quote.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/quote.test.ts index 037ef4d..0137b30 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/quote.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/quote.test.ts @@ -2,6 +2,8 @@ import { quoteTools } from '@/providers/vercel-ai'; const mockQuote = { getQuote: jest.fn(), + getHistoricalPrice: jest.fn(), + getIntraday: jest.fn(), }; jest.mock('@/client', () => ({ @@ -23,4 +25,47 @@ describe('Vercel AI Quote Tools (minimal)', () => { expect(mockQuote.getQuote).toHaveBeenCalledWith('AAPL'); expect(JSON.parse(result)).toEqual({ symbol: 'AAPL' }); }); + + it('getHistoricalPrice caps the historical array to the limit', async () => { + const historical = Array.from({ length: 100 }, (_, i) => ({ date: `d${i}`, close: i })); + mockQuote.getHistoricalPrice.mockResolvedValueOnce({ + success: true, + data: { symbol: 'AAPL', historical }, + error: null, + status: 200, + }); + + const result = await (quoteTools.getHistoricalPrice.execute as any)({ symbol: 'AAPL', limit: 5 }); + + expect(mockQuote.getHistoricalPrice).toHaveBeenCalledWith({ + symbol: 'AAPL', + from: undefined, + to: undefined, + }); + expect(JSON.parse(result).historical).toHaveLength(5); + }); + + it('getIntraday caps the array to the limit', async () => { + const bars = Array.from({ length: 100 }, (_, i) => ({ date: `d${i}`, close: i })); + mockQuote.getIntraday.mockResolvedValueOnce({ + success: true, + data: bars, + error: null, + status: 200, + }); + + const result = await (quoteTools.getIntraday.execute as any)({ + symbol: 'AAPL', + interval: '5min', + limit: 10, + }); + + expect(mockQuote.getIntraday).toHaveBeenCalledWith({ + symbol: 'AAPL', + interval: '5min', + from: undefined, + to: undefined, + }); + expect(JSON.parse(result)).toHaveLength(10); + }); }); diff --git a/packages/tools/src/__tests__/providers/vercel-ai/screener.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/screener.test.ts new file mode 100644 index 0000000..00e5ec1 --- /dev/null +++ b/packages/tools/src/__tests__/providers/vercel-ai/screener.test.ts @@ -0,0 +1,34 @@ +import { screenerTools } from '@/providers/vercel-ai'; + +const mockScreener = { + getScreener: jest.fn(), +}; + +jest.mock('@/client', () => ({ + getFMPClient: jest.fn(() => ({ + screener: mockScreener, + })), +})); + +describe('Vercel AI Screener Tools (minimal)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('screenStocks drops null/undefined filters and applies the default limit', async () => { + mockScreener.getScreener.mockResolvedValueOnce({ data: [{ symbol: 'AAPL' }] }); + + const result = await (screenerTools.screenStocks.execute as any)({ + sector: 'Technology', + marketCapMoreThan: 1000000000, + industry: null, + }); + + expect(mockScreener.getScreener).toHaveBeenCalledWith({ + limit: 50, + sector: 'Technology', + marketCapMoreThan: 1000000000, + }); + expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); + }); +}); diff --git a/packages/tools/src/__tests__/providers/vercel-ai/search.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/search.test.ts new file mode 100644 index 0000000..8a0ddae --- /dev/null +++ b/packages/tools/src/__tests__/providers/vercel-ai/search.test.ts @@ -0,0 +1,30 @@ +import { searchTools } from '@/providers/vercel-ai'; + +const mockSearch = { + search: jest.fn(), +}; + +jest.mock('@/client', () => ({ + getFMPClient: jest.fn(() => ({ + search: mockSearch, + })), +})); + +describe('Vercel AI Search Tools (minimal)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('searchSymbol forwards the query with the default limit', async () => { + mockSearch.search.mockResolvedValueOnce({ data: [{ symbol: 'AAPL', name: 'Apple Inc.' }] }); + + const result = await (searchTools.searchSymbol.execute as any)({ query: 'Apple' }); + + expect(mockSearch.search).toHaveBeenCalledWith({ + query: 'Apple', + limit: 10, + exchange: undefined, + }); + expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL', name: 'Apple Inc.' }]); + }); +}); diff --git a/packages/tools/src/client.ts b/packages/tools/src/client.ts index 5797e6a..793dc98 100644 --- a/packages/tools/src/client.ts +++ b/packages/tools/src/client.ts @@ -1,10 +1,35 @@ /** - * Internal FMP API client instance - * Used internally by the tools, not exported publicly + * Internal FMP API client used by the tools. + * + * The client is memoized so tools reuse a single `FMP` instance instead of + * constructing one (and re-validating the API key) on every tool call. By + * default it reads the `FMP_API_KEY` environment variable; consumers can call + * `configureFMPClient(...)` once at startup to provide a key/timeout explicitly. */ -import { FMP } from 'fmp-node-api'; +import { FMP, type FMPConfig } from 'fmp-node-api'; -// Create a function to get the FMP client instance (internal use only) -export function getFMPClient() { - return new FMP(); +let cached: FMP | undefined; +let configured: FMPConfig | undefined; + +/** + * Configure the FMP client used by all tools. Optional — by default the client + * reads the `FMP_API_KEY` environment variable. Call once before using the tools. + */ +export function configureFMPClient(config: FMPConfig): void { + configured = config; + cached = undefined; // rebuilt lazily on next use +} + +/** Reset the memoized client and configuration (mainly for tests / serverless reuse). */ +export function resetFMPClient(): void { + cached = undefined; + configured = undefined; +} + +/** Get the memoized FMP client instance (internal use by tools). */ +export function getFMPClient(): FMP { + if (!cached) { + cached = new FMP(configured); + } + return cached; } diff --git a/packages/tools/src/definitions/index.ts b/packages/tools/src/definitions/index.ts index 58a9a47..1714814 100644 --- a/packages/tools/src/definitions/index.ts +++ b/packages/tools/src/definitions/index.ts @@ -7,6 +7,9 @@ import { etfDefinitions } from './etf'; import { insiderDefinitions } from './insider'; import { institutionalDefinitions } from './institutional'; import { marketDefinitions } from './market'; +import { newsDefinitions } from './news'; +import { screenerDefinitions } from './screener'; +import { searchDefinitions } from './search'; import { senateHouseDefinitions } from './senate-house'; import { stockDefinitions } from './stock'; import type { FMPToolDefinition } from './types'; @@ -23,6 +26,9 @@ export { insiderDefinitions, institutionalDefinitions, marketDefinitions, + newsDefinitions, + screenerDefinitions, + searchDefinitions, senateHouseDefinitions, stockDefinitions, }; @@ -38,6 +44,9 @@ export const allDefinitions: FMPToolDefinition[] = [ ...insiderDefinitions, ...institutionalDefinitions, ...marketDefinitions, + ...newsDefinitions, + ...screenerDefinitions, + ...searchDefinitions, ...senateHouseDefinitions, ...stockDefinitions, ]; diff --git a/packages/tools/src/definitions/news.ts b/packages/tools/src/definitions/news.ts new file mode 100644 index 0000000..00b3e03 --- /dev/null +++ b/packages/tools/src/definitions/news.ts @@ -0,0 +1,56 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +export const newsDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getStockNews', + description: 'Get the latest general stock market news articles', + inputSchema: z.object({ + from: z.string().optional().nullable().describe('Start date in YYYY-MM-DD format (optional)'), + to: z.string().optional().nullable().describe('End date in YYYY-MM-DD format (optional)'), + limit: z + .number() + .int() + .positive() + .default(20) + .describe('Max number of articles to return (default 20)'), + }), + execute: async ({ from, to, limit = 20 }) => + toToolResponse( + await getFMPClient().news.getStockNews({ + from: from ?? undefined, + to: to ?? undefined, + limit, + }), + ), + }), + defineTool({ + name: 'getStockNewsBySymbol', + description: 'Get the latest news articles for one or more specific stock symbols', + inputSchema: z.object({ + symbols: z + .array(z.string().min(1)) + .min(1) + .describe('Stock symbols to get news for (e.g., ["AAPL", "MSFT"])'), + from: z.string().optional().nullable().describe('Start date in YYYY-MM-DD format (optional)'), + to: z.string().optional().nullable().describe('End date in YYYY-MM-DD format (optional)'), + limit: z + .number() + .int() + .positive() + .default(20) + .describe('Max number of articles to return (default 20)'), + }), + execute: async ({ symbols, from, to, limit = 20 }) => + toToolResponse( + await getFMPClient().news.getStockNewsBySymbol({ + symbols, + from: from ?? undefined, + to: to ?? undefined, + limit, + }), + ), + }), +]; diff --git a/packages/tools/src/definitions/quote.ts b/packages/tools/src/definitions/quote.ts index 47f8f66..26d6cd0 100644 --- a/packages/tools/src/definitions/quote.ts +++ b/packages/tools/src/definitions/quote.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { getFMPClient } from '@/client'; import { toToolResponse } from '@/utils/format-response'; import { defineTool, type FMPToolDefinition } from './types'; +import type { APIResponse } from 'fmp-node-api'; export const quoteDefinitions: FMPToolDefinition[] = [ defineTool({ @@ -16,4 +17,68 @@ export const quoteDefinitions: FMPToolDefinition[] = [ }), execute: async ({ symbol }) => toToolResponse(await getFMPClient().quote.getQuote(symbol)), }), + defineTool({ + name: 'getHistoricalPrice', + description: + 'Get historical daily prices (open/high/low/close/volume) for a symbol. Returns the most recent `limit` days.', + inputSchema: z.object({ + symbol: z.string().min(1).describe('The stock/ETF symbol (e.g., AAPL)'), + from: z.string().optional().nullable().describe('Start date in YYYY-MM-DD format (optional)'), + to: z.string().optional().nullable().describe('End date in YYYY-MM-DD format (optional)'), + limit: z + .number() + .int() + .positive() + .default(30) + .describe('Max number of most-recent days to return (default 30)'), + }), + execute: async ({ symbol, from, to, limit = 30 }) => { + const res = await getFMPClient().quote.getHistoricalPrice({ + symbol, + from: from ?? undefined, + to: to ?? undefined, + }); + // Response is { symbol, historical: [...] } (newest first); cap to `limit`. + const data = res.data as { historical?: unknown[] } | null; + if (res.success && data && Array.isArray(data.historical)) { + return toToolResponse({ + ...res, + data: { ...data, historical: data.historical.slice(0, limit) }, + } as APIResponse); + } + return toToolResponse(res); + }, + }), + defineTool({ + name: 'getIntraday', + description: + 'Get intraday price bars for a symbol at a given interval. Returns the most recent `limit` bars.', + inputSchema: z.object({ + symbol: z.string().min(1).describe('The stock/ETF symbol (e.g., AAPL)'), + interval: z + .enum(['1min', '5min', '15min', '30min', '1hour', '4hour']) + .default('5min') + .describe('Bar interval'), + from: z.string().optional().nullable().describe('Start date in YYYY-MM-DD format (optional)'), + to: z.string().optional().nullable().describe('End date in YYYY-MM-DD format (optional)'), + limit: z + .number() + .int() + .positive() + .default(50) + .describe('Max number of most-recent bars to return (default 50)'), + }), + execute: async ({ symbol, interval = '5min', from, to, limit = 50 }) => { + const res = await getFMPClient().quote.getIntraday({ + symbol, + interval, + from: from ?? undefined, + to: to ?? undefined, + }); + if (res.success && Array.isArray(res.data)) { + return toToolResponse({ ...res, data: res.data.slice(0, limit) } as APIResponse); + } + return toToolResponse(res); + }, + }), ]; diff --git a/packages/tools/src/definitions/screener.ts b/packages/tools/src/definitions/screener.ts new file mode 100644 index 0000000..df4bad0 --- /dev/null +++ b/packages/tools/src/definitions/screener.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import type { ScreenerParams } from 'fmp-node-api'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +export const screenerDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'screenStocks', + description: + 'Screen for stocks matching financial criteria (market cap, price, sector, exchange, etc.). Returns matching companies.', + inputSchema: z.object({ + marketCapMoreThan: z.number().optional().nullable().describe('Minimum market capitalization'), + marketCapLowerThan: z.number().optional().nullable().describe('Maximum market capitalization'), + priceMoreThan: z.number().optional().nullable().describe('Minimum stock price'), + priceLowerThan: z.number().optional().nullable().describe('Maximum stock price'), + sector: z.string().optional().nullable().describe('Sector filter (e.g., "Technology")'), + industry: z.string().optional().nullable().describe('Industry filter'), + exchange: z.string().optional().nullable().describe('Exchange filter (e.g., "NASDAQ")'), + country: z.string().optional().nullable().describe('Country filter (e.g., "US")'), + isEtf: z.boolean().optional().nullable().describe('Restrict to ETFs'), + isActivelyTrading: z.boolean().optional().nullable().describe('Restrict to actively trading'), + limit: z + .number() + .int() + .positive() + .default(50) + .describe('Max number of results to return (default 50)'), + }), + execute: async ({ limit = 50, ...filters }) => { + // Drop null/undefined so they aren't sent as query params. + const params: Record = { limit }; + for (const [key, value] of Object.entries(filters)) { + if (value !== null && value !== undefined) params[key] = value; + } + return toToolResponse(await getFMPClient().screener.getScreener(params as ScreenerParams)); + }, + }), +]; diff --git a/packages/tools/src/definitions/search.ts b/packages/tools/src/definitions/search.ts new file mode 100644 index 0000000..2344fb2 --- /dev/null +++ b/packages/tools/src/definitions/search.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +export const searchDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'searchSymbol', + description: + 'Search for a ticker symbol by company name or partial ticker (e.g., resolve "Apple" to "AAPL")', + inputSchema: z.object({ + query: z.string().min(1).describe('Company name or ticker to search for'), + limit: z + .number() + .int() + .positive() + .default(10) + .describe('Max number of results to return (default 10)'), + exchange: z + .string() + .optional() + .nullable() + .describe('Restrict to a specific exchange (optional, e.g., "NASDAQ")'), + }), + execute: async ({ query, limit = 10, exchange }) => + toToolResponse( + await getFMPClient().search.search({ query, limit, exchange: exchange ?? undefined }), + ), + }), +]; diff --git a/packages/tools/src/providers/openai/index.ts b/packages/tools/src/providers/openai/index.ts index 58c1c22..6d56c01 100644 --- a/packages/tools/src/providers/openai/index.ts +++ b/packages/tools/src/providers/openai/index.ts @@ -1,5 +1,9 @@ import type { Tool } from '@openai/agents'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; + +// Re-export client configuration helpers (optional; tools default to FMP_API_KEY). +export { configureFMPClient, resetFMPClient } from '@/client'; + import { allDefinitions, quoteDefinitions, @@ -11,6 +15,9 @@ import { insiderDefinitions, institutionalDefinitions, marketDefinitions, + newsDefinitions, + screenerDefinitions, + searchDefinitions, senateHouseDefinitions, stockDefinitions, type FMPToolDefinition, @@ -26,6 +33,8 @@ const pick = (defs: FMPToolDefinition[]): Tool[] => defs.map(def => byN // Individual tools for direct import export const getStockQuote = byName.getStockQuote; +export const getHistoricalPrice = byName.getHistoricalPrice; +export const getIntraday = byName.getIntraday; export const getCompanyProfile = byName.getCompanyProfile; export const getCompanySharesFloat = byName.getCompanySharesFloat; export const getCompanyExecutiveCompensation = byName.getCompanyExecutiveCompensation; @@ -59,6 +68,10 @@ export const getSenateTradingByName = byName.getSenateTradingByName; export const getHouseTradingByName = byName.getHouseTradingByName; export const getSenateTradingRSSFeed = byName.getSenateTradingRSSFeed; export const getHouseTradingRSSFeed = byName.getHouseTradingRSSFeed; +export const getStockNews = byName.getStockNews; +export const getStockNewsBySymbol = byName.getStockNewsBySymbol; +export const screenStocks = byName.screenStocks; +export const searchSymbol = byName.searchSymbol; export const getMarketCap = byName.getMarketCap; export const getStockSplits = byName.getStockSplits; export const getDividendHistory = byName.getDividendHistory; @@ -73,6 +86,9 @@ export const etfTools = pick(etfDefinitions); export const insiderTools = pick(insiderDefinitions); export const institutionalTools = pick(institutionalDefinitions); export const marketTools = pick(marketDefinitions); +export const newsTools = pick(newsDefinitions); +export const screenerTools = pick(screenerDefinitions); +export const searchTools = pick(searchDefinitions); export const senateHouseTools = pick(senateHouseDefinitions); export const stockTools = pick(stockDefinitions); diff --git a/packages/tools/src/providers/vercel-ai/index.ts b/packages/tools/src/providers/vercel-ai/index.ts index 3163b28..54f2e97 100644 --- a/packages/tools/src/providers/vercel-ai/index.ts +++ b/packages/tools/src/providers/vercel-ai/index.ts @@ -1,5 +1,9 @@ import { ToolSet } from 'ai'; import { createTool } from '@/utils/aisdk-tool-wrapper'; + +// Re-export client configuration helpers (optional; tools default to FMP_API_KEY). +export { configureFMPClient, resetFMPClient } from '@/client'; + import { quoteDefinitions, companyDefinitions, @@ -10,6 +14,9 @@ import { insiderDefinitions, institutionalDefinitions, marketDefinitions, + newsDefinitions, + screenerDefinitions, + searchDefinitions, senateHouseDefinitions, stockDefinitions, type FMPToolDefinition, @@ -29,6 +36,9 @@ export const etfTools = toToolSet(etfDefinitions); export const insiderTools = toToolSet(insiderDefinitions); export const institutionalTools = toToolSet(institutionalDefinitions); export const marketTools = toToolSet(marketDefinitions); +export const newsTools = toToolSet(newsDefinitions); +export const screenerTools = toToolSet(screenerDefinitions); +export const searchTools = toToolSet(searchDefinitions); export const senateHouseTools = toToolSet(senateHouseDefinitions); export const stockTools = toToolSet(stockDefinitions); @@ -43,12 +53,15 @@ export const fmpTools: ToolSet = { ...insiderTools, ...institutionalTools, ...marketTools, + ...newsTools, + ...screenerTools, + ...searchTools, ...senateHouseTools, ...stockTools, }; // Individual tools for direct import -export const { getStockQuote } = quoteTools; +export const { getStockQuote, getHistoricalPrice, getIntraday } = quoteTools; export const { getCompanyProfile, getCompanySharesFloat, getCompanyExecutiveCompensation } = companyTools; export const { getEarningsCalendar, getEconomicCalendar } = calendarTools; @@ -79,4 +92,7 @@ export const { getSenateTradingRSSFeed, getHouseTradingRSSFeed, } = senateHouseTools; +export const { getStockNews, getStockNewsBySymbol } = newsTools; +export const { screenStocks } = screenerTools; +export const { searchSymbol } = searchTools; export const { getMarketCap, getStockSplits, getDividendHistory } = stockTools; diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index c6f5b3f..63830eb 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,11 @@ # fmp-node-types +## 0.2.0-beta.2 + +### Minor Changes + +- Add a `/search` endpoint: `fmp.search.search({ query, limit?, exchange? })` resolves a company name or partial ticker to matching symbols, returning the new `SearchResult` type. Wired into the live-API shape-check manifest. + ## 0.2.0-beta.1 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index b2f6d66..1f59426 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-types", - "version": "0.2.0-beta.1", + "version": "0.2.0-beta.2", "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 6df9abe..a0c4385 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -46,6 +46,9 @@ export * from './sec'; // Screener types export * from './screener'; +// Search types +export * from './search'; + // Senate house types export * from './senate-house'; diff --git a/packages/types/src/search.ts b/packages/types/src/search.ts new file mode 100644 index 0000000..0bb8247 --- /dev/null +++ b/packages/types/src/search.ts @@ -0,0 +1,14 @@ +// search types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +import { z } from "zod"; + +export const SearchResultSchema = z.object({ + symbol: z.string(), + name: z.string(), + currency: z.string().nullable(), + stockExchange: z.string().nullable(), + exchangeShortName: z.string().nullable() +}); + +export type SearchResult = z.infer; From 7030a688ed41da7f0c541fdee0e273fef77a8922 Mon Sep 17 00:00:00 2001 From: e-roy Date: Wed, 27 May 2026 14:28:52 -0400 Subject: [PATCH 08/13] feat: analyst, valuation & technical API categories + tools; live-verified release gate Add three fmp-node-api categories (live-verified against the stable API): - analyst: getEstimates, getPriceTargetConsensus, getPriceTargetSummary, getGrades - valuation: getDiscountedCashFlow, getRatingSnapshot, getHistoricalRating - technical: getTechnicalIndicator (SMA/EMA/RSI/etc.) Plus six matching AI tools (count 43 -> 49), client memoization + configureFMPClient, and price-history/news/screener/search tools from the prior round. Schemas corrected to the real stable responses (analyst field names, price-target-summary *Count fields, DCF "Stock Price" key, optional rating date); getEstimates defaults period. Process: wire the live API shape-check (test:live) into the release path as a hard gate -- publish-packages runs it before version/publish, and the CI publish job runs it as its own step -- so a renamed/removed route can no longer ship. Always release via pnpm publish-packages. Versions: fmp-node-types/fmp-node-api 0.2.0-beta.4, fmp-ai-tools 0.2.0-beta.8. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/analyst-valuation-technical-api.md | 12 ++++ .../analyst-valuation-technical-tools.md | 5 ++ .changeset/fix-analyst-valuation-schemas.md | 12 ++++ .changeset/pre.json | 3 + .github/workflows/ci.yml | 11 +++- CLAUDE.md | 2 +- .../src/app/docs/tools/categories/page.mdx | 35 ++++++++++ apps/docs/src/app/docs/tools/openai/page.mdx | 12 ++++ .../src/app/docs/tools/vercel-ai/page.mdx | 12 ++++ package.json | 2 +- packages/api/CHANGELOG.md | 22 +++++++ packages/api/package.json | 2 +- packages/api/scripts/live/manifest.ts | 27 ++++++++ .../src/__tests__/endpoints/analyst.test.ts | 47 ++++++++++++++ .../src/__tests__/endpoints/technical.test.ts | 32 +++++++++ .../src/__tests__/endpoints/valuation.test.ts | 48 ++++++++++++++ packages/api/src/endpoints/analyst.ts | 50 ++++++++++++++ packages/api/src/endpoints/technical.ts | 52 +++++++++++++++ packages/api/src/endpoints/valuation.ts | 27 ++++++++ packages/api/src/fmp.ts | 9 +++ packages/api/src/index.ts | 3 + packages/tools/CHANGELOG.md | 18 +++++ packages/tools/README.md | 15 +++++ packages/tools/package.json | 2 +- .../__tests__/definitions/consistency.test.ts | 6 +- .../providers/vercel-ai/analyst.test.ts | 56 ++++++++++++++++ .../providers/vercel-ai/technical.test.ts | 42 ++++++++++++ .../providers/vercel-ai/valuation.test.ts | 36 ++++++++++ packages/tools/src/definitions/analyst.ts | 59 +++++++++++++++++ packages/tools/src/definitions/index.ts | 9 +++ packages/tools/src/definitions/technical.ts | 47 ++++++++++++++ packages/tools/src/definitions/valuation.ts | 26 ++++++++ packages/tools/src/providers/openai/index.ts | 12 ++++ .../tools/src/providers/vercel-ai/index.ts | 12 ++++ packages/types/CHANGELOG.md | 22 +++++++ packages/types/package.json | 2 +- packages/types/src/analyst.ts | 65 +++++++++++++++++++ packages/types/src/index.ts | 9 +++ packages/types/src/technical.ts | 27 ++++++++ packages/types/src/valuation.ts | 30 +++++++++ 40 files changed, 910 insertions(+), 10 deletions(-) create mode 100644 .changeset/analyst-valuation-technical-api.md create mode 100644 .changeset/analyst-valuation-technical-tools.md create mode 100644 .changeset/fix-analyst-valuation-schemas.md create mode 100644 packages/api/src/__tests__/endpoints/analyst.test.ts create mode 100644 packages/api/src/__tests__/endpoints/technical.test.ts create mode 100644 packages/api/src/__tests__/endpoints/valuation.test.ts create mode 100644 packages/api/src/endpoints/analyst.ts create mode 100644 packages/api/src/endpoints/technical.ts create mode 100644 packages/api/src/endpoints/valuation.ts create mode 100644 packages/tools/src/__tests__/providers/vercel-ai/analyst.test.ts create mode 100644 packages/tools/src/__tests__/providers/vercel-ai/technical.test.ts create mode 100644 packages/tools/src/__tests__/providers/vercel-ai/valuation.test.ts create mode 100644 packages/tools/src/definitions/analyst.ts create mode 100644 packages/tools/src/definitions/technical.ts create mode 100644 packages/tools/src/definitions/valuation.ts create mode 100644 packages/types/src/analyst.ts create mode 100644 packages/types/src/technical.ts create mode 100644 packages/types/src/valuation.ts diff --git a/.changeset/analyst-valuation-technical-api.md b/.changeset/analyst-valuation-technical-api.md new file mode 100644 index 0000000..13535be --- /dev/null +++ b/.changeset/analyst-valuation-technical-api.md @@ -0,0 +1,12 @@ +--- +"fmp-node-types": minor +"fmp-node-api": minor +--- + +Add three new endpoint categories: + +- **`fmp.analyst`** — `getEstimates`, `getPriceTargetConsensus`, `getPriceTargetSummary`, `getGrades`. +- **`fmp.valuation`** — `getDiscountedCashFlow`, `getRatingSnapshot`, `getHistoricalRating`. +- **`fmp.technical`** — `getTechnicalIndicator` (SMA/EMA/RSI/etc. via a `type` param). + +Adds the matching `SearchResult`-style types and live-API shape-check manifest cases. diff --git a/.changeset/analyst-valuation-technical-tools.md b/.changeset/analyst-valuation-technical-tools.md new file mode 100644 index 0000000..3d8d2cd --- /dev/null +++ b/.changeset/analyst-valuation-technical-tools.md @@ -0,0 +1,5 @@ +--- +"fmp-ai-tools": minor +--- + +Add AI tools for the new analyst, valuation, and technical endpoints: `getAnalystEstimates`, `getPriceTargetConsensus`, `getStockGrades`, `getDiscountedCashFlow`, `getCompanyRating`, and `getTechnicalIndicator`. List-returning tools apply a default result `limit`. diff --git a/.changeset/fix-analyst-valuation-schemas.md b/.changeset/fix-analyst-valuation-schemas.md new file mode 100644 index 0000000..fc42dcc --- /dev/null +++ b/.changeset/fix-analyst-valuation-schemas.md @@ -0,0 +1,12 @@ +--- +"fmp-node-types": patch +"fmp-node-api": patch +--- + +Correct the analyst/valuation schemas to match the live FMP `stable` API (verified): + +- **AnalystEstimate**: fields drop the `estimated` prefix and use the real names (`revenueLow/High/Avg`, `ebitda*`, `ebit*`, `netIncome*`, `sgaExpense*`, `epsAvg/High/Low`, `numAnalystsRevenue`, `numAnalystsEps`). +- **PriceTargetSummary**: count fields are `lastMonthCount`/`lastQuarterCount`/`lastYearCount`/`allTimeCount`. +- **DCFValuation**: the price field is keyed `"Stock Price"` (with a space). +- **CompanyRating**: gains an optional `date` (present on historical rows). +- `analyst.getEstimates` now defaults `period` to `annual` (the `stable` endpoint returns 400 without it). diff --git a/.changeset/pre.json b/.changeset/pre.json index 1962011..528f70e 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -10,7 +10,10 @@ "fmp-node-types": "0.1.4" }, "changesets": [ + "analyst-valuation-technical-api", + "analyst-valuation-technical-tools", "error-handling", + "fix-analyst-valuation-schemas", "loosen-tool-dep-ranges", "openai-bundler-safe", "search-endpoint", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b3fd5d..8e2c669 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,7 +104,14 @@ jobs: - name: Build packages run: pnpm build - - name: Publish packages - run: pnpm publish-packages + # Release gate: validate the live FMP API against the canonical schemas before + # publishing, so a broken/renamed route can never ship. Kept as its own step + # (rather than relying on publish-packages) so CI publishes are gated + # independently of how the publish command is invoked. + - name: Live API drift check (release gate) + run: pnpm test:live + + - name: Version and publish + run: pnpm exec changeset version && pnpm exec changeset publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CLAUDE.md b/CLAUDE.md index cb20f8c..c1cfebe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,7 +83,7 @@ To add a tool: add one `defineTool({...})` to the relevant `src/definitions/ fmp.search.search({ query: 'AAPL', limit: 5 }) }, + // ---- analyst ---- + { category: 'analyst', name: 'getEstimates(AAPL,annual,2)', schema: AnalystEstimateSchema, kind: 'array', call: (fmp) => fmp.analyst.getEstimates({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, + { category: 'analyst', name: 'getPriceTargetConsensus(AAPL)', schema: PriceTargetConsensusSchema, kind: 'object', call: (fmp) => fmp.analyst.getPriceTargetConsensus({ symbol: 'AAPL' }) }, + { category: 'analyst', name: 'getPriceTargetSummary(AAPL)', schema: PriceTargetSummarySchema, kind: 'object', call: (fmp) => fmp.analyst.getPriceTargetSummary({ symbol: 'AAPL' }) }, + { category: 'analyst', name: 'getGrades(AAPL)', schema: StockGradeSchema, kind: 'array', call: (fmp) => fmp.analyst.getGrades({ symbol: 'AAPL' }) }, + + // ---- valuation ---- + { category: 'valuation', name: 'getDiscountedCashFlow(AAPL)', schema: DCFValuationSchema, kind: 'object', call: (fmp) => fmp.valuation.getDiscountedCashFlow({ symbol: 'AAPL' }) }, + { category: 'valuation', name: 'getRatingSnapshot(AAPL)', schema: CompanyRatingSchema, kind: 'object', call: (fmp) => fmp.valuation.getRatingSnapshot({ symbol: 'AAPL' }) }, + { category: 'valuation', name: 'getHistoricalRating(AAPL,2)', schema: CompanyRatingSchema, kind: 'array', call: (fmp) => fmp.valuation.getHistoricalRating({ symbol: 'AAPL', limit: 2 }) }, + + // ---- technical ---- + { category: 'technical', name: 'getTechnicalIndicator(AAPL,sma,10,1day)', schema: TechnicalIndicatorSchema, kind: 'array', call: (fmp) => fmp.technical.getTechnicalIndicator({ symbol: 'AAPL', type: 'sma', periodLength: 10, timeframe: '1day' }) }, + // ---- sec ---- { category: 'sec', name: 'getRSSFeed()', schema: RSSFeedItemSchema, kind: 'array', call: (fmp) => fmp.sec.getRSSFeed({ limit: 5, type: '10-K', from: '2024-01-01', to: '2024-12-31', isDone: true }) }, { category: 'sec', name: 'getRSSFeedAll()', schema: RSSFeedAllItemSchema, kind: 'array', call: (fmp) => fmp.sec.getRSSFeedAll({ page: 0 }) }, diff --git a/packages/api/src/__tests__/endpoints/analyst.test.ts b/packages/api/src/__tests__/endpoints/analyst.test.ts new file mode 100644 index 0000000..95d2f76 --- /dev/null +++ b/packages/api/src/__tests__/endpoints/analyst.test.ts @@ -0,0 +1,47 @@ +import { AnalystEndpoints } from '../../endpoints/analyst'; +import { FMPClient } from '../../client'; + +jest.mock('../../client'); + +describe('AnalystEndpoints', () => { + let analyst: AnalystEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + analyst = new AnalystEndpoints(mockClient); + }); + + it('getEstimates uses /analyst-estimates (stable) with params', async () => { + const res = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(res); + + await analyst.getEstimates({ symbol: 'AAPL', period: 'annual', limit: 2 }); + + expect(mockClient.get).toHaveBeenCalledWith('/analyst-estimates', 'stable', { + symbol: 'AAPL', + period: 'annual', + limit: 2, + }); + }); + + it('getPriceTargetConsensus uses getSingle on /price-target-consensus', async () => { + const res = { success: true, data: {}, error: null, status: 200 }; + mockClient.getSingle.mockResolvedValue(res); + + await analyst.getPriceTargetConsensus({ symbol: 'AAPL' }); + + expect(mockClient.getSingle).toHaveBeenCalledWith('/price-target-consensus', 'stable', { + symbol: 'AAPL', + }); + }); + + it('getGrades uses /grades (stable)', async () => { + const res = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(res); + + await analyst.getGrades({ symbol: 'AAPL' }); + + expect(mockClient.get).toHaveBeenCalledWith('/grades', 'stable', { symbol: 'AAPL' }); + }); +}); diff --git a/packages/api/src/__tests__/endpoints/technical.test.ts b/packages/api/src/__tests__/endpoints/technical.test.ts new file mode 100644 index 0000000..6a9116b --- /dev/null +++ b/packages/api/src/__tests__/endpoints/technical.test.ts @@ -0,0 +1,32 @@ +import { TechnicalEndpoints } from '../../endpoints/technical'; +import { FMPClient } from '../../client'; + +jest.mock('../../client'); + +describe('TechnicalEndpoints', () => { + let technical: TechnicalEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + technical = new TechnicalEndpoints(mockClient); + }); + + it('getTechnicalIndicator puts the type in the path and passes params (stable)', async () => { + const res = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(res); + + await technical.getTechnicalIndicator({ + symbol: 'AAPL', + type: 'rsi', + periodLength: 14, + timeframe: '1day', + }); + + expect(mockClient.get).toHaveBeenCalledWith('/technical-indicators/rsi', 'stable', { + symbol: 'AAPL', + periodLength: 14, + timeframe: '1day', + }); + }); +}); diff --git a/packages/api/src/__tests__/endpoints/valuation.test.ts b/packages/api/src/__tests__/endpoints/valuation.test.ts new file mode 100644 index 0000000..497481c --- /dev/null +++ b/packages/api/src/__tests__/endpoints/valuation.test.ts @@ -0,0 +1,48 @@ +import { ValuationEndpoints } from '../../endpoints/valuation'; +import { FMPClient } from '../../client'; + +jest.mock('../../client'); + +describe('ValuationEndpoints', () => { + let valuation: ValuationEndpoints; + let mockClient: jest.Mocked; + + beforeEach(() => { + mockClient = new FMPClient({ apiKey: 'test-key' }) as jest.Mocked; + valuation = new ValuationEndpoints(mockClient); + }); + + it('getDiscountedCashFlow uses getSingle on /discounted-cash-flow', async () => { + const res = { success: true, data: {}, error: null, status: 200 }; + mockClient.getSingle.mockResolvedValue(res); + + await valuation.getDiscountedCashFlow({ symbol: 'AAPL' }); + + expect(mockClient.getSingle).toHaveBeenCalledWith('/discounted-cash-flow', 'stable', { + symbol: 'AAPL', + }); + }); + + it('getRatingSnapshot uses getSingle on /ratings-snapshot', async () => { + const res = { success: true, data: {}, error: null, status: 200 }; + mockClient.getSingle.mockResolvedValue(res); + + await valuation.getRatingSnapshot({ symbol: 'AAPL' }); + + expect(mockClient.getSingle).toHaveBeenCalledWith('/ratings-snapshot', 'stable', { + symbol: 'AAPL', + }); + }); + + it('getHistoricalRating uses /ratings-historical (stable) with limit', async () => { + const res = { success: true, data: [], error: null, status: 200 }; + mockClient.get.mockResolvedValue(res); + + await valuation.getHistoricalRating({ symbol: 'AAPL', limit: 5 }); + + expect(mockClient.get).toHaveBeenCalledWith('/ratings-historical', 'stable', { + symbol: 'AAPL', + limit: 5, + }); + }); +}); diff --git a/packages/api/src/endpoints/analyst.ts b/packages/api/src/endpoints/analyst.ts new file mode 100644 index 0000000..fa2205c --- /dev/null +++ b/packages/api/src/endpoints/analyst.ts @@ -0,0 +1,50 @@ +// Analyst endpoints for FMP API + +import { FMPClient } from '@/client'; +import { + APIResponse, + AnalystEstimate, + PriceTargetConsensus, + PriceTargetSummary, + StockGrade, +} from 'fmp-node-types'; + +export class AnalystEndpoints { + constructor(private client: FMPClient) {} + + /** + * Get analyst estimates (revenue, EBITDA, net income, EPS) for a company. + * + * @param params.symbol - Stock symbol + * @param params.period - 'annual' or 'quarter' (optional) + * @param params.limit - Max number of periods (optional) + */ + async getEstimates(params: { + symbol: string; + period?: 'annual' | 'quarter'; + limit?: number; + }): Promise> { + // `stable` /analyst-estimates returns 400 without a period, so default it. + const { symbol, period = 'annual', limit } = params; + return this.client.get('/analyst-estimates', 'stable', { symbol, period, limit }); + } + + /** Get the analyst price-target consensus (high/low/consensus/median) for a company. */ + async getPriceTargetConsensus(params: { + symbol: string; + }): Promise> { + return this.client.getSingle('/price-target-consensus', 'stable', { symbol: params.symbol }); + } + + /** Get a summary of analyst price targets over recent periods for a company. */ + async getPriceTargetSummary(params: { + symbol: string; + }): Promise> { + return this.client.getSingle('/price-target-summary', 'stable', { symbol: params.symbol }); + } + + /** Get analyst grades (upgrades/downgrades) for a company. */ + async getGrades(params: { symbol: string }): Promise> { + return this.client.get('/grades', 'stable', { symbol: params.symbol }); + } +} diff --git a/packages/api/src/endpoints/technical.ts b/packages/api/src/endpoints/technical.ts new file mode 100644 index 0000000..04e63ba --- /dev/null +++ b/packages/api/src/endpoints/technical.ts @@ -0,0 +1,52 @@ +// Technical indicator endpoints for FMP API + +import { FMPClient } from '@/client'; +import { APIResponse, TechnicalIndicator } from 'fmp-node-types'; + +export type TechnicalIndicatorType = + | 'sma' + | 'ema' + | 'wma' + | 'dema' + | 'tema' + | 'rsi' + | 'standardDeviation' + | 'williams' + | 'adx'; + +export type TechnicalTimeframe = + | '1min' + | '5min' + | '15min' + | '30min' + | '1hour' + | '4hour' + | '1day' + | '1week' + | '1month'; + +export class TechnicalEndpoints { + constructor(private client: FMPClient) {} + + /** + * Get a technical indicator series for a symbol. + * + * @param params.symbol - Stock/ETF symbol + * @param params.type - Indicator type (sma, ema, rsi, ...) + * @param params.periodLength - Lookback period for the indicator (e.g. 10) + * @param params.timeframe - Bar timeframe (e.g. '1day') + */ + async getTechnicalIndicator(params: { + symbol: string; + type: TechnicalIndicatorType; + periodLength: number; + timeframe: TechnicalTimeframe; + }): Promise> { + const { symbol, type, periodLength, timeframe } = params; + return this.client.get(`/technical-indicators/${type}`, 'stable', { + symbol, + periodLength, + timeframe, + }); + } +} diff --git a/packages/api/src/endpoints/valuation.ts b/packages/api/src/endpoints/valuation.ts new file mode 100644 index 0000000..a1315bb --- /dev/null +++ b/packages/api/src/endpoints/valuation.ts @@ -0,0 +1,27 @@ +// Valuation endpoints for FMP API (discounted cash flow + company ratings) + +import { FMPClient } from '@/client'; +import { APIResponse, DCFValuation, CompanyRating } from 'fmp-node-types'; + +export class ValuationEndpoints { + constructor(private client: FMPClient) {} + + /** Get the discounted-cash-flow (DCF) fair-value estimate for a company. */ + async getDiscountedCashFlow(params: { symbol: string }): Promise> { + return this.client.getSingle('/discounted-cash-flow', 'stable', { symbol: params.symbol }); + } + + /** Get FMP's current rating/score snapshot for a company. */ + async getRatingSnapshot(params: { symbol: string }): Promise> { + return this.client.getSingle('/ratings-snapshot', 'stable', { symbol: params.symbol }); + } + + /** Get FMP's historical ratings for a company. */ + async getHistoricalRating(params: { + symbol: string; + limit?: number; + }): Promise> { + const { symbol, limit } = params; + return this.client.get('/ratings-historical', 'stable', { symbol, limit }); + } +} diff --git a/packages/api/src/fmp.ts b/packages/api/src/fmp.ts index babc843..0004522 100644 --- a/packages/api/src/fmp.ts +++ b/packages/api/src/fmp.ts @@ -18,6 +18,9 @@ import { NewsEndpoints } from './endpoints/news'; import { QuoteEndpoints } from './endpoints/quote'; import { ScreenerEndpoints } from './endpoints/screener'; import { SearchEndpoints } from './endpoints/search'; +import { AnalystEndpoints } from './endpoints/analyst'; +import { ValuationEndpoints } from './endpoints/valuation'; +import { TechnicalEndpoints } from './endpoints/technical'; import { SECEndpoints } from './endpoints/sec'; import { SenateHouseEndpoints } from './endpoints/senate-house'; import { StockEndpoints } from './endpoints/stock'; @@ -67,6 +70,9 @@ export class FMP { public readonly quote: QuoteEndpoints; public readonly screener: ScreenerEndpoints; public readonly search: SearchEndpoints; + public readonly analyst: AnalystEndpoints; + public readonly valuation: ValuationEndpoints; + public readonly technical: TechnicalEndpoints; public readonly sec: SECEndpoints; public readonly senateHouse: SenateHouseEndpoints; public readonly stock: StockEndpoints; @@ -102,6 +108,9 @@ export class FMP { this.quote = new QuoteEndpoints(client); this.screener = new ScreenerEndpoints(client); this.search = new SearchEndpoints(client); + this.analyst = new AnalystEndpoints(client); + this.valuation = new ValuationEndpoints(client); + this.technical = new TechnicalEndpoints(client); this.sec = new SECEndpoints(client); this.senateHouse = new SenateHouseEndpoints(client); this.stock = new StockEndpoints(client); diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 9eab6e8..5b72094 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -24,6 +24,9 @@ export { NewsEndpoints } from './endpoints/news'; export { QuoteEndpoints } from './endpoints/quote'; export { ScreenerEndpoints } from './endpoints/screener'; export { SearchEndpoints } from './endpoints/search'; +export { AnalystEndpoints } from './endpoints/analyst'; +export { ValuationEndpoints } from './endpoints/valuation'; +export { TechnicalEndpoints } from './endpoints/technical'; export { StockEndpoints } from './endpoints/stock'; export { SenateHouseEndpoints } from './endpoints/senate-house'; export { SECEndpoints } from './endpoints/sec'; diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index 99bfd3b..475adb0 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -1,5 +1,23 @@ # fmp-ai-tools +## 0.2.0-beta.8 + +### Patch Changes + +- Updated dependencies + - fmp-node-api@0.2.0-beta.4 + +## 0.2.0-beta.7 + +### Minor Changes + +- Add AI tools for the new analyst, valuation, and technical endpoints: `getAnalystEstimates`, `getPriceTargetConsensus`, `getStockGrades`, `getDiscountedCashFlow`, `getCompanyRating`, and `getTechnicalIndicator`. List-returning tools apply a default result `limit`. + +### Patch Changes + +- Updated dependencies + - fmp-node-api@0.2.0-beta.3 + ## 0.2.0-beta.6 ### Minor Changes diff --git a/packages/tools/README.md b/packages/tools/README.md index 1dd84b7..f36973c 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -232,6 +232,21 @@ Logs: result summary and formatted JSON response data. - `searchSymbol` - Resolve a company name or partial ticker to matching symbols (e.g., "Apple" → AAPL) +### Analyst Tools + +- `getAnalystEstimates` - Analyst estimates (revenue, EBITDA, net income, EPS) by period (default `limit` 10) +- `getPriceTargetConsensus` - Analyst price-target consensus (high/low/consensus/median) +- `getStockGrades` - Recent analyst grades / upgrades & downgrades (default `limit` 20) + +### Valuation Tools + +- `getDiscountedCashFlow` - Discounted-cash-flow fair value vs. current price +- `getCompanyRating` - FMP's current rating/score snapshot for a company + +### Technical Tools + +- `getTechnicalIndicator` - Indicator series (SMA, EMA, RSI, etc.) at a chosen timeframe (default `limit` 50) + ## Using Individual Tools You can import and use specific tool categories or individual tools from either provider: diff --git a/packages/tools/package.json b/packages/tools/package.json index eb9abcd..ed53fff 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.2.0-beta.6", + "version": "0.2.0-beta.8", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/tools/src/__tests__/definitions/consistency.test.ts b/packages/tools/src/__tests__/definitions/consistency.test.ts index c1f147c..49fb5e1 100644 --- a/packages/tools/src/__tests__/definitions/consistency.test.ts +++ b/packages/tools/src/__tests__/definitions/consistency.test.ts @@ -7,9 +7,9 @@ import { fmpTools as openaiTools } from '@/providers/openai'; describe('cross-provider consistency', () => { const names = allDefinitions.map(d => d.name); - it('has 43 uniquely-named definitions', () => { - expect(names.length).toBe(43); - expect(new Set(names).size).toBe(43); + it('has 49 uniquely-named definitions', () => { + expect(names.length).toBe(49); + expect(new Set(names).size).toBe(49); }); it('Vercel AI exposes exactly the defined tools', () => { diff --git a/packages/tools/src/__tests__/providers/vercel-ai/analyst.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/analyst.test.ts new file mode 100644 index 0000000..d7d25b6 --- /dev/null +++ b/packages/tools/src/__tests__/providers/vercel-ai/analyst.test.ts @@ -0,0 +1,56 @@ +import { analystTools } from '@/providers/vercel-ai'; + +const mockAnalyst = { + getEstimates: jest.fn(), + getPriceTargetConsensus: jest.fn(), + getGrades: jest.fn(), +}; + +jest.mock('@/client', () => ({ + getFMPClient: jest.fn(() => ({ + analyst: mockAnalyst, + })), +})); + +describe('Vercel AI Analyst Tools (minimal)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('getAnalystEstimates applies period/limit defaults', async () => { + mockAnalyst.getEstimates.mockResolvedValueOnce({ data: [{ symbol: 'AAPL' }] }); + + const result = await (analystTools.getAnalystEstimates.execute as any)({ symbol: 'AAPL' }); + + expect(mockAnalyst.getEstimates).toHaveBeenCalledWith({ + symbol: 'AAPL', + period: 'annual', + limit: 10, + }); + expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); + }); + + it('getPriceTargetConsensus forwards the symbol', async () => { + mockAnalyst.getPriceTargetConsensus.mockResolvedValueOnce({ data: { symbol: 'AAPL' } }); + + const result = await (analystTools.getPriceTargetConsensus.execute as any)({ symbol: 'AAPL' }); + + expect(mockAnalyst.getPriceTargetConsensus).toHaveBeenCalledWith({ symbol: 'AAPL' }); + expect(JSON.parse(result)).toEqual({ symbol: 'AAPL' }); + }); + + it('getStockGrades caps the array to the limit', async () => { + const grades = Array.from({ length: 40 }, (_, i) => ({ newGrade: `g${i}` })); + mockAnalyst.getGrades.mockResolvedValueOnce({ + success: true, + data: grades, + error: null, + status: 200, + }); + + const result = await (analystTools.getStockGrades.execute as any)({ symbol: 'AAPL', limit: 5 }); + + expect(mockAnalyst.getGrades).toHaveBeenCalledWith({ symbol: 'AAPL' }); + expect(JSON.parse(result)).toHaveLength(5); + }); +}); diff --git a/packages/tools/src/__tests__/providers/vercel-ai/technical.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/technical.test.ts new file mode 100644 index 0000000..c9f3345 --- /dev/null +++ b/packages/tools/src/__tests__/providers/vercel-ai/technical.test.ts @@ -0,0 +1,42 @@ +import { technicalTools } from '@/providers/vercel-ai'; + +const mockTechnical = { + getTechnicalIndicator: jest.fn(), +}; + +jest.mock('@/client', () => ({ + getFMPClient: jest.fn(() => ({ + technical: mockTechnical, + })), +})); + +describe('Vercel AI Technical Tools (minimal)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('getTechnicalIndicator applies defaults and caps the array to the limit', async () => { + const bars = Array.from({ length: 100 }, (_, i) => ({ date: `d${i}`, rsi: i })); + mockTechnical.getTechnicalIndicator.mockResolvedValueOnce({ + success: true, + data: bars, + error: null, + status: 200, + }); + + const result = await (technicalTools.getTechnicalIndicator.execute as any)({ + symbol: 'AAPL', + type: 'rsi', + periodLength: 14, + limit: 10, + }); + + expect(mockTechnical.getTechnicalIndicator).toHaveBeenCalledWith({ + symbol: 'AAPL', + type: 'rsi', + periodLength: 14, + timeframe: '1day', + }); + expect(JSON.parse(result)).toHaveLength(10); + }); +}); diff --git a/packages/tools/src/__tests__/providers/vercel-ai/valuation.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/valuation.test.ts new file mode 100644 index 0000000..073c96c --- /dev/null +++ b/packages/tools/src/__tests__/providers/vercel-ai/valuation.test.ts @@ -0,0 +1,36 @@ +import { valuationTools } from '@/providers/vercel-ai'; + +const mockValuation = { + getDiscountedCashFlow: jest.fn(), + getRatingSnapshot: jest.fn(), +}; + +jest.mock('@/client', () => ({ + getFMPClient: jest.fn(() => ({ + valuation: mockValuation, + })), +})); + +describe('Vercel AI Valuation Tools (minimal)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('getDiscountedCashFlow forwards the symbol', async () => { + mockValuation.getDiscountedCashFlow.mockResolvedValueOnce({ data: { symbol: 'AAPL', dcf: 200 } }); + + const result = await (valuationTools.getDiscountedCashFlow.execute as any)({ symbol: 'AAPL' }); + + expect(mockValuation.getDiscountedCashFlow).toHaveBeenCalledWith({ symbol: 'AAPL' }); + expect(JSON.parse(result)).toEqual({ symbol: 'AAPL', dcf: 200 }); + }); + + it('getCompanyRating calls the rating snapshot', async () => { + mockValuation.getRatingSnapshot.mockResolvedValueOnce({ data: { symbol: 'AAPL', rating: 'A' } }); + + const result = await (valuationTools.getCompanyRating.execute as any)({ symbol: 'AAPL' }); + + expect(mockValuation.getRatingSnapshot).toHaveBeenCalledWith({ symbol: 'AAPL' }); + expect(JSON.parse(result)).toEqual({ symbol: 'AAPL', rating: 'A' }); + }); +}); diff --git a/packages/tools/src/definitions/analyst.ts b/packages/tools/src/definitions/analyst.ts new file mode 100644 index 0000000..1611e1f --- /dev/null +++ b/packages/tools/src/definitions/analyst.ts @@ -0,0 +1,59 @@ +import { z } from 'zod'; +import type { APIResponse } from 'fmp-node-api'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +export const analystDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getAnalystEstimates', + description: + 'Get analyst estimates (revenue, EBITDA, net income, EPS) for a company, by period', + inputSchema: z.object({ + symbol: z.string().min(1).describe('The stock symbol (e.g., AAPL)'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z + .number() + .int() + .positive() + .default(10) + .describe('Max number of periods to return (default 10)'), + }), + execute: async ({ symbol, period = 'annual', limit = 10 }) => + toToolResponse(await getFMPClient().analyst.getEstimates({ symbol, period, limit })), + }), + defineTool({ + name: 'getPriceTargetConsensus', + description: + 'Get the analyst price-target consensus (high, low, consensus, median) for a company', + inputSchema: z.object({ + symbol: z.string().min(1).describe('The stock symbol (e.g., AAPL)'), + }), + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().analyst.getPriceTargetConsensus({ symbol })), + }), + defineTool({ + name: 'getStockGrades', + description: + 'Get recent analyst grades (upgrades/downgrades) for a company (most recent `limit`)', + inputSchema: z.object({ + symbol: z.string().min(1).describe('The stock symbol (e.g., AAPL)'), + limit: z + .number() + .int() + .positive() + .default(20) + .describe('Max number of grade entries to return (default 20)'), + }), + execute: async ({ symbol, limit = 20 }) => { + const res = await getFMPClient().analyst.getGrades({ symbol }); + if (res.success && Array.isArray(res.data)) { + return toToolResponse({ ...res, data: res.data.slice(0, limit) } as APIResponse); + } + return toToolResponse(res); + }, + }), +]; diff --git a/packages/tools/src/definitions/index.ts b/packages/tools/src/definitions/index.ts index 1714814..45e2b43 100644 --- a/packages/tools/src/definitions/index.ts +++ b/packages/tools/src/definitions/index.ts @@ -10,6 +10,9 @@ import { marketDefinitions } from './market'; import { newsDefinitions } from './news'; import { screenerDefinitions } from './screener'; import { searchDefinitions } from './search'; +import { analystDefinitions } from './analyst'; +import { valuationDefinitions } from './valuation'; +import { technicalDefinitions } from './technical'; import { senateHouseDefinitions } from './senate-house'; import { stockDefinitions } from './stock'; import type { FMPToolDefinition } from './types'; @@ -29,6 +32,9 @@ export { newsDefinitions, screenerDefinitions, searchDefinitions, + analystDefinitions, + valuationDefinitions, + technicalDefinitions, senateHouseDefinitions, stockDefinitions, }; @@ -47,6 +53,9 @@ export const allDefinitions: FMPToolDefinition[] = [ ...newsDefinitions, ...screenerDefinitions, ...searchDefinitions, + ...analystDefinitions, + ...valuationDefinitions, + ...technicalDefinitions, ...senateHouseDefinitions, ...stockDefinitions, ]; diff --git a/packages/tools/src/definitions/technical.ts b/packages/tools/src/definitions/technical.ts new file mode 100644 index 0000000..fe1e88f --- /dev/null +++ b/packages/tools/src/definitions/technical.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; +import type { APIResponse } from 'fmp-node-api'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +export const technicalDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getTechnicalIndicator', + description: + 'Get a technical indicator series (SMA, EMA, RSI, etc.) for a symbol at a given timeframe. Returns the most recent `limit` bars.', + inputSchema: z.object({ + symbol: z.string().min(1).describe('The stock/ETF symbol (e.g., AAPL)'), + type: z + .enum(['sma', 'ema', 'wma', 'dema', 'tema', 'rsi', 'standardDeviation', 'williams', 'adx']) + .describe('The indicator type'), + periodLength: z + .number() + .int() + .positive() + .default(10) + .describe('Lookback period for the indicator (e.g. 14 for a 14-day RSI)'), + timeframe: z + .enum(['1min', '5min', '15min', '30min', '1hour', '4hour', '1day', '1week', '1month']) + .default('1day') + .describe('Bar timeframe'), + limit: z + .number() + .int() + .positive() + .default(50) + .describe('Max number of most-recent bars to return (default 50)'), + }), + execute: async ({ symbol, type, periodLength = 10, timeframe = '1day', limit = 50 }) => { + const res = await getFMPClient().technical.getTechnicalIndicator({ + symbol, + type, + periodLength, + timeframe, + }); + if (res.success && Array.isArray(res.data)) { + return toToolResponse({ ...res, data: res.data.slice(0, limit) } as APIResponse); + } + return toToolResponse(res); + }, + }), +]; diff --git a/packages/tools/src/definitions/valuation.ts b/packages/tools/src/definitions/valuation.ts new file mode 100644 index 0000000..3586209 --- /dev/null +++ b/packages/tools/src/definitions/valuation.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; +import { getFMPClient } from '@/client'; +import { toToolResponse } from '@/utils/format-response'; +import { defineTool, type FMPToolDefinition } from './types'; + +const symbolSchema = z.object({ + symbol: z.string().min(1).describe('The stock symbol (e.g., AAPL)'), +}); + +export const valuationDefinitions: FMPToolDefinition[] = [ + defineTool({ + name: 'getDiscountedCashFlow', + description: + "Get the discounted-cash-flow (DCF) fair-value estimate vs. the current price for a company", + inputSchema: symbolSchema, + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().valuation.getDiscountedCashFlow({ symbol })), + }), + defineTool({ + name: 'getCompanyRating', + description: "Get FMP's current rating/score snapshot for a company", + inputSchema: symbolSchema, + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().valuation.getRatingSnapshot({ symbol })), + }), +]; diff --git a/packages/tools/src/providers/openai/index.ts b/packages/tools/src/providers/openai/index.ts index 6d56c01..a86cdda 100644 --- a/packages/tools/src/providers/openai/index.ts +++ b/packages/tools/src/providers/openai/index.ts @@ -18,6 +18,9 @@ import { newsDefinitions, screenerDefinitions, searchDefinitions, + analystDefinitions, + valuationDefinitions, + technicalDefinitions, senateHouseDefinitions, stockDefinitions, type FMPToolDefinition, @@ -72,6 +75,12 @@ export const getStockNews = byName.getStockNews; export const getStockNewsBySymbol = byName.getStockNewsBySymbol; export const screenStocks = byName.screenStocks; export const searchSymbol = byName.searchSymbol; +export const getAnalystEstimates = byName.getAnalystEstimates; +export const getPriceTargetConsensus = byName.getPriceTargetConsensus; +export const getStockGrades = byName.getStockGrades; +export const getDiscountedCashFlow = byName.getDiscountedCashFlow; +export const getCompanyRating = byName.getCompanyRating; +export const getTechnicalIndicator = byName.getTechnicalIndicator; export const getMarketCap = byName.getMarketCap; export const getStockSplits = byName.getStockSplits; export const getDividendHistory = byName.getDividendHistory; @@ -89,6 +98,9 @@ export const marketTools = pick(marketDefinitions); export const newsTools = pick(newsDefinitions); export const screenerTools = pick(screenerDefinitions); export const searchTools = pick(searchDefinitions); +export const analystTools = pick(analystDefinitions); +export const valuationTools = pick(valuationDefinitions); +export const technicalTools = pick(technicalDefinitions); export const senateHouseTools = pick(senateHouseDefinitions); export const stockTools = pick(stockDefinitions); diff --git a/packages/tools/src/providers/vercel-ai/index.ts b/packages/tools/src/providers/vercel-ai/index.ts index 54f2e97..840a646 100644 --- a/packages/tools/src/providers/vercel-ai/index.ts +++ b/packages/tools/src/providers/vercel-ai/index.ts @@ -17,6 +17,9 @@ import { newsDefinitions, screenerDefinitions, searchDefinitions, + analystDefinitions, + valuationDefinitions, + technicalDefinitions, senateHouseDefinitions, stockDefinitions, type FMPToolDefinition, @@ -39,6 +42,9 @@ export const marketTools = toToolSet(marketDefinitions); export const newsTools = toToolSet(newsDefinitions); export const screenerTools = toToolSet(screenerDefinitions); export const searchTools = toToolSet(searchDefinitions); +export const analystTools = toToolSet(analystDefinitions); +export const valuationTools = toToolSet(valuationDefinitions); +export const technicalTools = toToolSet(technicalDefinitions); export const senateHouseTools = toToolSet(senateHouseDefinitions); export const stockTools = toToolSet(stockDefinitions); @@ -56,6 +62,9 @@ export const fmpTools: ToolSet = { ...newsTools, ...screenerTools, ...searchTools, + ...analystTools, + ...valuationTools, + ...technicalTools, ...senateHouseTools, ...stockTools, }; @@ -95,4 +104,7 @@ export const { export const { getStockNews, getStockNewsBySymbol } = newsTools; export const { screenStocks } = screenerTools; export const { searchSymbol } = searchTools; +export const { getAnalystEstimates, getPriceTargetConsensus, getStockGrades } = analystTools; +export const { getDiscountedCashFlow, getCompanyRating } = valuationTools; +export const { getTechnicalIndicator } = technicalTools; export const { getMarketCap, getStockSplits, getDividendHistory } = stockTools; diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 63830eb..233fce8 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,27 @@ # fmp-node-types +## 0.2.0-beta.4 + +### Patch Changes + +- Correct the analyst/valuation schemas to match the live FMP `stable` API (verified): + - **AnalystEstimate**: fields drop the `estimated` prefix and use the real names (`revenueLow/High/Avg`, `ebitda*`, `ebit*`, `netIncome*`, `sgaExpense*`, `epsAvg/High/Low`, `numAnalystsRevenue`, `numAnalystsEps`). + - **PriceTargetSummary**: count fields are `lastMonthCount`/`lastQuarterCount`/`lastYearCount`/`allTimeCount`. + - **DCFValuation**: the price field is keyed `"Stock Price"` (with a space). + - **CompanyRating**: gains an optional `date` (present on historical rows). + - `analyst.getEstimates` now defaults `period` to `annual` (the `stable` endpoint returns 400 without it). + +## 0.2.0-beta.3 + +### Minor Changes + +- Add three new endpoint categories: + - **`fmp.analyst`** — `getEstimates`, `getPriceTargetConsensus`, `getPriceTargetSummary`, `getGrades`. + - **`fmp.valuation`** — `getDiscountedCashFlow`, `getRatingSnapshot`, `getHistoricalRating`. + - **`fmp.technical`** — `getTechnicalIndicator` (SMA/EMA/RSI/etc. via a `type` param). + + Adds the matching `SearchResult`-style types and live-API shape-check manifest cases. + ## 0.2.0-beta.2 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index 1f59426..39ebbdb 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-types", - "version": "0.2.0-beta.2", + "version": "0.2.0-beta.4", "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/analyst.ts b/packages/types/src/analyst.ts new file mode 100644 index 0000000..0c34aca --- /dev/null +++ b/packages/types/src/analyst.ts @@ -0,0 +1,65 @@ +// analyst types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Verified against the live FMP `stable` API (2026-05-27). +import { z } from "zod"; + +export const AnalystEstimateSchema = z.object({ + symbol: z.string(), + date: z.string(), + revenueLow: z.number().nullable(), + revenueHigh: z.number().nullable(), + revenueAvg: z.number().nullable(), + ebitdaLow: z.number().nullable(), + ebitdaHigh: z.number().nullable(), + ebitdaAvg: z.number().nullable(), + ebitLow: z.number().nullable(), + ebitHigh: z.number().nullable(), + ebitAvg: z.number().nullable(), + netIncomeLow: z.number().nullable(), + netIncomeHigh: z.number().nullable(), + netIncomeAvg: z.number().nullable(), + sgaExpenseLow: z.number().nullable(), + sgaExpenseHigh: z.number().nullable(), + sgaExpenseAvg: z.number().nullable(), + epsAvg: z.number().nullable(), + epsHigh: z.number().nullable(), + epsLow: z.number().nullable(), + numAnalystsRevenue: z.number().nullable(), + numAnalystsEps: z.number().nullable() +}); + +export const PriceTargetConsensusSchema = z.object({ + symbol: z.string(), + targetHigh: z.number().nullable(), + targetLow: z.number().nullable(), + targetConsensus: z.number().nullable(), + targetMedian: z.number().nullable() +}); + +export const PriceTargetSummarySchema = z.object({ + symbol: z.string(), + lastMonthCount: z.number().nullable(), + lastMonthAvgPriceTarget: z.number().nullable(), + lastQuarterCount: z.number().nullable(), + lastQuarterAvgPriceTarget: z.number().nullable(), + lastYearCount: z.number().nullable(), + lastYearAvgPriceTarget: z.number().nullable(), + allTimeCount: z.number().nullable(), + allTimeAvgPriceTarget: z.number().nullable(), + publishers: z.string().nullable() +}); + +export const StockGradeSchema = z.object({ + symbol: z.string(), + date: z.string(), + gradingCompany: z.string().nullable(), + previousGrade: z.string().nullable(), + newGrade: z.string().nullable(), + action: z.string().nullable() +}); + +export type AnalystEstimate = z.infer; +export type PriceTargetConsensus = z.infer; +export type PriceTargetSummary = z.infer; +export type StockGrade = z.infer; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a0c4385..1e73cbd 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -49,6 +49,15 @@ export * from './screener'; // Search types export * from './search'; +// Analyst types +export * from './analyst'; + +// Valuation types +export * from './valuation'; + +// Technical indicator types +export * from './technical'; + // Senate house types export * from './senate-house'; diff --git a/packages/types/src/technical.ts b/packages/types/src/technical.ts new file mode 100644 index 0000000..1bd4241 --- /dev/null +++ b/packages/types/src/technical.ts @@ -0,0 +1,27 @@ +// technical indicator types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// One schema covers every indicator `type`; the indicator value lands in the +// correspondingly-named optional field (e.g. `sma`, `rsi`). Verified against the +// live FMP `stable` API (2026-05-27). +import { z } from "zod"; + +export const TechnicalIndicatorSchema = z.object({ + date: z.string(), + open: z.number().nullable(), + high: z.number().nullable(), + low: z.number().nullable(), + close: z.number().nullable(), + volume: z.number().nullable(), + sma: z.number().optional(), + ema: z.number().optional(), + wma: z.number().optional(), + dema: z.number().optional(), + tema: z.number().optional(), + rsi: z.number().optional(), + standardDeviation: z.number().optional(), + williams: z.number().optional(), + adx: z.number().optional() +}); + +export type TechnicalIndicator = z.infer; diff --git a/packages/types/src/valuation.ts b/packages/types/src/valuation.ts new file mode 100644 index 0000000..b392bb1 --- /dev/null +++ b/packages/types/src/valuation.ts @@ -0,0 +1,30 @@ +// valuation types for FMP API +// +// Schema-first: Zod schemas are the source of truth; types are derived via z.infer. +// Verified against the live FMP `stable` API (2026-05-27). Note the DCF price field +// is keyed "Stock Price" (with a space) in the real response. +import { z } from "zod"; + +export const DCFValuationSchema = z.object({ + symbol: z.string(), + date: z.string().nullable(), + dcf: z.number().nullable(), + "Stock Price": z.number().nullable() +}); + +export const CompanyRatingSchema = z.object({ + symbol: z.string(), + // Present on historical rating rows; absent on the current snapshot. + date: z.string().optional(), + rating: z.string().nullable(), + overallScore: z.number().nullable(), + discountedCashFlowScore: z.number().nullable(), + returnOnEquityScore: z.number().nullable(), + returnOnAssetsScore: z.number().nullable(), + debtToEquityScore: z.number().nullable(), + priceToEarningsScore: z.number().nullable(), + priceToBookScore: z.number().nullable() +}); + +export type DCFValuation = z.infer; +export type CompanyRating = z.infer; From bad0c16211e08e91ec5dfd90d51ba335c64660d0 Mon Sep 17 00:00:00 2001 From: e-roy Date: Thu, 28 May 2026 21:21:09 -0400 Subject: [PATCH 09/13] feat: 7 Starter-verified endpoints + tools; new analyst docs page Adds 7 endpoints (each with a matching AI tool; tool count 49 -> 56), all live-verified against the FMP Starter plan (test:live PASS, 0 drift): - fmp.financial: getFinancialScores (Altman Z + Piotroski), getKeyMetricsTTM, getFinancialRatiosTTM, getRevenueProductSegmentation, getRevenueGeographicSegmentation. - fmp.analyst: getGradesConsensus (buy/hold/sell counts + label). - fmp.company: getStockPeers (peers with price + market cap). Canonical Zod schemas in fmp-node-types (schema-first; full field sets captured from the live API before writing). New live-check manifest cases. Tools wired into both Vercel-AI and OpenAI providers; consistency test bumped to 56. Docs: extended financial + company pages, and added the first /docs/api/analyst page + sidebar entry (covers the whole analyst category, not just the new method). Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/starter-fundamental-signals.md | 13 + apps/docs/src/app/docs/api/analyst/page.mdx | 275 ++++++++++++++++++ apps/docs/src/app/docs/api/company/page.mdx | 57 ++++ apps/docs/src/app/docs/api/financial/page.mdx | 248 ++++++++++++++++ apps/docs/src/app/docs/api/layout.tsx | 4 + packages/api/scripts/live/manifest.ts | 14 + packages/api/src/endpoints/analyst.ts | 9 + packages/api/src/endpoints/company.ts | 24 ++ packages/api/src/endpoints/financial.ts | 132 +++++++++ .../__tests__/definitions/consistency.test.ts | 11 +- packages/tools/src/definitions/analyst.ts | 10 + packages/tools/src/definitions/company.ts | 7 + packages/tools/src/definitions/financial.ts | 66 ++++- packages/tools/src/providers/openai/index.ts | 7 + .../tools/src/providers/vercel-ai/index.ts | 16 +- packages/types/src/analyst.ts | 11 + packages/types/src/company.ts | 10 + packages/types/src/financial.ts | 153 ++++++++++ 18 files changed, 1044 insertions(+), 23 deletions(-) create mode 100644 .changeset/starter-fundamental-signals.md create mode 100644 apps/docs/src/app/docs/api/analyst/page.mdx diff --git a/.changeset/starter-fundamental-signals.md b/.changeset/starter-fundamental-signals.md new file mode 100644 index 0000000..ec8dc18 --- /dev/null +++ b/.changeset/starter-fundamental-signals.md @@ -0,0 +1,13 @@ +--- +"fmp-node-types": minor +"fmp-node-api": minor +"fmp-ai-tools": minor +--- + +Add 7 Starter-plan-verified endpoints (each with a matching AI tool; tool count 49 → 56): + +- **`fmp.financial`** — `getFinancialScores` (Altman Z-Score + Piotroski), `getKeyMetricsTTM`, `getFinancialRatiosTTM`, `getRevenueProductSegmentation`, `getRevenueGeographicSegmentation`. +- **`fmp.analyst`** — `getGradesConsensus` (buy/hold/sell counts + overall consensus). +- **`fmp.company`** — `getStockPeers` (peer companies with price + market cap). + +Adds canonical Zod schemas/types, live-API shape-check manifest cases (all PASS, 0 drift against the live `stable` API), docs for the new financial/company endpoints, and a new analyst documentation page. diff --git a/apps/docs/src/app/docs/api/analyst/page.mdx b/apps/docs/src/app/docs/api/analyst/page.mdx new file mode 100644 index 0000000..ea5d853 --- /dev/null +++ b/apps/docs/src/app/docs/api/analyst/page.mdx @@ -0,0 +1,275 @@ +# Analyst Endpoints + +The Analyst Endpoints provide access to analyst-driven data: forward estimates, +price targets, rating grades (upgrades/downgrades), and the overall rating +consensus for a company. + +## Available Methods + + + +## Get Analyst Estimates + +Retrieve forward analyst estimates including revenue, EBITDA, net income, and EPS, +with low/high/average ranges per period. + + + {`const estimates = await fmp.analyst.getEstimates({ + symbol: 'AAPL', + period: 'annual', + limit: 5, +});`} + + + + +### Example Response + + + {`{ + success: true, + data: [ + { + symbol: 'AAPL', + date: '2025-09-27', + revenueLow: 410000000000, + revenueHigh: 440000000000, + revenueAvg: 425000000000, + ebitdaLow: 140000000000, + ebitdaHigh: 155000000000, + ebitdaAvg: 147000000000, + netIncomeLow: 95000000000, + netIncomeHigh: 105000000000, + netIncomeAvg: 100000000000, + epsLow: 6.50, + epsHigh: 7.10, + epsAvg: 6.80, + numAnalystsRevenue: 24, + numAnalystsEps: 26 + } + ] +}`} + + +## Get Price Target Consensus + +Retrieve the analyst price-target consensus for a company (high, low, consensus, +median). Returns a single snapshot object. + + + {`const consensus = await fmp.analyst.getPriceTargetConsensus({ symbol: 'AAPL' });`} + + + + +### Example Response + + + {`{ + success: true, + data: { + symbol: 'AAPL', + targetHigh: 300, + targetLow: 200, + targetConsensus: 255, + targetMedian: 260 + } +}`} + + +## Get Price Target Summary + +Retrieve a summary of analyst price targets over recent periods (last month, +quarter, year, and all-time counts with average targets). + + + {`const summary = await fmp.analyst.getPriceTargetSummary({ symbol: 'AAPL' });`} + + + + +### Example Response + + + {`{ + success: true, + data: { + symbol: 'AAPL', + lastMonthCount: 5, + lastMonthAvgPriceTarget: 258, + lastQuarterCount: 14, + lastQuarterAvgPriceTarget: 252, + lastYearCount: 38, + lastYearAvgPriceTarget: 240, + allTimeCount: 220, + allTimeAvgPriceTarget: 198, + publishers: '["Morgan Stanley","Goldman Sachs"]' + } +}`} + + +## Get Stock Grades + +Retrieve recent analyst grades (upgrades/downgrades) for a company. + + + {`const grades = await fmp.analyst.getGrades({ symbol: 'AAPL' });`} + + + + +### Example Response + + + {`{ + success: true, + data: [ + { + symbol: 'AAPL', + date: '2026-05-01', + gradingCompany: 'Morgan Stanley', + previousGrade: 'Equal-Weight', + newGrade: 'Overweight', + action: 'upgrade' + } + ] +}`} + + +## Get Grades Consensus + +Retrieve the analyst rating consensus for a company: the number of analysts at each +rating (strongBuy/buy/hold/sell/strongSell) plus the overall consensus label. +Returns a single snapshot object. + + + {`const consensus = await fmp.analyst.getGradesConsensus({ symbol: 'AAPL' });`} + + + + +### Example Response + + + {`{ + success: true, + data: { + symbol: 'AAPL', + strongBuy: 1, + buy: 69, + hold: 33, + sell: 7, + strongSell: 0, + consensus: 'Buy' + } +}`} + + +## Error Handling + +Always check the success property before accessing data: + + +{`const consensus = await fmp.analyst.getGradesConsensus({ symbol: 'INVALID' }); + +if (consensus.success) { +console.log('Consensus:', consensus.data.consensus); +} else { +console.error('Error:', consensus.error); +console.error('Status:', consensus.status); +}`} + + + +## Next Steps + +Explore other endpoint categories: + +- [Financial Endpoints](/docs/api/financial) - Statements, ratios, and fundamental data +- [Company Endpoints](/docs/api/company) - Profiles, peers, and company information +- [Examples](/docs/api/examples) - Practical code samples diff --git a/apps/docs/src/app/docs/api/company/page.mdx b/apps/docs/src/app/docs/api/company/page.mdx index 9fcb9c7..3ba43e5 100644 --- a/apps/docs/src/app/docs/api/company/page.mdx +++ b/apps/docs/src/app/docs/api/company/page.mdx @@ -46,6 +46,11 @@ The Company Endpoints provide access to comprehensive company information includ path: 'v3/earning_call_transcript/{symbol}', description: 'Get company transcript dates and data', }, + { + method: 'GET', + path: 'stable/stock-peers?symbol={symbol}', + description: 'Get peer companies with price and market cap', + }, ]} /> @@ -401,6 +406,47 @@ Retrieve available transcript dates and data for a company's earnings calls. }`} +## Get Stock Peers + +Retrieve a list of peer companies for a stock, each with its current price and market cap. Useful for relative valuation and "compare X to its peers" analysis. + + + {`const peers = await fmp.company.getStockPeers('AAPL');`} + + + + +### Example Response + + + {`{ + success: true, + data: [ + { + symbol: 'GOOGL', + companyName: 'Alphabet Inc.', + price: 388.83, + mktCap: 4702848416027 + }, + { + symbol: 'MSFT', + companyName: 'Microsoft Corporation', + price: 430.16, + mktCap: 3198000000000 + } + ] +}`} + + ## Data Types ### CompanyProfile @@ -500,6 +546,17 @@ Retrieve available transcript dates and data for a company's earnings calls. {`type CompanyTranscriptData = [number, number, string];`} +### StockPeer + + + {`interface StockPeer { + symbol: string; + companyName: string; + price: number; + mktCap: number; +}`} + + **Note**: The `CompanyTranscriptData` type represents an array where: - `[0]`: Year (number) diff --git a/apps/docs/src/app/docs/api/financial/page.mdx b/apps/docs/src/app/docs/api/financial/page.mdx index 76bec64..8dd0433 100644 --- a/apps/docs/src/app/docs/api/financial/page.mdx +++ b/apps/docs/src/app/docs/api/financial/page.mdx @@ -67,6 +67,31 @@ and fundamental data for companies. path: '/earnings-surprises/{symbol}', description: 'Get earnings surprises data', }, + { + method: 'GET', + path: '/stable/financial-scores?symbol={symbol}', + description: 'Get financial health scores (Altman Z + Piotroski)', + }, + { + method: 'GET', + path: '/stable/key-metrics-ttm?symbol={symbol}', + description: 'Get trailing-twelve-month (TTM) key metrics', + }, + { + method: 'GET', + path: '/stable/ratios-ttm?symbol={symbol}', + description: 'Get trailing-twelve-month (TTM) financial ratios', + }, + { + method: 'GET', + path: '/stable/revenue-product-segmentation?symbol={symbol}', + description: 'Get revenue broken down by product line', + }, + { + method: 'GET', + path: '/stable/revenue-geographic-segmentation?symbol={symbol}', + description: 'Get revenue broken down by geographic region', + }, ]} /> @@ -1087,6 +1112,229 @@ Retrieve earnings surprises data showing the difference between estimated and ac }`} +## Get Financial Scores + +Retrieve financial health scores for a company: the Altman Z-Score (bankruptcy-risk indicator) and the Piotroski Score (fundamental-strength score, 0-9), plus the underlying components. Returns a single snapshot object. + + + {`const scores = await fmp.financial.getFinancialScores({ symbol: 'AAPL' });`} + + + + +### Example Response + + + {`{ + success: true, + data: { + symbol: 'AAPL', + reportedCurrency: 'USD', + altmanZScore: 12.89, + piotroskiScore: 9, + workingCapital: 9473000000, + totalAssets: 371082000000, + retainedEarnings: 12359000000, + ebit: 147722000000, + marketCap: 4535749279920, + totalLiabilities: 264591000000, + revenue: 451442000000 + } +}`} + + +## Get Key Metrics (TTM) + +Retrieve a single trailing-twelve-month snapshot of key valuation and performance metrics (EV multiples, returns, per-share figures). More token-efficient than the multi-period key-metrics statement when you only need the company's latest standing. + + + {`const ttm = await fmp.financial.getKeyMetricsTTM({ symbol: 'AAPL' });`} + + + + +### Example Response + + + {`{ + success: true, + data: { + symbol: 'AAPL', + marketCap: 4565564612600, + enterpriseValueTTM: 4613947612600, + evToSalesTTM: 10.22, + evToFreeCashFlowTTM: 35.72, + evToEBITDATTM: 28.78, + netDebtToEBITDATTM: 0.30, + currentRatioTTM: 1.07, + returnOnEquityTTM: 1.47, + returnOnInvestedCapitalTTM: 0.50, + earningsYieldTTM: 0.027, + freeCashFlowYieldTTM: 0.028, + freeCashFlowToEquityTTM: 114843000000 + // ...additional TTM fields + } +}`} + + +## Get Financial Ratios (TTM) + +Retrieve a single trailing-twelve-month snapshot of profitability, liquidity, leverage, and per-share ratios. Ideal for "what is this company's current P/E / margins / ROE" questions. + + + {`const ratios = await fmp.financial.getFinancialRatiosTTM({ symbol: 'AAPL' });`} + + + + +### Example Response + + + {`{ + success: true, + data: { + symbol: 'AAPL', + grossProfitMarginTTM: 0.479, + operatingProfitMarginTTM: 0.326, + netProfitMarginTTM: 0.272, + currentRatioTTM: 1.07, + quickRatioTTM: 1.02, + priceToEarningsRatioTTM: 37.31, + priceToBookRatioTTM: 42.94, + priceToSalesRatioTTM: 10.11, + debtToEquityRatioTTM: 0.80, + dividendYieldTTM: 0.0034, + dividendPayoutRatioTTM: 0.127 + // ...additional TTM ratios + } +}`} + + +## Get Revenue Product Segmentation + +Retrieve per-period revenue segmented by product/service line. The \`data\` object is keyed dynamically by product line. + + + {`const seg = await fmp.financial.getRevenueProductSegmentation({ symbol: 'AAPL' });`} + + + + +### Example Response + + + {`{ + success: true, + data: [ + { + symbol: 'AAPL', + fiscalYear: 2025, + period: 'FY', + reportedCurrency: null, + date: '2025-09-27', + data: { + iPhone: 209586000000, + Mac: 33708000000, + iPad: 28023000000, + 'Wearables, Home and Accessories': 35686000000, + Service: 109158000000 + } + } + ] +}`} + + +## Get Revenue Geographic Segmentation + +Retrieve per-period revenue segmented by geographic region. The \`data\` object is keyed dynamically by region. + + + {`const seg = await fmp.financial.getRevenueGeographicSegmentation({ symbol: 'AAPL' });`} + + + + +### Example Response + + + {`{ + success: true, + data: [ + { + symbol: 'AAPL', + fiscalYear: 2025, + period: 'FY', + reportedCurrency: null, + date: '2025-09-27', + data: { + 'Americas Segment': 178353000000, + 'Europe Segment': 111032000000, + 'Greater China Segment': 64377000000, + 'Japan Segment': 28703000000, + 'Rest of Asia Pacific Segment': 33696000000 + } + } + ] +}`} + + ## Error Handling Always check the success property before accessing data: diff --git a/apps/docs/src/app/docs/api/layout.tsx b/apps/docs/src/app/docs/api/layout.tsx index 4e0f292..40ca018 100644 --- a/apps/docs/src/app/docs/api/layout.tsx +++ b/apps/docs/src/app/docs/api/layout.tsx @@ -35,6 +35,10 @@ const apiNavigationGroups = [ { name: 'News Endpoints', href: '/docs/api/news' }, ], }, + { + title: 'Analysis', + items: [{ name: 'Analyst Endpoints', href: '/docs/api/analyst' }], + }, { title: 'Information', items: [ diff --git a/packages/api/scripts/live/manifest.ts b/packages/api/scripts/live/manifest.ts index 3a667b6..09e25ea 100644 --- a/packages/api/scripts/live/manifest.ts +++ b/packages/api/scripts/live/manifest.ts @@ -35,6 +35,11 @@ import { FinancialGrowthSchema, EarningsHistoricalSchema, EarningsSurprisesSchema, + FinancialScoresSchema, + KeyMetricsTTMSchema, + FinancialRatiosTTMSchema, + RevenueProductSegmentationSchema, + RevenueGeographicSegmentationSchema, // calendar EarningsCalendarSchema, EarningsConfirmedSchema, @@ -51,6 +56,7 @@ import { HistoricalSharesFloatSchema, EarningsCallTranscriptSchema, CompanyTranscriptDataSchema, + StockPeerSchema, // economic TreasuryRateSchema, EconomicIndicatorSchema, @@ -99,6 +105,7 @@ import { PriceTargetConsensusSchema, PriceTargetSummarySchema, StockGradeSchema, + GradesConsensusSchema, // valuation DCFValuationSchema, CompanyRatingSchema, @@ -193,6 +200,11 @@ export const manifest: LiveCase[] = [ { category: 'financial', name: 'getFinancialGrowth(AAPL,annual,2)', schema: FinancialGrowthSchema, kind: 'array', call: (fmp) => fmp.financial.getFinancialGrowth({ symbol: 'AAPL', period: 'annual', limit: 2 }) }, { category: 'financial', name: 'getEarningsHistorical(AAPL,2)', schema: EarningsHistoricalSchema, kind: 'array', call: (fmp) => fmp.financial.getEarningsHistorical({ symbol: 'AAPL', limit: 2 }) }, { category: 'financial', name: 'getEarningsSurprises(AAPL)', schema: EarningsSurprisesSchema, kind: 'array', call: (fmp) => fmp.financial.getEarningsSurprises('AAPL') }, + { category: 'financial', name: 'getFinancialScores(AAPL)', schema: FinancialScoresSchema, kind: 'object', call: (fmp) => fmp.financial.getFinancialScores({ symbol: 'AAPL' }) }, + { category: 'financial', name: 'getKeyMetricsTTM(AAPL)', schema: KeyMetricsTTMSchema, kind: 'object', call: (fmp) => fmp.financial.getKeyMetricsTTM({ symbol: 'AAPL' }) }, + { category: 'financial', name: 'getFinancialRatiosTTM(AAPL)', schema: FinancialRatiosTTMSchema, kind: 'object', call: (fmp) => fmp.financial.getFinancialRatiosTTM({ symbol: 'AAPL' }) }, + { category: 'financial', name: 'getRevenueProductSegmentation(AAPL)', schema: RevenueProductSegmentationSchema, kind: 'array', call: (fmp) => fmp.financial.getRevenueProductSegmentation({ symbol: 'AAPL' }) }, + { category: 'financial', name: 'getRevenueGeographicSegmentation(AAPL)', schema: RevenueGeographicSegmentationSchema, kind: 'array', call: (fmp) => fmp.financial.getRevenueGeographicSegmentation({ symbol: 'AAPL' }) }, // ---- calendar ---- { category: 'calendar', name: 'getEarningsCalendar()', schema: EarningsCalendarSchema, kind: 'array', call: (fmp) => fmp.calendar.getEarningsCalendar({ from: '2024-01-15', to: '2024-01-21' }) }, @@ -211,6 +223,7 @@ export const manifest: LiveCase[] = [ { category: 'company', name: 'getHistoricalSharesFloat(AAPL)', schema: HistoricalSharesFloatSchema, kind: 'array', call: (fmp) => fmp.company.getHistoricalSharesFloat('AAPL') }, { category: 'company', name: 'getEarningsCallTranscript(AAPL,2020,3)', schema: EarningsCallTranscriptSchema, kind: 'object', call: (fmp) => fmp.company.getEarningsCallTranscript({ symbol: 'AAPL', year: 2020, quarter: 3 }) }, { category: 'company', name: 'getCompanyTranscriptData(AAPL)', schema: CompanyTranscriptDataSchema, kind: 'array', call: (fmp) => fmp.company.getCompanyTranscriptData('AAPL') }, + { category: 'company', name: 'getStockPeers(AAPL)', schema: StockPeerSchema, kind: 'array', call: (fmp) => fmp.company.getStockPeers('AAPL') }, // ---- economic ---- { category: 'economic', name: 'getTreasuryRates()', schema: TreasuryRateSchema, kind: 'array', call: (fmp) => fmp.economic.getTreasuryRates({ from: '2024-01-01', to: '2024-12-31' }) }, @@ -280,6 +293,7 @@ export const manifest: LiveCase[] = [ { category: 'analyst', name: 'getPriceTargetConsensus(AAPL)', schema: PriceTargetConsensusSchema, kind: 'object', call: (fmp) => fmp.analyst.getPriceTargetConsensus({ symbol: 'AAPL' }) }, { category: 'analyst', name: 'getPriceTargetSummary(AAPL)', schema: PriceTargetSummarySchema, kind: 'object', call: (fmp) => fmp.analyst.getPriceTargetSummary({ symbol: 'AAPL' }) }, { category: 'analyst', name: 'getGrades(AAPL)', schema: StockGradeSchema, kind: 'array', call: (fmp) => fmp.analyst.getGrades({ symbol: 'AAPL' }) }, + { category: 'analyst', name: 'getGradesConsensus(AAPL)', schema: GradesConsensusSchema, kind: 'object', call: (fmp) => fmp.analyst.getGradesConsensus({ symbol: 'AAPL' }) }, // ---- valuation ---- { category: 'valuation', name: 'getDiscountedCashFlow(AAPL)', schema: DCFValuationSchema, kind: 'object', call: (fmp) => fmp.valuation.getDiscountedCashFlow({ symbol: 'AAPL' }) }, diff --git a/packages/api/src/endpoints/analyst.ts b/packages/api/src/endpoints/analyst.ts index fa2205c..aa67341 100644 --- a/packages/api/src/endpoints/analyst.ts +++ b/packages/api/src/endpoints/analyst.ts @@ -7,6 +7,7 @@ import { PriceTargetConsensus, PriceTargetSummary, StockGrade, + GradesConsensus, } from 'fmp-node-types'; export class AnalystEndpoints { @@ -47,4 +48,12 @@ export class AnalystEndpoints { async getGrades(params: { symbol: string }): Promise> { return this.client.get('/grades', 'stable', { symbol: params.symbol }); } + + /** + * Get the analyst rating consensus for a company: the count of analysts at each + * rating (strongBuy/buy/hold/sell/strongSell) plus the overall consensus label. + */ + async getGradesConsensus(params: { symbol: string }): Promise> { + return this.client.getSingle('/grades-consensus', 'stable', { symbol: params.symbol }); + } } diff --git a/packages/api/src/endpoints/company.ts b/packages/api/src/endpoints/company.ts index ea7610c..a459dea 100644 --- a/packages/api/src/endpoints/company.ts +++ b/packages/api/src/endpoints/company.ts @@ -8,6 +8,7 @@ import { HistoricalEmployeeCount, HistoricalSharesFloat, SharesFloat, + StockPeer, } from 'fmp-node-types'; import { FMPClient } from '@/client'; @@ -266,4 +267,27 @@ export class CompanyEndpoints { async getCompanyTranscriptData(symbol: string): Promise> { return this.client.getSingle(`/earning_call_transcript`, 'v4', { symbol }); } + + /** + * Get peer companies for a stock + * + * Provides a list of comparable companies (peers) for a stock, each with its + * current price and market cap. Useful for relative valuation and "compare X to + * its peers" analysis. + * + * @param symbol - The stock symbol to get peers for (e.g., 'AAPL', 'MSFT', 'GOOGL') + * + * @returns Promise resolving to an array of peer companies with price and market cap + * + * @example + * ```typescript + * const peers = await fmp.company.getStockPeers('AAPL'); + * peers.data.forEach(p => console.log(`${p.symbol} (${p.companyName}): $${p.price}`)); + * ``` + * + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#stock-peers|FMP Stock Peers Documentation} + */ + async getStockPeers(symbol: string): Promise> { + return this.client.get('/stock-peers', 'stable', { symbol }); + } } diff --git a/packages/api/src/endpoints/financial.ts b/packages/api/src/endpoints/financial.ts index 8157dbd..35091a3 100644 --- a/packages/api/src/endpoints/financial.ts +++ b/packages/api/src/endpoints/financial.ts @@ -12,6 +12,11 @@ import { CashFlowStatement, EarningsSurprises, EarningsHistorical, + FinancialScores, + KeyMetricsTTM, + FinancialRatiosTTM, + RevenueProductSegmentation, + RevenueGeographicSegmentation, } from 'fmp-node-types'; import { FMPClient } from '@/client'; @@ -478,4 +483,131 @@ export class FinancialEndpoints { async getEarningsSurprises(symbol: string): Promise> { return this.client.get(`/earnings-surprises/${symbol}`, 'v3'); } + + /** + * Get financial health scores for a company + * + * Provides the Altman Z-Score (bankruptcy-risk indicator) and Piotroski Score + * (fundamental-strength score, 0-9) along with the underlying components. + * A single, high-signal snapshot of a company's financial health. + * + * @param params.symbol - The stock symbol (e.g., 'AAPL', 'MSFT', 'GOOGL') + * + * @returns Promise resolving to the company's financial scores + * + * @example + * ```typescript + * const scores = await fmp.financial.getFinancialScores({ symbol: 'AAPL' }); + * console.log(`Altman Z: ${scores.data.altmanZScore}, Piotroski: ${scores.data.piotroskiScore}`); + * ``` + * + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#financial-scores|FMP Financial Scores Documentation} + */ + async getFinancialScores(params: { symbol: string }): Promise> { + return this.client.getSingle('/financial-scores', 'stable', { symbol: params.symbol }); + } + + /** + * Get trailing-twelve-month (TTM) key metrics for a company + * + * Provides a single current snapshot of key valuation and performance metrics + * (EV multiples, returns, per-share figures) computed over the trailing twelve + * months. More token-efficient than the multi-period key-metrics statement when + * you only need the company's latest standing. + * + * @param params.symbol - The stock symbol (e.g., 'AAPL', 'MSFT', 'GOOGL') + * + * @returns Promise resolving to the company's TTM key metrics + * + * @example + * ```typescript + * const ttm = await fmp.financial.getKeyMetricsTTM({ symbol: 'AAPL' }); + * console.log(`EV/EBITDA: ${ttm.data.evToEBITDATTM}, ROE: ${ttm.data.returnOnEquityTTM}`); + * ``` + * + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#key-metrics-ttm|FMP Key Metrics TTM Documentation} + */ + async getKeyMetricsTTM(params: { symbol: string }): Promise> { + return this.client.getSingle('/key-metrics-ttm', 'stable', { symbol: params.symbol }); + } + + /** + * Get trailing-twelve-month (TTM) financial ratios for a company + * + * Provides a single current snapshot of profitability, liquidity, leverage, and + * per-share ratios computed over the trailing twelve months. Ideal for "what is + * this company's current P/E / margins / ROE" questions. + * + * @param params.symbol - The stock symbol (e.g., 'AAPL', 'MSFT', 'GOOGL') + * + * @returns Promise resolving to the company's TTM financial ratios + * + * @example + * ```typescript + * const ratios = await fmp.financial.getFinancialRatiosTTM({ symbol: 'AAPL' }); + * console.log(`P/E: ${ratios.data.priceToEarningsRatioTTM}, Net margin: ${ratios.data.netProfitMarginTTM}`); + * ``` + * + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#ratios-ttm|FMP Financial Ratios TTM Documentation} + */ + async getFinancialRatiosTTM(params: { + symbol: string; + }): Promise> { + return this.client.getSingle('/ratios-ttm', 'stable', { symbol: params.symbol }); + } + + /** + * Get revenue broken down by product line for a company + * + * Provides per-period revenue segmented by product/service line. Useful for + * understanding which products drive a company's revenue and how the mix shifts + * over time. + * + * @param params.symbol - The stock symbol (e.g., 'AAPL', 'MSFT', 'GOOGL') + * @param params.period - The reporting period: 'annual' or 'quarter' (optional) + * + * @returns Promise resolving to an array of per-period product revenue breakdowns + * + * @example + * ```typescript + * const seg = await fmp.financial.getRevenueProductSegmentation({ symbol: 'AAPL' }); + * console.log(seg.data[0].data); // { iPhone: ..., Mac: ..., Service: ..., ... } + * ``` + * + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#revenue-product-segmentation|FMP Revenue Product Segmentation Documentation} + */ + async getRevenueProductSegmentation(params: { + symbol: string; + period?: 'annual' | 'quarter'; + }): Promise> { + const { symbol, period } = params; + return this.client.get('/revenue-product-segmentation', 'stable', { symbol, period }); + } + + /** + * Get revenue broken down by geographic region for a company + * + * Provides per-period revenue segmented by geographic region. Useful for + * understanding a company's regional exposure and how it shifts over time. + * + * @param params.symbol - The stock symbol (e.g., 'AAPL', 'MSFT', 'GOOGL') + * @param params.period - The reporting period: 'annual' or 'quarter' (optional) + * + * @returns Promise resolving to an array of per-period geographic revenue breakdowns + * + * @example + * ```typescript + * const seg = await fmp.financial.getRevenueGeographicSegmentation({ symbol: 'AAPL' }); + * console.log(seg.data[0].data); // { 'Americas Segment': ..., 'Europe Segment': ..., ... } + * ``` + * + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#revenue-geographic-segmentation|FMP Revenue Geographic Segmentation Documentation} + */ + async getRevenueGeographicSegmentation(params: { + symbol: string; + period?: 'annual' | 'quarter'; + }): Promise> { + const { symbol, period } = params; + return this.client.get('/revenue-geographic-segmentation', 'stable', { symbol, period }); + } } diff --git a/packages/tools/src/__tests__/definitions/consistency.test.ts b/packages/tools/src/__tests__/definitions/consistency.test.ts index 49fb5e1..d324350 100644 --- a/packages/tools/src/__tests__/definitions/consistency.test.ts +++ b/packages/tools/src/__tests__/definitions/consistency.test.ts @@ -7,9 +7,9 @@ import { fmpTools as openaiTools } from '@/providers/openai'; describe('cross-provider consistency', () => { const names = allDefinitions.map(d => d.name); - it('has 49 uniquely-named definitions', () => { - expect(names.length).toBe(49); - expect(new Set(names).size).toBe(49); + it('has 56 uniquely-named definitions', () => { + expect(names.length).toBe(56); + expect(new Set(names).size).toBe(56); }); it('Vercel AI exposes exactly the defined tools', () => { @@ -23,7 +23,10 @@ describe('cross-provider consistency', () => { it('descriptions match across both providers and the definition', () => { const openaiByName = new Map( - (openaiTools as Array<{ name: string; description: string }>).map(t => [t.name, t.description]), + (openaiTools as Array<{ name: string; description: string }>).map(t => [ + t.name, + t.description, + ]), ); for (const def of allDefinitions) { expect((vercelTools as Record)[def.name].description).toBe( diff --git a/packages/tools/src/definitions/analyst.ts b/packages/tools/src/definitions/analyst.ts index 1611e1f..ed347c9 100644 --- a/packages/tools/src/definitions/analyst.ts +++ b/packages/tools/src/definitions/analyst.ts @@ -56,4 +56,14 @@ export const analystDefinitions: FMPToolDefinition[] = [ return toToolResponse(res); }, }), + defineTool({ + name: 'getGradesConsensus', + description: + 'Get the analyst rating consensus for a company (counts of strongBuy/buy/hold/sell/strongSell + overall consensus)', + inputSchema: z.object({ + symbol: z.string().min(1).describe('The stock symbol (e.g., AAPL)'), + }), + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().analyst.getGradesConsensus({ symbol })), + }), ]; diff --git a/packages/tools/src/definitions/company.ts b/packages/tools/src/definitions/company.ts index 075da9b..4098abb 100644 --- a/packages/tools/src/definitions/company.ts +++ b/packages/tools/src/definitions/company.ts @@ -32,4 +32,11 @@ export const companyDefinitions: FMPToolDefinition[] = [ execute: async ({ symbol }) => toToolResponse(await getFMPClient().company.getExecutiveCompensation(symbol)), }), + defineTool({ + name: 'getStockPeers', + description: 'Get a list of peer companies (with price and market cap) for a company', + inputSchema: symbolSchema, + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().company.getStockPeers(symbol)), + }), ]; diff --git a/packages/tools/src/definitions/financial.ts b/packages/tools/src/definitions/financial.ts index 3d8ac26..73a0020 100644 --- a/packages/tools/src/definitions/financial.ts +++ b/packages/tools/src/definitions/financial.ts @@ -23,7 +23,8 @@ const statementTool = ( name, description, inputSchema: z.object({ symbol: symbol(what), period, limit }), - execute: async ({ symbol, period, limit }) => toToolResponse(await call({ symbol, period, limit })), + execute: async ({ symbol, period, limit }) => + toToolResponse(await call({ symbol, period, limit })), }); export const financialDefinitions: FMPToolDefinition[] = [ @@ -45,11 +46,8 @@ export const financialDefinitions: FMPToolDefinition[] = [ 'cash flow statement', args => getFMPClient().financial.getCashFlowStatement(args), ), - statementTool( - 'getKeyMetrics', - 'Get key metrics for a company', - 'key metrics', - args => getFMPClient().financial.getKeyMetrics(args), + statementTool('getKeyMetrics', 'Get key metrics for a company', 'key metrics', args => + getFMPClient().financial.getKeyMetrics(args), ), statementTool( 'getFinancialRatios', @@ -63,17 +61,11 @@ export const financialDefinitions: FMPToolDefinition[] = [ 'enterprise value', args => getFMPClient().financial.getEnterpriseValue(args), ), - statementTool( - 'getCashflowGrowth', - 'Get cashflow growth for a company', - 'cashflow growth', - args => getFMPClient().financial.getCashflowGrowth(args), + statementTool('getCashflowGrowth', 'Get cashflow growth for a company', 'cashflow growth', args => + getFMPClient().financial.getCashflowGrowth(args), ), - statementTool( - 'getIncomeGrowth', - 'Get income growth for a company', - 'income growth', - args => getFMPClient().financial.getIncomeGrowth(args), + statementTool('getIncomeGrowth', 'Get income growth for a company', 'income growth', args => + getFMPClient().financial.getIncomeGrowth(args), ), statementTool( 'getBalanceSheetGrowth', @@ -97,4 +89,46 @@ export const financialDefinitions: FMPToolDefinition[] = [ execute: async ({ symbol, limit }) => toToolResponse(await getFMPClient().financial.getEarningsHistorical({ symbol, limit })), }), + defineTool({ + name: 'getFinancialScores', + description: + 'Get financial health scores for a company (Altman Z-Score bankruptcy risk + Piotroski fundamental-strength score)', + inputSchema: z.object({ symbol: symbol('financial scores') }), + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().financial.getFinancialScores({ symbol })), + }), + defineTool({ + name: 'getKeyMetricsTTM', + description: + 'Get current trailing-twelve-month (TTM) key metrics for a company (one snapshot row)', + inputSchema: z.object({ symbol: symbol('TTM key metrics') }), + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().financial.getKeyMetricsTTM({ symbol })), + }), + defineTool({ + name: 'getFinancialRatiosTTM', + description: + 'Get current trailing-twelve-month (TTM) financial ratios for a company (margins, returns, liquidity)', + inputSchema: z.object({ symbol: symbol('TTM financial ratios') }), + execute: async ({ symbol }) => + toToolResponse(await getFMPClient().financial.getFinancialRatiosTTM({ symbol })), + }), + defineTool({ + name: 'getRevenueProductSegmentation', + description: 'Get revenue broken down by product line for a company', + inputSchema: z.object({ symbol: symbol('product revenue segmentation'), period }), + execute: async ({ symbol, period }) => + toToolResponse( + await getFMPClient().financial.getRevenueProductSegmentation({ symbol, period }), + ), + }), + defineTool({ + name: 'getRevenueGeographicSegmentation', + description: 'Get revenue broken down by geographic region for a company', + inputSchema: z.object({ symbol: symbol('geographic revenue segmentation'), period }), + execute: async ({ symbol, period }) => + toToolResponse( + await getFMPClient().financial.getRevenueGeographicSegmentation({ symbol, period }), + ), + }), ]; diff --git a/packages/tools/src/providers/openai/index.ts b/packages/tools/src/providers/openai/index.ts index a86cdda..8284b9b 100644 --- a/packages/tools/src/providers/openai/index.ts +++ b/packages/tools/src/providers/openai/index.ts @@ -41,6 +41,7 @@ export const getIntraday = byName.getIntraday; export const getCompanyProfile = byName.getCompanyProfile; export const getCompanySharesFloat = byName.getCompanySharesFloat; export const getCompanyExecutiveCompensation = byName.getCompanyExecutiveCompensation; +export const getStockPeers = byName.getStockPeers; export const getEarningsCalendar = byName.getEarningsCalendar; export const getEconomicCalendar = byName.getEconomicCalendar; export const getTreasuryRates = byName.getTreasuryRates; @@ -58,6 +59,11 @@ export const getIncomeGrowth = byName.getIncomeGrowth; export const getBalanceSheetGrowth = byName.getBalanceSheetGrowth; export const getFinancialGrowth = byName.getFinancialGrowth; export const getEarningsHistorical = byName.getEarningsHistorical; +export const getFinancialScores = byName.getFinancialScores; +export const getKeyMetricsTTM = byName.getKeyMetricsTTM; +export const getFinancialRatiosTTM = byName.getFinancialRatiosTTM; +export const getRevenueProductSegmentation = byName.getRevenueProductSegmentation; +export const getRevenueGeographicSegmentation = byName.getRevenueGeographicSegmentation; export const getInsiderTrading = byName.getInsiderTrading; export const getInstitutionalHolders = byName.getInstitutionalHolders; export const getMarketPerformance = byName.getMarketPerformance; @@ -78,6 +84,7 @@ export const searchSymbol = byName.searchSymbol; export const getAnalystEstimates = byName.getAnalystEstimates; export const getPriceTargetConsensus = byName.getPriceTargetConsensus; export const getStockGrades = byName.getStockGrades; +export const getGradesConsensus = byName.getGradesConsensus; export const getDiscountedCashFlow = byName.getDiscountedCashFlow; export const getCompanyRating = byName.getCompanyRating; export const getTechnicalIndicator = byName.getTechnicalIndicator; diff --git a/packages/tools/src/providers/vercel-ai/index.ts b/packages/tools/src/providers/vercel-ai/index.ts index 840a646..e46d9b2 100644 --- a/packages/tools/src/providers/vercel-ai/index.ts +++ b/packages/tools/src/providers/vercel-ai/index.ts @@ -71,8 +71,12 @@ export const fmpTools: ToolSet = { // Individual tools for direct import export const { getStockQuote, getHistoricalPrice, getIntraday } = quoteTools; -export const { getCompanyProfile, getCompanySharesFloat, getCompanyExecutiveCompensation } = - companyTools; +export const { + getCompanyProfile, + getCompanySharesFloat, + getCompanyExecutiveCompensation, + getStockPeers, +} = companyTools; export const { getEarningsCalendar, getEconomicCalendar } = calendarTools; export const { getTreasuryRates, getEconomicIndicators } = economicTools; export const { getETFHoldings, getETFProfile } = etfTools; @@ -88,6 +92,11 @@ export const { getBalanceSheetGrowth, getFinancialGrowth, getEarningsHistorical, + getFinancialScores, + getKeyMetricsTTM, + getFinancialRatiosTTM, + getRevenueProductSegmentation, + getRevenueGeographicSegmentation, } = financialTools; export const { getInsiderTrading } = insiderTools; export const { getInstitutionalHolders } = institutionalTools; @@ -104,7 +113,8 @@ export const { export const { getStockNews, getStockNewsBySymbol } = newsTools; export const { screenStocks } = screenerTools; export const { searchSymbol } = searchTools; -export const { getAnalystEstimates, getPriceTargetConsensus, getStockGrades } = analystTools; +export const { getAnalystEstimates, getPriceTargetConsensus, getStockGrades, getGradesConsensus } = + analystTools; export const { getDiscountedCashFlow, getCompanyRating } = valuationTools; export const { getTechnicalIndicator } = technicalTools; export const { getMarketCap, getStockSplits, getDividendHistory } = stockTools; diff --git a/packages/types/src/analyst.ts b/packages/types/src/analyst.ts index 0c34aca..29050e1 100644 --- a/packages/types/src/analyst.ts +++ b/packages/types/src/analyst.ts @@ -59,7 +59,18 @@ export const StockGradeSchema = z.object({ action: z.string().nullable() }); +export const GradesConsensusSchema = z.object({ + symbol: z.string(), + strongBuy: z.number(), + buy: z.number(), + hold: z.number(), + sell: z.number(), + strongSell: z.number(), + consensus: z.string() +}); + export type AnalystEstimate = z.infer; export type PriceTargetConsensus = z.infer; export type PriceTargetSummary = z.infer; export type StockGrade = z.infer; +export type GradesConsensus = z.infer; diff --git a/packages/types/src/company.ts b/packages/types/src/company.ts index 19c8c36..0662075 100644 --- a/packages/types/src/company.ts +++ b/packages/types/src/company.ts @@ -108,6 +108,15 @@ export const EarningsCallTranscriptSchema = z.object({ export const CompanyTranscriptDataSchema = z.tuple([z.number(), z.number(), z.string()]); +// Stock Peers — stable /stock-peers. Peer companies with price + market cap. +// Verified against the live FMP `stable` API (2026-05-27). +export const StockPeerSchema = z.object({ + symbol: z.string(), + companyName: z.string(), + price: z.number(), + mktCap: z.number() +}); + export type CompanyProfile = z.infer; export type ExecutiveCompensation = z.infer; export type CompanyNotes = z.infer; @@ -116,3 +125,4 @@ export type SharesFloat = z.infer; export type HistoricalSharesFloat = z.infer; export type EarningsCallTranscript = z.infer; export type CompanyTranscriptData = z.infer; +export type StockPeer = z.infer; diff --git a/packages/types/src/financial.ts b/packages/types/src/financial.ts index c4e31ad..8508618 100644 --- a/packages/types/src/financial.ts +++ b/packages/types/src/financial.ts @@ -530,3 +530,156 @@ export const EarningsSurprisesSchema = z.object({ }); export type EarningsSurprises = z.infer; + +// Financial Scores (Altman Z-Score + Piotroski + components) — stable /financial-scores. +// Verified against the live FMP `stable` API (2026-05-27). +export const FinancialScoresSchema = z.object({ + symbol: z.string(), + reportedCurrency: z.string().nullable(), + altmanZScore: z.number(), + piotroskiScore: z.number(), + workingCapital: z.number(), + totalAssets: z.number(), + retainedEarnings: z.number(), + ebit: z.number(), + marketCap: z.number(), + totalLiabilities: z.number(), + revenue: z.number(), +}); + +export type FinancialScores = z.infer; + +// Key Metrics TTM (trailing-twelve-month snapshot) — stable /key-metrics-ttm. +// Verified against the live FMP `stable` API (2026-05-27). +export const KeyMetricsTTMSchema = z.object({ + symbol: z.string(), + marketCap: z.number(), + enterpriseValueTTM: z.number(), + evToSalesTTM: z.number(), + evToOperatingCashFlowTTM: z.number(), + evToFreeCashFlowTTM: z.number(), + evToEBITDATTM: z.number(), + netDebtToEBITDATTM: z.number(), + currentRatioTTM: z.number(), + incomeQualityTTM: z.number(), + grahamNumberTTM: z.number(), + grahamNetNetTTM: z.number(), + taxBurdenTTM: z.number(), + interestBurdenTTM: z.number(), + workingCapitalTTM: z.number(), + investedCapitalTTM: z.number(), + returnOnAssetsTTM: z.number(), + operatingReturnOnAssetsTTM: z.number(), + returnOnTangibleAssetsTTM: z.number(), + returnOnEquityTTM: z.number(), + returnOnInvestedCapitalTTM: z.number(), + returnOnCapitalEmployedTTM: z.number(), + earningsYieldTTM: z.number(), + freeCashFlowYieldTTM: z.number(), + capexToOperatingCashFlowTTM: z.number(), + capexToDepreciationTTM: z.number(), + capexToRevenueTTM: z.number(), + salesGeneralAndAdministrativeToRevenueTTM: z.number(), + researchAndDevelopementToRevenueTTM: z.number(), + stockBasedCompensationToRevenueTTM: z.number(), + intangiblesToTotalAssetsTTM: z.number(), + averageReceivablesTTM: z.number(), + averagePayablesTTM: z.number(), + averageInventoryTTM: z.number(), + daysOfSalesOutstandingTTM: z.number(), + daysOfPayablesOutstandingTTM: z.number(), + daysOfInventoryOutstandingTTM: z.number(), + operatingCycleTTM: z.number(), + cashConversionCycleTTM: z.number(), + freeCashFlowToEquityTTM: z.number(), + freeCashFlowToFirmTTM: z.number(), + tangibleAssetValueTTM: z.number(), + netCurrentAssetValueTTM: z.number(), +}); + +export type KeyMetricsTTM = z.infer; + +// Financial Ratios TTM (trailing-twelve-month snapshot) — stable /ratios-ttm. +// Verified against the live FMP `stable` API (2026-05-27). +export const FinancialRatiosTTMSchema = z.object({ + symbol: z.string(), + grossProfitMarginTTM: z.number(), + ebitMarginTTM: z.number(), + ebitdaMarginTTM: z.number(), + operatingProfitMarginTTM: z.number(), + pretaxProfitMarginTTM: z.number(), + continuousOperationsProfitMarginTTM: z.number(), + netProfitMarginTTM: z.number(), + bottomLineProfitMarginTTM: z.number(), + receivablesTurnoverTTM: z.number(), + payablesTurnoverTTM: z.number(), + inventoryTurnoverTTM: z.number(), + fixedAssetTurnoverTTM: z.number(), + assetTurnoverTTM: z.number(), + currentRatioTTM: z.number(), + quickRatioTTM: z.number(), + solvencyRatioTTM: z.number(), + cashRatioTTM: z.number(), + priceToEarningsRatioTTM: z.number(), + priceToEarningsGrowthRatioTTM: z.number(), + forwardPriceToEarningsGrowthRatioTTM: z.number(), + priceToBookRatioTTM: z.number(), + priceToSalesRatioTTM: z.number(), + priceToFreeCashFlowRatioTTM: z.number(), + priceToOperatingCashFlowRatioTTM: z.number(), + debtToAssetsRatioTTM: z.number(), + debtToEquityRatioTTM: z.number(), + debtToCapitalRatioTTM: z.number(), + longTermDebtToCapitalRatioTTM: z.number(), + financialLeverageRatioTTM: z.number(), + workingCapitalTurnoverRatioTTM: z.number(), + operatingCashFlowRatioTTM: z.number(), + operatingCashFlowSalesRatioTTM: z.number(), + freeCashFlowOperatingCashFlowRatioTTM: z.number(), + debtServiceCoverageRatioTTM: z.number(), + interestCoverageRatioTTM: z.number(), + shortTermOperatingCashFlowCoverageRatioTTM: z.number(), + operatingCashFlowCoverageRatioTTM: z.number(), + capitalExpenditureCoverageRatioTTM: z.number(), + dividendPaidAndCapexCoverageRatioTTM: z.number(), + dividendPayoutRatioTTM: z.number(), + dividendYieldTTM: z.number(), + enterpriseValueTTM: z.number(), + revenuePerShareTTM: z.number(), + netIncomePerShareTTM: z.number(), + interestDebtPerShareTTM: z.number(), + cashPerShareTTM: z.number(), + bookValuePerShareTTM: z.number(), + tangibleBookValuePerShareTTM: z.number(), + shareholdersEquityPerShareTTM: z.number(), + operatingCashFlowPerShareTTM: z.number(), + capexPerShareTTM: z.number(), + freeCashFlowPerShareTTM: z.number(), + netIncomePerEBTTTM: z.number(), + ebtPerEbitTTM: z.number(), + priceToFairValueTTM: z.number(), + debtToMarketCapTTM: z.number(), + effectiveTaxRateTTM: z.number(), + enterpriseValueMultipleTTM: z.number(), + dividendPerShareTTM: z.number(), +}); + +export type FinancialRatiosTTM = z.infer; + +// Revenue segmentation (product & geographic share one shape) — stable +// /revenue-product-segmentation and /revenue-geographic-segmentation. The `data` +// object is keyed dynamically by product line / region. +// Verified against the live FMP `stable` API (2026-05-27). +export const RevenueSegmentationSchema = z.object({ + symbol: z.string(), + fiscalYear: z.number(), + period: z.string(), + reportedCurrency: z.string().nullable(), + date: z.string(), + data: z.record(z.string(), z.number()), +}); + +export const RevenueProductSegmentationSchema = RevenueSegmentationSchema; +export const RevenueGeographicSegmentationSchema = RevenueSegmentationSchema; +export type RevenueProductSegmentation = z.infer; +export type RevenueGeographicSegmentation = z.infer; From 36d890e4febc6a46c4b1568b4e384f362576c4e3 Mon Sep 17 00:00:00 2001 From: e-roy Date: Thu, 28 May 2026 21:50:20 -0400 Subject: [PATCH 10/13] chore: version packages 0.2.0 (exit pre-release) Exits Changesets pre-release mode and consolidates all 12 accumulated pre-release changesets into the first stable 0.2.0 of fmp-node-types, fmp-node-api, and fmp-ai-tools. The 0.2 line bundles: schema-first Zod types with live-API drift checks as a release gate, typed error classification surfaced through the AI tools (incl. plan-restricted), the shared-definitions tools refactor (one definition -> Vercel-AI + OpenAI provider adapters), client config + memoization, expanded coverage (search/price history/news/screener + analyst/valuation/technical categories), and the latest batch of 7 Starter-verified endpoints (financial scores, TTM key metrics/ratios, revenue segmentation, grades consensus, stock peers). Tool count: 56. Live-check: 0 drift across all 116 manifest cases. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/analyst-valuation-technical-api.md | 12 ------ .../analyst-valuation-technical-tools.md | 5 --- .changeset/error-handling.md | 11 ----- .changeset/fix-analyst-valuation-schemas.md | 12 ------ .changeset/loosen-tool-dep-ranges.md | 5 --- .changeset/openai-bundler-safe.md | 5 --- .changeset/pre.json | 25 ------------ .changeset/search-endpoint.md | 6 --- .changeset/starter-fundamental-signals.md | 13 ------ .changeset/tools-catch-thrown-errors.md | 5 --- .changeset/tools-config-and-coverage.md | 8 ---- .changeset/tools-shared-definitions.md | 5 --- .changeset/zod4-v6-beta.md | 11 ----- packages/api/CHANGELOG.md | 38 ++++++++++++++++++ packages/api/package.json | 2 +- packages/tools/CHANGELOG.md | 40 +++++++++++++++++++ packages/tools/package.json | 2 +- packages/types/CHANGELOG.md | 38 ++++++++++++++++++ packages/types/package.json | 2 +- 19 files changed, 119 insertions(+), 126 deletions(-) delete mode 100644 .changeset/analyst-valuation-technical-api.md delete mode 100644 .changeset/analyst-valuation-technical-tools.md delete mode 100644 .changeset/error-handling.md delete mode 100644 .changeset/fix-analyst-valuation-schemas.md delete mode 100644 .changeset/loosen-tool-dep-ranges.md delete mode 100644 .changeset/openai-bundler-safe.md delete mode 100644 .changeset/pre.json delete mode 100644 .changeset/search-endpoint.md delete mode 100644 .changeset/starter-fundamental-signals.md delete mode 100644 .changeset/tools-catch-thrown-errors.md delete mode 100644 .changeset/tools-config-and-coverage.md delete mode 100644 .changeset/tools-shared-definitions.md delete mode 100644 .changeset/zod4-v6-beta.md diff --git a/.changeset/analyst-valuation-technical-api.md b/.changeset/analyst-valuation-technical-api.md deleted file mode 100644 index 13535be..0000000 --- a/.changeset/analyst-valuation-technical-api.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"fmp-node-types": minor -"fmp-node-api": minor ---- - -Add three new endpoint categories: - -- **`fmp.analyst`** — `getEstimates`, `getPriceTargetConsensus`, `getPriceTargetSummary`, `getGrades`. -- **`fmp.valuation`** — `getDiscountedCashFlow`, `getRatingSnapshot`, `getHistoricalRating`. -- **`fmp.technical`** — `getTechnicalIndicator` (SMA/EMA/RSI/etc. via a `type` param). - -Adds the matching `SearchResult`-style types and live-API shape-check manifest cases. diff --git a/.changeset/analyst-valuation-technical-tools.md b/.changeset/analyst-valuation-technical-tools.md deleted file mode 100644 index 3d8d2cd..0000000 --- a/.changeset/analyst-valuation-technical-tools.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"fmp-ai-tools": minor ---- - -Add AI tools for the new analyst, valuation, and technical endpoints: `getAnalystEstimates`, `getPriceTargetConsensus`, `getStockGrades`, `getDiscountedCashFlow`, `getCompanyRating`, and `getTechnicalIndicator`. List-returning tools apply a default result `limit`. diff --git a/.changeset/error-handling.md b/.changeset/error-handling.md deleted file mode 100644 index 934f7b6..0000000 --- a/.changeset/error-handling.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"fmp-node-types": minor -"fmp-node-api": minor -"fmp-ai-tools": minor ---- - -Typed error classification for FMP failures, surfaced through the AI tools. - -- **fmp-node-types**: `APIResponse` gains an optional `errorType` (`plan-restricted | rate-limit | auth | not-found | bad-request | network | unknown`). -- **fmp-node-api**: the client now reads FMP's real error message from the response body and classifies failures (new `classifyError` export). Plan/subscription-restricted endpoints (402/403 or "Exclusive/Special Endpoint") are reported as `plan-restricted` instead of a generic error. -- **fmp-ai-tools**: every tool now returns a structured error (`{ error, type, message, status }`) to the model on failure instead of `null`, so an agent can explain *why* a call failed — e.g. that the data requires a higher FMP plan. diff --git a/.changeset/fix-analyst-valuation-schemas.md b/.changeset/fix-analyst-valuation-schemas.md deleted file mode 100644 index fc42dcc..0000000 --- a/.changeset/fix-analyst-valuation-schemas.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"fmp-node-types": patch -"fmp-node-api": patch ---- - -Correct the analyst/valuation schemas to match the live FMP `stable` API (verified): - -- **AnalystEstimate**: fields drop the `estimated` prefix and use the real names (`revenueLow/High/Avg`, `ebitda*`, `ebit*`, `netIncome*`, `sgaExpense*`, `epsAvg/High/Low`, `numAnalystsRevenue`, `numAnalystsEps`). -- **PriceTargetSummary**: count fields are `lastMonthCount`/`lastQuarterCount`/`lastYearCount`/`allTimeCount`. -- **DCFValuation**: the price field is keyed `"Stock Price"` (with a space). -- **CompanyRating**: gains an optional `date` (present on historical rows). -- `analyst.getEstimates` now defaults `period` to `annual` (the `stable` endpoint returns 400 without it). diff --git a/.changeset/loosen-tool-dep-ranges.md b/.changeset/loosen-tool-dep-ranges.md deleted file mode 100644 index a3ad738..0000000 --- a/.changeset/loosen-tool-dep-ranges.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"fmp-ai-tools": patch ---- - -Loosen the `@openai/agents` (`^0.11.0`) and `ai` (`^6.0.0`) dependency ranges so installs resolve to settled, widely-available versions instead of being pinned to the newest release. This avoids install failures for consumers using pnpm's `minimum-release-age` supply-chain guard, which previously had no mature version to resolve. diff --git a/.changeset/openai-bundler-safe.md b/.changeset/openai-bundler-safe.md deleted file mode 100644 index d2d0d10..0000000 --- a/.changeset/openai-bundler-safe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"fmp-ai-tools": patch ---- - -Make the `fmp-ai-tools/openai` entry point bundler-safe so it works in Next.js (App Router / Turbopack) with a normal static import. Removed the filesystem-based `checkOpenAIAgentsVersion()` that ran on import (it used `require.resolve`/`fs` and broke under bundlers), and moved `@openai/agents` and `ai` to **optional** `peerDependencies`. Consumers no longer need `serverExternalPackages`, `createRequire`, or any other workaround. diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 528f70e..0000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "mode": "pre", - "tag": "beta", - "initialVersions": { - "fmp-docs": "0.0.0", - "fmp-ai-tools-openai-example": "0.1.0", - "fmp-ai-tools-vercel-ai-example": "0.1.0", - "fmp-node-api": "0.1.9", - "fmp-ai-tools": "0.1.0", - "fmp-node-types": "0.1.4" - }, - "changesets": [ - "analyst-valuation-technical-api", - "analyst-valuation-technical-tools", - "error-handling", - "fix-analyst-valuation-schemas", - "loosen-tool-dep-ranges", - "openai-bundler-safe", - "search-endpoint", - "tools-catch-thrown-errors", - "tools-config-and-coverage", - "tools-shared-definitions", - "zod4-v6-beta" - ] -} diff --git a/.changeset/search-endpoint.md b/.changeset/search-endpoint.md deleted file mode 100644 index 9d9c607..0000000 --- a/.changeset/search-endpoint.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"fmp-node-types": minor -"fmp-node-api": minor ---- - -Add a `/search` endpoint: `fmp.search.search({ query, limit?, exchange? })` resolves a company name or partial ticker to matching symbols, returning the new `SearchResult` type. Wired into the live-API shape-check manifest. diff --git a/.changeset/starter-fundamental-signals.md b/.changeset/starter-fundamental-signals.md deleted file mode 100644 index ec8dc18..0000000 --- a/.changeset/starter-fundamental-signals.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"fmp-node-types": minor -"fmp-node-api": minor -"fmp-ai-tools": minor ---- - -Add 7 Starter-plan-verified endpoints (each with a matching AI tool; tool count 49 → 56): - -- **`fmp.financial`** — `getFinancialScores` (Altman Z-Score + Piotroski), `getKeyMetricsTTM`, `getFinancialRatiosTTM`, `getRevenueProductSegmentation`, `getRevenueGeographicSegmentation`. -- **`fmp.analyst`** — `getGradesConsensus` (buy/hold/sell counts + overall consensus). -- **`fmp.company`** — `getStockPeers` (peer companies with price + market cap). - -Adds canonical Zod schemas/types, live-API shape-check manifest cases (all PASS, 0 drift against the live `stable` API), docs for the new financial/company endpoints, and a new analyst documentation page. diff --git a/.changeset/tools-catch-thrown-errors.md b/.changeset/tools-catch-thrown-errors.md deleted file mode 100644 index 3ba7692..0000000 --- a/.changeset/tools-catch-thrown-errors.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"fmp-ai-tools": patch ---- - -Tools no longer throw out of `execute`. A thrown error — most importantly a missing/invalid `FMP_API_KEY`, which throws from the `FMP` constructor before any request is made — is now caught at the tool boundary and returned to the model as the same structured `{ error, type, message, status }` shape (with `type: "auth"` for key problems). Previously the raw exception reached the AI SDK and the agent received only a vague failure with no reason. diff --git a/.changeset/tools-config-and-coverage.md b/.changeset/tools-config-and-coverage.md deleted file mode 100644 index 7a4ef40..0000000 --- a/.changeset/tools-config-and-coverage.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"fmp-ai-tools": minor ---- - -Add client configuration and new tools. - -- **Client**: the FMP client is now memoized (no longer reconstructed on every tool call), and a new `configureFMPClient({ apiKey, timeout })` (exported from both entry points) lets consumers configure it instead of relying solely on the `FMP_API_KEY` environment variable. -- **New tools**: `getHistoricalPrice` and `getIntraday` (price history), `getStockNews` and `getStockNewsBySymbol` (news), `screenStocks` (screener), and `searchSymbol` (symbol search). High-volume tools apply a default result `limit` to keep outputs context-friendly. diff --git a/.changeset/tools-shared-definitions.md b/.changeset/tools-shared-definitions.md deleted file mode 100644 index 6b68b6e..0000000 --- a/.changeset/tools-shared-definitions.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"fmp-ai-tools": patch ---- - -Internal refactor: each tool is now defined once in a shared `definitions/` layer, with the Vercel AI and OpenAI wrappers acting as thin per-provider adapters. The public API (tool names, subpath exports `fmp-ai-tools/vercel-ai` and `fmp-ai-tools/openai`, `fmpTools`, category groups, and individual tools) is unchanged. Minor drift between the two providers was canonicalized: numeric params use `z.number()` (OpenAI no longer string-coerces), and descriptions/validation use the richer variant. Adding a new provider is now a single adapter file. diff --git a/.changeset/zod4-v6-beta.md b/.changeset/zod4-v6-beta.md deleted file mode 100644 index ac858e8..0000000 --- a/.changeset/zod4-v6-beta.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"fmp-node-types": minor -"fmp-node-api": minor -"fmp-ai-tools": minor ---- - -Schema-first types and updated AI SDKs. - -- **fmp-node-types**: now ships Zod schemas as the source of truth, with TypeScript types derived via `z.infer`. -- **fmp-node-api**: consumes the schema-first types; response types corrected against the live FMP API (e.g. `getIntraday` → `IntradayPrice[]`, `getMarketPerformance`/`getMarketIndex` → quote-shaped `MarketIndex[]`, plus nullability fixes). Adds an internal live-API shape-check tool. -- **fmp-ai-tools**: updated to Vercel AI SDK v6 (`ai@6`, `@ai-sdk/*@3`), `@openai/agents@0.11.5`, and Zod 4. The OpenAI tool wrapper now passes the Zod schema natively to `tool()`. diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index eaa143f..6f78b72 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,43 @@ # fmp-node-api +## 0.2.0 + +### Minor Changes + +- 7030a68: Add three new endpoint categories: + - **`fmp.analyst`** — `getEstimates`, `getPriceTargetConsensus`, `getPriceTargetSummary`, `getGrades`. + - **`fmp.valuation`** — `getDiscountedCashFlow`, `getRatingSnapshot`, `getHistoricalRating`. + - **`fmp.technical`** — `getTechnicalIndicator` (SMA/EMA/RSI/etc. via a `type` param). + + Adds the matching `SearchResult`-style types and live-API shape-check manifest cases. + +- 005a6e9: Typed error classification for FMP failures, surfaced through the AI tools. + - **fmp-node-types**: `APIResponse` gains an optional `errorType` (`plan-restricted | rate-limit | auth | not-found | bad-request | network | unknown`). + - **fmp-node-api**: the client now reads FMP's real error message from the response body and classifies failures (new `classifyError` export). Plan/subscription-restricted endpoints (402/403 or "Exclusive/Special Endpoint") are reported as `plan-restricted` instead of a generic error. + - **fmp-ai-tools**: every tool now returns a structured error (`{ error, type, message, status }`) to the model on failure instead of `null`, so an agent can explain _why_ a call failed — e.g. that the data requires a higher FMP plan. + +- 0260327: Add a `/search` endpoint: `fmp.search.search({ query, limit?, exchange? })` resolves a company name or partial ticker to matching symbols, returning the new `SearchResult` type. Wired into the live-API shape-check manifest. +- bad0c16: Add 7 Starter-plan-verified endpoints (each with a matching AI tool; tool count 49 → 56): + - **`fmp.financial`** — `getFinancialScores` (Altman Z-Score + Piotroski), `getKeyMetricsTTM`, `getFinancialRatiosTTM`, `getRevenueProductSegmentation`, `getRevenueGeographicSegmentation`. + - **`fmp.analyst`** — `getGradesConsensus` (buy/hold/sell counts + overall consensus). + - **`fmp.company`** — `getStockPeers` (peer companies with price + market cap). + + Adds canonical Zod schemas/types, live-API shape-check manifest cases (all PASS, 0 drift against the live `stable` API), docs for the new financial/company endpoints, and a new analyst documentation page. + +- e7042b4: Schema-first types and updated AI SDKs. + - **fmp-node-types**: now ships Zod schemas as the source of truth, with TypeScript types derived via `z.infer`. + - **fmp-node-api**: consumes the schema-first types; response types corrected against the live FMP API (e.g. `getIntraday` → `IntradayPrice[]`, `getMarketPerformance`/`getMarketIndex` → quote-shaped `MarketIndex[]`, plus nullability fixes). Adds an internal live-API shape-check tool. + - **fmp-ai-tools**: updated to Vercel AI SDK v6 (`ai@6`, `@ai-sdk/*@3`), `@openai/agents@0.11.5`, and Zod 4. The OpenAI tool wrapper now passes the Zod schema natively to `tool()`. + +### Patch Changes + +- 7030a68: Correct the analyst/valuation schemas to match the live FMP `stable` API (verified): + - **AnalystEstimate**: fields drop the `estimated` prefix and use the real names (`revenueLow/High/Avg`, `ebitda*`, `ebit*`, `netIncome*`, `sgaExpense*`, `epsAvg/High/Low`, `numAnalystsRevenue`, `numAnalystsEps`). + - **PriceTargetSummary**: count fields are `lastMonthCount`/`lastQuarterCount`/`lastYearCount`/`allTimeCount`. + - **DCFValuation**: the price field is keyed `"Stock Price"` (with a space). + - **CompanyRating**: gains an optional `date` (present on historical rows). + - `analyst.getEstimates` now defaults `period` to `annual` (the `stable` endpoint returns 400 without it). + ## 0.2.0-beta.4 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index 77951b1..b4e624d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-api", - "version": "0.2.0-beta.4", + "version": "0.2.0", "description": "A comprehensive Node.js wrapper for Financial Modeling Prep API", "disclaimer": "This package is not affiliated with, endorsed by, or sponsored by Financial Modeling Prep (FMP). This is an independent, community-developed wrapper.", "main": "./dist/index.js", diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index 475adb0..6050a25 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -1,5 +1,45 @@ # fmp-ai-tools +## 0.2.0 + +### Minor Changes + +- 7030a68: Add AI tools for the new analyst, valuation, and technical endpoints: `getAnalystEstimates`, `getPriceTargetConsensus`, `getStockGrades`, `getDiscountedCashFlow`, `getCompanyRating`, and `getTechnicalIndicator`. List-returning tools apply a default result `limit`. +- 005a6e9: Typed error classification for FMP failures, surfaced through the AI tools. + - **fmp-node-types**: `APIResponse` gains an optional `errorType` (`plan-restricted | rate-limit | auth | not-found | bad-request | network | unknown`). + - **fmp-node-api**: the client now reads FMP's real error message from the response body and classifies failures (new `classifyError` export). Plan/subscription-restricted endpoints (402/403 or "Exclusive/Special Endpoint") are reported as `plan-restricted` instead of a generic error. + - **fmp-ai-tools**: every tool now returns a structured error (`{ error, type, message, status }`) to the model on failure instead of `null`, so an agent can explain _why_ a call failed — e.g. that the data requires a higher FMP plan. + +- bad0c16: Add 7 Starter-plan-verified endpoints (each with a matching AI tool; tool count 49 → 56): + - **`fmp.financial`** — `getFinancialScores` (Altman Z-Score + Piotroski), `getKeyMetricsTTM`, `getFinancialRatiosTTM`, `getRevenueProductSegmentation`, `getRevenueGeographicSegmentation`. + - **`fmp.analyst`** — `getGradesConsensus` (buy/hold/sell counts + overall consensus). + - **`fmp.company`** — `getStockPeers` (peer companies with price + market cap). + + Adds canonical Zod schemas/types, live-API shape-check manifest cases (all PASS, 0 drift against the live `stable` API), docs for the new financial/company endpoints, and a new analyst documentation page. + +- 0260327: Add client configuration and new tools. + - **Client**: the FMP client is now memoized (no longer reconstructed on every tool call), and a new `configureFMPClient({ apiKey, timeout })` (exported from both entry points) lets consumers configure it instead of relying solely on the `FMP_API_KEY` environment variable. + - **New tools**: `getHistoricalPrice` and `getIntraday` (price history), `getStockNews` and `getStockNewsBySymbol` (news), `screenStocks` (screener), and `searchSymbol` (symbol search). High-volume tools apply a default result `limit` to keep outputs context-friendly. + +- e7042b4: Schema-first types and updated AI SDKs. + - **fmp-node-types**: now ships Zod schemas as the source of truth, with TypeScript types derived via `z.infer`. + - **fmp-node-api**: consumes the schema-first types; response types corrected against the live FMP API (e.g. `getIntraday` → `IntradayPrice[]`, `getMarketPerformance`/`getMarketIndex` → quote-shaped `MarketIndex[]`, plus nullability fixes). Adds an internal live-API shape-check tool. + - **fmp-ai-tools**: updated to Vercel AI SDK v6 (`ai@6`, `@ai-sdk/*@3`), `@openai/agents@0.11.5`, and Zod 4. The OpenAI tool wrapper now passes the Zod schema natively to `tool()`. + +### Patch Changes + +- e7042b4: Loosen the `@openai/agents` (`^0.11.0`) and `ai` (`^6.0.0`) dependency ranges so installs resolve to settled, widely-available versions instead of being pinned to the newest release. This avoids install failures for consumers using pnpm's `minimum-release-age` supply-chain guard, which previously had no mature version to resolve. +- e7042b4: Make the `fmp-ai-tools/openai` entry point bundler-safe so it works in Next.js (App Router / Turbopack) with a normal static import. Removed the filesystem-based `checkOpenAIAgentsVersion()` that ran on import (it used `require.resolve`/`fs` and broke under bundlers), and moved `@openai/agents` and `ai` to **optional** `peerDependencies`. Consumers no longer need `serverExternalPackages`, `createRequire`, or any other workaround. +- 005a6e9: Tools no longer throw out of `execute`. A thrown error — most importantly a missing/invalid `FMP_API_KEY`, which throws from the `FMP` constructor before any request is made — is now caught at the tool boundary and returned to the model as the same structured `{ error, type, message, status }` shape (with `type: "auth"` for key problems). Previously the raw exception reached the AI SDK and the agent received only a vague failure with no reason. +- 1736ba8: Internal refactor: each tool is now defined once in a shared `definitions/` layer, with the Vercel AI and OpenAI wrappers acting as thin per-provider adapters. The public API (tool names, subpath exports `fmp-ai-tools/vercel-ai` and `fmp-ai-tools/openai`, `fmpTools`, category groups, and individual tools) is unchanged. Minor drift between the two providers was canonicalized: numeric params use `z.number()` (OpenAI no longer string-coerces), and descriptions/validation use the richer variant. Adding a new provider is now a single adapter file. +- Updated dependencies [7030a68] +- Updated dependencies [005a6e9] +- Updated dependencies [7030a68] +- Updated dependencies [0260327] +- Updated dependencies [bad0c16] +- Updated dependencies [e7042b4] + - fmp-node-api@0.2.0 + ## 0.2.0-beta.8 ### Patch Changes diff --git a/packages/tools/package.json b/packages/tools/package.json index ed53fff..cc90497 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.2.0-beta.8", + "version": "0.2.0", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 233fce8..e1d5ecb 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,43 @@ # fmp-node-types +## 0.2.0 + +### Minor Changes + +- 7030a68: Add three new endpoint categories: + - **`fmp.analyst`** — `getEstimates`, `getPriceTargetConsensus`, `getPriceTargetSummary`, `getGrades`. + - **`fmp.valuation`** — `getDiscountedCashFlow`, `getRatingSnapshot`, `getHistoricalRating`. + - **`fmp.technical`** — `getTechnicalIndicator` (SMA/EMA/RSI/etc. via a `type` param). + + Adds the matching `SearchResult`-style types and live-API shape-check manifest cases. + +- 005a6e9: Typed error classification for FMP failures, surfaced through the AI tools. + - **fmp-node-types**: `APIResponse` gains an optional `errorType` (`plan-restricted | rate-limit | auth | not-found | bad-request | network | unknown`). + - **fmp-node-api**: the client now reads FMP's real error message from the response body and classifies failures (new `classifyError` export). Plan/subscription-restricted endpoints (402/403 or "Exclusive/Special Endpoint") are reported as `plan-restricted` instead of a generic error. + - **fmp-ai-tools**: every tool now returns a structured error (`{ error, type, message, status }`) to the model on failure instead of `null`, so an agent can explain _why_ a call failed — e.g. that the data requires a higher FMP plan. + +- 0260327: Add a `/search` endpoint: `fmp.search.search({ query, limit?, exchange? })` resolves a company name or partial ticker to matching symbols, returning the new `SearchResult` type. Wired into the live-API shape-check manifest. +- bad0c16: Add 7 Starter-plan-verified endpoints (each with a matching AI tool; tool count 49 → 56): + - **`fmp.financial`** — `getFinancialScores` (Altman Z-Score + Piotroski), `getKeyMetricsTTM`, `getFinancialRatiosTTM`, `getRevenueProductSegmentation`, `getRevenueGeographicSegmentation`. + - **`fmp.analyst`** — `getGradesConsensus` (buy/hold/sell counts + overall consensus). + - **`fmp.company`** — `getStockPeers` (peer companies with price + market cap). + + Adds canonical Zod schemas/types, live-API shape-check manifest cases (all PASS, 0 drift against the live `stable` API), docs for the new financial/company endpoints, and a new analyst documentation page. + +- e7042b4: Schema-first types and updated AI SDKs. + - **fmp-node-types**: now ships Zod schemas as the source of truth, with TypeScript types derived via `z.infer`. + - **fmp-node-api**: consumes the schema-first types; response types corrected against the live FMP API (e.g. `getIntraday` → `IntradayPrice[]`, `getMarketPerformance`/`getMarketIndex` → quote-shaped `MarketIndex[]`, plus nullability fixes). Adds an internal live-API shape-check tool. + - **fmp-ai-tools**: updated to Vercel AI SDK v6 (`ai@6`, `@ai-sdk/*@3`), `@openai/agents@0.11.5`, and Zod 4. The OpenAI tool wrapper now passes the Zod schema natively to `tool()`. + +### Patch Changes + +- 7030a68: Correct the analyst/valuation schemas to match the live FMP `stable` API (verified): + - **AnalystEstimate**: fields drop the `estimated` prefix and use the real names (`revenueLow/High/Avg`, `ebitda*`, `ebit*`, `netIncome*`, `sgaExpense*`, `epsAvg/High/Low`, `numAnalystsRevenue`, `numAnalystsEps`). + - **PriceTargetSummary**: count fields are `lastMonthCount`/`lastQuarterCount`/`lastYearCount`/`allTimeCount`. + - **DCFValuation**: the price field is keyed `"Stock Price"` (with a space). + - **CompanyRating**: gains an optional `date` (present on historical rows). + - `analyst.getEstimates` now defaults `period` to `annual` (the `stable` endpoint returns 400 without it). + ## 0.2.0-beta.4 ### Patch Changes diff --git a/packages/types/package.json b/packages/types/package.json index 39ebbdb..c94d6e8 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-types", - "version": "0.2.0-beta.4", + "version": "0.2.0", "description": "Shared TypeScript types for FMP Node Wrapper ecosystem (internal package)", "main": "./dist/index.js", "module": "./dist/index.mjs", From 787c59306b29ed62488c3a2da585205aa7c3376b Mon Sep 17 00:00:00 2001 From: e-roy Date: Thu, 28 May 2026 23:32:34 -0400 Subject: [PATCH 11/13] chore(apps): update deps (Next 16, Tailwind v4 examples, turbo); fix Next CVEs Bumps the three apps to clear the critical/high Next.js CVEs and modernizes the rest of the dependency surface. - apps/docs, both example apps -> Next 16.2.x, React 19.2.x, latest @types. - apps/docs: eslint-config-next 16; switched lint to flat-config (Next 16 removed `next lint`). Added eslint.config.mjs using eslint-config-next/core-web-vitals. - apps/docs MDX: Next 16 builds with Turbopack by default, which requires serializable loader options. Switched remark-gfm to string-form plugin spec (`[['remark-gfm', {}]]`) so @next/mdx serializes cleanly. - apps/examples/*: migrated Tailwind v3 -> v4. Replaced `tailwindcss + autoprefixer` PostCSS plugins with `@tailwindcss/postcss`, collapsed the three @tailwind directives to `@import 'tailwindcss';`, deleted the empty tailwind.config.js files (v4 auto-detects content paths). Dropped `next lint` scripts (examples are demos with no lint config; lint:all now skips them cleanly via turbo). - Bumped apps/examples deps to latest minor: ai 6.0.191, @ai-sdk/openai 3.0.65, @ai-sdk/react 3.0.193, @openai/agents 0.11.5, zod 4.3.6. - Root: turbo 2.5.5 -> 2.9.16 (clears the moderate CVE on <=2.9.13). - Trivial JSX cleanup in docs (unescaped apostrophes; annotated the next-themes "mounted" effect for the new react-hooks/set-state-in-effect rule). Verification: build/type-check/lint:all/test all green across the 6 turbo tasks. pnpm audit: 130 -> 50 vulnerabilities (critical 4 -> 1, high 56 -> 23). Remaining 1 critical and most high advisories trace through packages/api > axios@1.10.0 > form-data@4.0.3; bumping axios to >=1.15.0 in packages/api is the fix and is tracked as a separate 0.2.1 patch release (apps cannot fix a vuln in a workspace-linked published package). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/docs/eslint.config.mjs | 7 + apps/docs/next-env.d.ts | 1 + apps/docs/next.config.ts | 6 +- apps/docs/package.json | 24 +- apps/docs/src/app/not-found.tsx | 4 +- apps/docs/src/app/page.tsx | 4 +- .../src/components/theme/theme-toggle.tsx | 3 + apps/examples/openai/next-env.d.ts | 1 + apps/examples/openai/package.json | 28 +- apps/examples/openai/postcss.config.js | 3 +- apps/examples/openai/src/app/globals.css | 4 +- apps/examples/openai/tailwind.config.js | 12 - apps/examples/vercel-ai/next-env.d.ts | 1 + apps/examples/vercel-ai/package.json | 30 +- apps/examples/vercel-ai/postcss.config.js | 3 +- apps/examples/vercel-ai/src/app/globals.css | 4 +- apps/examples/vercel-ai/tailwind.config.js | 12 - package.json | 2 +- pnpm-lock.yaml | 2247 +++++++---------- 19 files changed, 912 insertions(+), 1484 deletions(-) create mode 100644 apps/docs/eslint.config.mjs delete mode 100644 apps/examples/openai/tailwind.config.js delete mode 100644 apps/examples/vercel-ai/tailwind.config.js diff --git a/apps/docs/eslint.config.mjs b/apps/docs/eslint.config.mjs new file mode 100644 index 0000000..bec7487 --- /dev/null +++ b/apps/docs/eslint.config.mjs @@ -0,0 +1,7 @@ +// Flat ESLint config for the docs site. +// Next 16 removed `next lint`, so we wire ESLint directly using the flat-config +// array exported by eslint-config-next/core-web-vitals (which extends the base +// next config with the Core Web Vitals rule pack). +import nextConfig from 'eslint-config-next/core-web-vitals'; + +export default nextConfig; diff --git a/apps/docs/next-env.d.ts b/apps/docs/next-env.d.ts index 1b3be08..9edff1c 100644 --- a/apps/docs/next-env.d.ts +++ b/apps/docs/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/docs/next.config.ts b/apps/docs/next.config.ts index 547d062..509125b 100644 --- a/apps/docs/next.config.ts +++ b/apps/docs/next.config.ts @@ -1,16 +1,18 @@ import type { NextConfig } from 'next'; import createMDX from '@next/mdx'; -import remarkGfm from 'remark-gfm'; const nextConfig: NextConfig = { pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'], transpilePackages: ['fmp-node-api'], }; +// Note: Next 16 defaults to Turbopack for builds, which requires serializable +// loader options. Pass remark/rehype plugins as string IDs (or `[name, options]` +// tuples) instead of imported functions so Turbopack can serialize them. const withMDX = createMDX({ extension: /\.mdx$/, options: { - remarkPlugins: [remarkGfm], + remarkPlugins: [['remark-gfm', {}]], rehypePlugins: [], }, }); diff --git a/apps/docs/package.json b/apps/docs/package.json index 758d2d2..0d6fee1 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -7,8 +7,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", - "lint:fix": "next lint --fix", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "type-check": "tsc --noEmit", "format": "prettier --write \"src/**/*.{ts,tsx,mdx}\"", "format:check": "prettier --check \"src/**/*.{ts,tsx,mdx}\"", @@ -18,29 +18,29 @@ "fmp-node-api": "workspace:*", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", - "@next/mdx": "^15.3.4", + "@next/mdx": "^16.2.1", "@radix-ui/react-slot": "^1.2.3", - "@tailwindcss/postcss": "^4.1.11", + "@tailwindcss/postcss": "^4.3.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.525.0", - "next": "^15.3.4", + "next": "^16.2.1", "next-themes": "^0.4.6", "prism-react-renderer": "^2.3.1", - "react": "^19.1.0", - "react-dom": "^19.1.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", "react-syntax-highlighter": "^15.5.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.11" + "tailwindcss": "^4.2.2" }, "devDependencies": { - "@types/node": "^24.0.7", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", + "@types/node": "^25.5.2", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", "eslint": "^9.0.0", - "eslint-config-next": "^15.2.3", + "eslint-config-next": "^16.2.1", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "prettier": "^3.2.5", diff --git a/apps/docs/src/app/not-found.tsx b/apps/docs/src/app/not-found.tsx index 09f4ada..214f06c 100644 --- a/apps/docs/src/app/not-found.tsx +++ b/apps/docs/src/app/not-found.tsx @@ -21,8 +21,8 @@ export default function NotFound() { Page Not Found

- Sorry, we couldn't find the page you're looking for. It might have been moved, deleted, - or you entered the wrong URL. + Sorry, we couldn't find the page you're looking for. It might have been + moved, deleted, or you entered the wrong URL.

diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx index 8c6b811..7ec2cde 100644 --- a/apps/docs/src/app/page.tsx +++ b/apps/docs/src/app/page.tsx @@ -39,8 +39,8 @@ export default function Home() { 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. + 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 diff --git a/apps/docs/src/components/theme/theme-toggle.tsx b/apps/docs/src/components/theme/theme-toggle.tsx index 28b5bf7..4e7cbe7 100644 --- a/apps/docs/src/components/theme/theme-toggle.tsx +++ b/apps/docs/src/components/theme/theme-toggle.tsx @@ -9,6 +9,9 @@ export function ThemeToggle() { const [mounted, setMounted] = useState(false); useEffect(() => { + // SSR-safe "mounted" flag is the documented next-themes pattern for reading + // `resolvedTheme` without hydration mismatch. Intentional setState-in-effect. + // eslint-disable-next-line react-hooks/set-state-in-effect setMounted(true); }, []); diff --git a/apps/examples/openai/next-env.d.ts b/apps/examples/openai/next-env.d.ts index 1b3be08..9edff1c 100644 --- a/apps/examples/openai/next-env.d.ts +++ b/apps/examples/openai/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // 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 0f26809..f6c9d67 100644 --- a/apps/examples/openai/package.json +++ b/apps/examples/openai/package.json @@ -5,26 +5,22 @@ "scripts": { "dev": "next dev -p 3001", "build": "next build", - "start": "next start", - "lint": "next lint" + "start": "next start" }, "dependencies": { - "@openai/agents": "^0.11.0", + "@openai/agents": "^0.11.5", "fmp-ai-tools": "workspace:*", - "next": "15.3.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "zod": "^4.0.0" + "next": "^16.2.1", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "zod": "^4.3.6" }, "devDependencies": { - "@types/node": "^20.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "autoprefixer": "^10.4.0", - "eslint": "^8.0.0", - "eslint-config-next": "15.0.0", - "postcss": "^8.4.0", - "tailwindcss": "^3.4.0", - "typescript": "^5.0.0" + "@types/node": "^25.5.2", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "tailwindcss": "^4.2.2", + "@tailwindcss/postcss": "^4.3.0", + "typescript": "^5.4.5" } } diff --git a/apps/examples/openai/postcss.config.js b/apps/examples/openai/postcss.config.js index 12a703d..e564072 100644 --- a/apps/examples/openai/postcss.config.js +++ b/apps/examples/openai/postcss.config.js @@ -1,6 +1,5 @@ module.exports = { plugins: { - tailwindcss: {}, - autoprefixer: {}, + '@tailwindcss/postcss': {}, }, }; diff --git a/apps/examples/openai/src/app/globals.css b/apps/examples/openai/src/app/globals.css index e0936ed..da496b1 100644 --- a/apps/examples/openai/src/app/globals.css +++ b/apps/examples/openai/src/app/globals.css @@ -1,6 +1,4 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; :root { --foreground-rgb: 0, 0, 0; diff --git a/apps/examples/openai/tailwind.config.js b/apps/examples/openai/tailwind.config.js deleted file mode 100644 index 6c96376..0000000 --- a/apps/examples/openai/tailwind.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - './src/pages/**/*.{js,ts,jsx,tsx,mdx}', - './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/app/**/*.{js,ts,jsx,tsx,mdx}', - ], - theme: { - extend: {}, - }, - plugins: [], -}; diff --git a/apps/examples/vercel-ai/next-env.d.ts b/apps/examples/vercel-ai/next-env.d.ts index 1b3be08..9edff1c 100644 --- a/apps/examples/vercel-ai/next-env.d.ts +++ b/apps/examples/vercel-ai/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // 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 6723699..48548ba 100644 --- a/apps/examples/vercel-ai/package.json +++ b/apps/examples/vercel-ai/package.json @@ -5,26 +5,24 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", - "lint": "next lint" + "start": "next start" }, "dependencies": { - "next": "15.3.0", - "react": "^19.2.1", - "react-dom": "^19.2.1", - "ai": "^6.0.0", - "@ai-sdk/openai": "^3.0.0", - "@ai-sdk/react": "^3.0.0", + "next": "^16.2.1", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "ai": "^6.0.191", + "@ai-sdk/openai": "^3.0.65", + "@ai-sdk/react": "^3.0.193", "fmp-ai-tools": "workspace:*", - "zod": "^4.0.0" + "zod": "^4.3.6" }, "devDependencies": { - "@types/node": "^20.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "typescript": "^5.0.0", - "tailwindcss": "^3.4.0", - "autoprefixer": "^10.4.0", - "postcss": "^8.4.0" + "@types/node": "^25.5.2", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "typescript": "^5.4.5", + "tailwindcss": "^4.2.2", + "@tailwindcss/postcss": "^4.3.0" } } diff --git a/apps/examples/vercel-ai/postcss.config.js b/apps/examples/vercel-ai/postcss.config.js index 12a703d..e564072 100644 --- a/apps/examples/vercel-ai/postcss.config.js +++ b/apps/examples/vercel-ai/postcss.config.js @@ -1,6 +1,5 @@ module.exports = { plugins: { - tailwindcss: {}, - autoprefixer: {}, + '@tailwindcss/postcss': {}, }, }; diff --git a/apps/examples/vercel-ai/src/app/globals.css b/apps/examples/vercel-ai/src/app/globals.css index e0936ed..da496b1 100644 --- a/apps/examples/vercel-ai/src/app/globals.css +++ b/apps/examples/vercel-ai/src/app/globals.css @@ -1,6 +1,4 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; :root { --foreground-rgb: 0, 0, 0; diff --git a/apps/examples/vercel-ai/tailwind.config.js b/apps/examples/vercel-ai/tailwind.config.js deleted file mode 100644 index 6c96376..0000000 --- a/apps/examples/vercel-ai/tailwind.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - './src/pages/**/*.{js,ts,jsx,tsx,mdx}', - './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/app/**/*.{js,ts,jsx,tsx,mdx}', - ], - theme: { - extend: {}, - }, - plugins: [], -}; diff --git a/package.json b/package.json index de0c6b7..df2e9c9 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "jest": "^29.7.0", "rimraf": "^5.0.5", "ts-jest": "^29.1.2", - "turbo": "^2.5.5", + "turbo": "^2.9.14", "typescript": "^5.3.3" }, "packageManager": "pnpm@9.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c33ef5..bced10a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,13 +16,13 @@ importers: version: 9.32.0 '@typescript-eslint/eslint-plugin': specifier: ^8.0.0 - version: 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) '@typescript-eslint/parser': specifier: ^8.0.0 - version: 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) eslint: specifier: ^9.0.0 - version: 9.32.0(jiti@2.4.2) + version: 9.32.0(jiti@2.7.0) jest: specifier: ^29.7.0 version: 29.7.0 @@ -33,8 +33,8 @@ importers: specifier: ^29.1.2 version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.8.3) turbo: - specifier: ^2.5.5 - version: 2.5.5 + specifier: ^2.9.14 + version: 2.9.16 typescript: specifier: ^5.3.3 version: 5.8.3 @@ -46,16 +46,16 @@ importers: version: 3.1.0(acorn@8.15.0) '@mdx-js/react': specifier: ^3.1.0 - version: 3.1.0(@types/react@19.1.8)(react@19.1.0) + version: 3.1.0(@types/react@19.2.15)(react@19.2.6) '@next/mdx': - specifier: ^15.3.4 - version: 15.3.4(@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)) + specifier: ^16.2.1 + version: 16.2.6(@mdx-js/loader@3.1.0(acorn@8.15.0))(@mdx-js/react@3.1.0(@types/react@19.2.15)(react@19.2.6)) '@radix-ui/react-slot': specifier: ^1.2.3 - version: 1.2.3(@types/react@19.1.8)(react@19.1.0) + version: 1.2.3(@types/react@19.2.15)(react@19.2.6) '@tailwindcss/postcss': - specifier: ^4.1.11 - version: 4.1.11 + specifier: ^4.3.0 + version: 4.3.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -67,25 +67,25 @@ importers: version: link:../../packages/api lucide-react: specifier: ^0.525.0 - version: 0.525.0(react@19.1.0) + version: 0.525.0(react@19.2.6) next: - specifier: ^15.3.4 - version: 15.3.4(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^16.2.1 + version: 16.2.6(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) next-themes: specifier: ^0.4.6 - version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) prism-react-renderer: specifier: ^2.3.1 - version: 2.4.1(react@19.1.0) + version: 2.4.1(react@19.2.6) react: - specifier: ^19.1.0 - version: 19.1.0 + specifier: ^19.2.6 + version: 19.2.6 react-dom: - specifier: ^19.1.0 - version: 19.1.0(react@19.1.0) + specifier: ^19.2.6 + version: 19.2.6(react@19.2.6) react-syntax-highlighter: specifier: ^15.5.0 - version: 15.6.1(react@19.1.0) + version: 15.6.1(react@19.2.6) remark-gfm: specifier: ^4.0.1 version: 4.0.1 @@ -93,33 +93,33 @@ importers: specifier: ^3.3.1 version: 3.3.1 tailwindcss: - specifier: ^4.1.11 - version: 4.1.11 + specifier: ^4.2.2 + version: 4.3.0 devDependencies: '@types/node': - specifier: ^24.0.7 - version: 24.0.7 + specifier: ^25.5.2 + version: 25.9.1 '@types/react': - specifier: ^19.1.8 - version: 19.1.8 + specifier: ^19.2.15 + version: 19.2.15 '@types/react-dom': - specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.8) + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.15) '@types/react-syntax-highlighter': specifier: ^15.5.13 version: 15.5.13 '@typescript-eslint/eslint-plugin': specifier: ^8.0.0 - version: 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) '@typescript-eslint/parser': specifier: ^8.0.0 - version: 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) eslint: specifier: ^9.0.0 - version: 9.32.0(jiti@2.4.2) + version: 9.32.0(jiti@2.7.0) eslint-config-next: - specifier: ^15.2.3 - version: 15.3.4(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^16.2.1 + version: 16.2.6(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) prettier: specifier: ^3.2.5 version: 3.6.2 @@ -133,99 +133,87 @@ importers: apps/examples/openai: dependencies: '@openai/agents': - specifier: ^0.11.0 + specifier: ^0.11.5 version: 0.11.5(ws@8.18.3)(zod@4.4.3) fmp-ai-tools: specifier: workspace:* version: link:../../../packages/tools next: - 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) + specifier: ^16.2.1 + version: 16.2.6(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: - specifier: ^19.0.0 - version: 19.1.0 + specifier: ^19.2.6 + version: 19.2.6 react-dom: - specifier: ^19.0.0 - version: 19.1.0(react@19.1.0) + specifier: ^19.2.6 + version: 19.2.6(react@19.2.6) zod: - specifier: ^4.0.0 + specifier: ^4.3.6 version: 4.4.3 devDependencies: + '@tailwindcss/postcss': + specifier: ^4.3.0 + version: 4.3.0 '@types/node': - specifier: ^20.0.0 - version: 20.19.2 + specifier: ^25.5.2 + version: 25.9.1 '@types/react': - specifier: ^19.0.0 - version: 19.1.8 + specifier: ^19.2.15 + version: 19.2.15 '@types/react-dom': - specifier: ^19.0.0 - version: 19.1.6(@types/react@19.1.8) - autoprefixer: - specifier: ^10.4.0 - version: 10.4.21(postcss@8.5.6) - eslint: - specifier: ^8.0.0 - version: 8.57.1 - eslint-config-next: - specifier: 15.0.0 - version: 15.0.0(eslint@8.57.1)(typescript@5.8.3) - postcss: - specifier: ^8.4.0 - version: 8.5.6 + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.15) tailwindcss: - specifier: ^3.4.0 - version: 3.4.17 + specifier: ^4.2.2 + version: 4.3.0 typescript: - specifier: ^5.0.0 + specifier: ^5.4.5 version: 5.8.3 apps/examples/vercel-ai: dependencies: '@ai-sdk/openai': - specifier: ^3.0.0 + specifier: ^3.0.65 version: 3.0.65(zod@4.4.3) '@ai-sdk/react': - specifier: ^3.0.0 + specifier: ^3.0.193 version: 3.0.193(react@19.2.6)(zod@4.4.3) ai: - specifier: ^6.0.0 + specifier: ^6.0.191 version: 6.0.191(zod@4.4.3) fmp-ai-tools: specifier: workspace:* version: link:../../../packages/tools next: - specifier: 15.3.0 - version: 15.3.0(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^16.2.1 + version: 16.2.6(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: - specifier: ^19.2.1 + specifier: ^19.2.6 version: 19.2.6 react-dom: - specifier: ^19.2.1 + specifier: ^19.2.6 version: 19.2.6(react@19.2.6) zod: - specifier: ^4.0.0 + specifier: ^4.3.6 version: 4.4.3 devDependencies: + '@tailwindcss/postcss': + specifier: ^4.3.0 + version: 4.3.0 '@types/node': - specifier: ^20.0.0 - version: 20.19.1 + specifier: ^25.5.2 + version: 25.9.1 '@types/react': - specifier: ^19.0.0 - version: 19.1.8 + specifier: ^19.2.15 + version: 19.2.15 '@types/react-dom': - specifier: ^19.0.0 - version: 19.1.6(@types/react@19.1.8) - autoprefixer: - specifier: ^10.4.0 - version: 10.4.21(postcss@8.5.6) - postcss: - specifier: ^8.4.0 - version: 8.5.6 + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.15) tailwindcss: - specifier: ^3.4.0 - version: 3.4.17 + specifier: ^4.2.2 + version: 4.3.0 typescript: - specifier: ^5.0.0 + specifier: ^5.4.5 version: 5.8.3 packages/api: @@ -257,7 +245,7 @@ importers: version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) tsup: specifier: ^8.0.0 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) + version: 8.5.0(jiti@2.7.0)(postcss@8.5.15)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) tsx: specifier: ^4.7.0 version: 4.20.3 @@ -285,10 +273,10 @@ importers: version: 20.19.1 '@typescript-eslint/eslint-plugin': specifier: ^8.0.0 - version: 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) '@typescript-eslint/parser': specifier: ^8.0.0 - version: 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) ai: specifier: ^6.0.0 version: 6.0.191(zod@4.4.3) @@ -297,7 +285,7 @@ importers: version: 16.5.0 eslint: specifier: ^9.0.0 - version: 9.32.0(jiti@2.4.2) + version: 9.32.0(jiti@2.7.0) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.19.1) @@ -309,7 +297,7 @@ importers: version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3) tsup: specifier: ^8.0.0 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) + version: 8.5.0(jiti@2.7.0)(postcss@8.5.15)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) tsx: specifier: ^4.20.3 version: 4.20.3 @@ -337,7 +325,7 @@ importers: version: 5.1.0 tsup: specifier: ^8.0.0 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) + version: 8.5.0(jiti@2.7.0)(postcss@8.5.15)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.3 @@ -609,6 +597,9 @@ packages: '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.4.3': resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} @@ -771,10 +762,20 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/config-array@0.21.0': resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -787,18 +788,10 @@ packages: resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@eslint/js@9.32.0': resolution: {integrity: sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -825,19 +818,10 @@ packages: resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} engines: {node: '>=18.18.0'} - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead - '@humanwhocodes/retry@0.3.1': resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} @@ -846,118 +830,139 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@img/sharp-darwin-arm64@0.34.2': - resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.2': - resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.1.0': - resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.1.0': - resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.1.0': - resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.1.0': - resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.1.0': - resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.1.0': - resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.1.0': - resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': - resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.1.0': - resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.2': - resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.2': - resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.34.2': - resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.2': - resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.2': - resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.2': - resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==} + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.2': - resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==} + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.2': - resolution: {integrity: sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==} + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.2': - resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==} + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.2': - resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==} + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -966,10 +971,6 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -1048,6 +1049,9 @@ packages: resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1059,6 +1063,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -1098,20 +1105,14 @@ packages: '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} - '@next/env@15.3.0': - resolution: {integrity: sha512-6mDmHX24nWlHOlbwUiAOmMyY7KELimmi+ed8qWcJYjqXeC+G6JzPZ3QosOAfjNwgMIzwhXBiRiCgdh8axTTdTA==} - - '@next/env@15.3.4': - resolution: {integrity: sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==} + '@next/env@16.2.6': + resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} - '@next/eslint-plugin-next@15.0.0': - resolution: {integrity: sha512-UG/Gnsq6Sc4wRhO9qk+vc/2v4OfRXH7GEH6/TGlNF5eU/vI9PIO7q+kgd65X2DxJ+qIpHWpzWwlPLmqMi1FE9A==} + '@next/eslint-plugin-next@16.2.6': + resolution: {integrity: sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==} - '@next/eslint-plugin-next@15.3.4': - resolution: {integrity: sha512-lBxYdj7TI8phbJcLSAqDt57nIcobEign5NYIKCiy0hXQhrUbTqLqOaSDi568U6vFg4hJfBdZYsG4iP/uKhCqgg==} - - '@next/mdx@15.3.4': - resolution: {integrity: sha512-Ok4Laq+Yxxu0hPefpE7Yi19dj8BBTIw9/Kf0fbRByn2sYF1cAINFG1EcfcZUy6tZ5ctB8jEtjzixUsKXvFuRXA==} + '@next/mdx@16.2.6': + resolution: {integrity: sha512-0hdoSkzRbyud1dNRRDiyqD9FrxR2wwdiW+ffhYx+n+fXrFOJ7Nwpi8o7nUz2LiiM44BB9M0eIO1Evy3BBrS50A==} peerDependencies: '@mdx-js/loader': '>=0.15.0' '@mdx-js/react': '>=0.15.0' @@ -1121,98 +1122,50 @@ packages: '@mdx-js/react': optional: true - '@next/swc-darwin-arm64@15.3.0': - resolution: {integrity: sha512-PDQcByT0ZfF2q7QR9d+PNj3wlNN4K6Q8JoHMwFyk252gWo4gKt7BF8Y2+KBgDjTFBETXZ/TkBEUY7NIIY7A/Kw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-arm64@15.3.4': - resolution: {integrity: sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==} + '@next/swc-darwin-arm64@16.2.6': + resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.3.0': - resolution: {integrity: sha512-m+eO21yg80En8HJ5c49AOQpFDq+nP51nu88ZOMCorvw3g//8g1JSUsEiPSiFpJo1KCTQ+jm9H0hwXK49H/RmXg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@next/swc-darwin-x64@15.3.4': - resolution: {integrity: sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==} + '@next/swc-darwin-x64@16.2.6': + resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.3.0': - resolution: {integrity: sha512-H0Kk04ZNzb6Aq/G6e0un4B3HekPnyy6D+eUBYPJv9Abx8KDYgNMWzKt4Qhj57HXV3sTTjsfc1Trc1SxuhQB+Tg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-arm64-gnu@15.3.4': - resolution: {integrity: sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-arm64-musl@15.3.0': - resolution: {integrity: sha512-k8GVkdMrh/+J9uIv/GpnHakzgDQhrprJ/FbGQvwWmstaeFG06nnAoZCJV+wO/bb603iKV1BXt4gHG+s2buJqZA==} + '@next/swc-linux-arm64-gnu@16.2.6': + resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.3.4': - resolution: {integrity: sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==} + '@next/swc-linux-arm64-musl@16.2.6': + resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.3.0': - resolution: {integrity: sha512-ZMQ9yzDEts/vkpFLRAqfYO1wSpIJGlQNK9gZ09PgyjBJUmg8F/bb8fw2EXKgEaHbCc4gmqMpDfh+T07qUphp9A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-linux-x64-gnu@15.3.4': - resolution: {integrity: sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-linux-x64-musl@15.3.0': - resolution: {integrity: sha512-RFwq5VKYTw9TMr4T3e5HRP6T4RiAzfDJ6XsxH8j/ZeYq2aLsBqCkFzwMI0FmnSsLaUbOb46Uov0VvN3UciHX5A==} + '@next/swc-linux-x64-gnu@16.2.6': + resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.3.4': - resolution: {integrity: sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==} + '@next/swc-linux-x64-musl@16.2.6': + resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.3.0': - resolution: {integrity: sha512-a7kUbqa/k09xPjfCl0RSVAvEjAkYBYxUzSVAzk2ptXiNEL+4bDBo9wNC43G/osLA/EOGzG4CuNRFnQyIHfkRgQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@next/swc-win32-arm64-msvc@15.3.4': - resolution: {integrity: sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==} + '@next/swc-win32-arm64-msvc@16.2.6': + resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.3.0': - resolution: {integrity: sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@next/swc-win32-x64-msvc@15.3.4': - resolution: {integrity: sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==} + '@next/swc-win32-x64-msvc@16.2.6': + resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1389,9 +1342,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@rushstack/eslint-patch@1.12.0': - resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==} - '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -1404,71 +1354,68 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@tailwindcss/node@4.1.11': - resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} - '@tailwindcss/oxide-android-arm64@4.1.11': - resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.11': - resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.11': - resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.11': - resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': - resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': - resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.11': - resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.11': - resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.11': - resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.11': - resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -1479,24 +1426,54 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': - resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.11': - resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.11': - resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} - engines: {node: '>= 10'} + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.3.0': + resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==} + + '@turbo/darwin-64@2.9.16': + resolution: {integrity: sha512-jLjApWTSNd7JZ5JaLYfelW1ytnGQOvB7ivl+2RD1xQvJTbi8I9gBjzcga7tDZVPyaxpl10YTfJt3BrYXR18KDw==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.9.16': + resolution: {integrity: sha512-YPgrn+5HIGzrx0O2a631SV4MBQUe4W/DafMFUuBVgaU32PW9/OTT0ehviF0QSxTXuRJlHvW2eUTemddF5/spmw==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.9.16': + resolution: {integrity: sha512-vAEf1H6l26lTpl9FJ/peQo1NUB8RC0sbEJJz5mPcUhHA2bPDup2x3CZPgo/bH8S4cUcBLm4FN3UHd5iUO2RAew==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.9.16': + resolution: {integrity: sha512-xDBLR2PZg4BrQOchfG6svgpv5FCNJ2TOtT2psLdEJcdKo1BH+pnPs9Xj6pvUjgfkHbuvBOfeE4R6tvxMoQKDHQ==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.9.16': + resolution: {integrity: sha512-NBAJnaUiGdgkSzQwUIdOvkCkcpTSu58G/sBGa0mvBtzfvFOOgrQwepKOOQ8cp6sWM6OcKDNFj2p1dsZA1OWjPg==} + cpu: [x64] + os: [win32] - '@tailwindcss/postcss@4.1.11': - resolution: {integrity: sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==} + '@turbo/windows-arm64@2.9.16': + resolution: {integrity: sha512-Y7SJppD0Z8wjO3Ec0ZGd9KQ4Yv0BMnA8CIowj5Vp+OEVsosXDG2weK6/t1RRLfJmc2Ozrnd6y4DOgQys+mn3WQ==} + cpu: [arm64] + os: [win32] '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} @@ -1567,22 +1544,22 @@ packages: '@types/node@20.19.2': resolution: {integrity: sha512-9pLGGwdzOUBDYi0GNjM97FIA+f92fqSke6joWeBjWXllfNxZBs7qeMF7tvtOIsbY45xkWkxrdwUfUf3MnQa9gA==} - '@types/node@24.0.7': - resolution: {integrity: sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==} + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} - '@types/react-dom@19.1.6': - resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^19.0.0 + '@types/react': ^19.2.0 '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} - '@types/react@19.1.8': - resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -1610,6 +1587,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/eslint-plugin@8.60.0': + resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.60.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/parser@8.39.0': resolution: {integrity: sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1617,22 +1602,45 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.60.0': + resolution: {integrity: sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.39.0': resolution: {integrity: sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.60.0': + resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@8.39.0': resolution: {integrity: sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.60.0': + resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.39.0': resolution: {integrity: sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.60.0': + resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.39.0': resolution: {integrity: sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1640,16 +1648,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.60.0': + resolution: {integrity: sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/types@8.39.0': resolution: {integrity: sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.60.0': + resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.39.0': resolution: {integrity: sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.60.0': + resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.39.0': resolution: {integrity: sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1657,10 +1682,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.60.0': + resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.39.0': resolution: {integrity: sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.60.0': + resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/vfs@1.6.4': resolution: {integrity: sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==} peerDependencies: @@ -1853,9 +1889,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1919,13 +1952,6 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1976,14 +2002,15 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + engines: {node: '>=6.0.0'} + hasBin: true + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -2023,10 +2050,6 @@ packages: peerDependencies: esbuild: '>=0.18' - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -2051,10 +2074,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} @@ -2101,18 +2120,10 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -2167,13 +2178,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -2233,13 +2237,8 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -2328,6 +2327,10 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -2335,9 +2338,6 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2346,17 +2346,10 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dotenv@16.5.0: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} @@ -2400,8 +2393,8 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - enhanced-resolve@5.18.2: - resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -2477,19 +2470,10 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-next@15.0.0: - resolution: {integrity: sha512-HFeTwCR2lFEUWmdB00WZrzaak2CvMvxici38gQknA6Bu2HPizSE4PNFGaFzr5GupjBt+SBJ/E0GIP57ZptOD3g==} - peerDependencies: - eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 - typescript: '>=3.3.1' - peerDependenciesMeta: - typescript: - optional: true - - eslint-config-next@15.3.4: - resolution: {integrity: sha512-WqeumCq57QcTP2lYlV6BRUySfGiBYEXlQ1L0mQ+u4N4X4ZhUVSSQ52WtjqHv60pJ6dD7jn+YZc0d1/ZSsxccvg==} + eslint-config-next@16.2.6: + resolution: {integrity: sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==} peerDependencies: - eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + eslint: '>=9.0.0' typescript: '>=3.3.1' peerDependenciesMeta: typescript: @@ -2548,11 +2532,11 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-react-hooks@5.2.0: - resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} - engines: {node: '>=10'} + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} @@ -2560,10 +2544,6 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2576,11 +2556,9 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint@9.32.0: resolution: {integrity: sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==} @@ -2596,10 +2574,6 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -2735,10 +2709,6 @@ packages: picomatch: optional: true - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2765,10 +2735,6 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2805,9 +2771,6 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -2893,14 +2856,14 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -2961,6 +2924,12 @@ packages: hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -3060,9 +3029,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -3071,10 +3037,6 @@ packages: resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} engines: {node: '>= 0.4'} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -3163,10 +3125,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -3397,12 +3355,8 @@ packages: node-notifier: optional: true - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} - hasBin: true - - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true jose@6.2.3: @@ -3487,68 +3441,74 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lightningcss-darwin-arm64@1.30.1: - resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.30.1: - resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.30.1: - resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.30.1: - resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.30.1: - resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.30.1: - resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.30.1: - resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.30.1: - resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.30.1: - resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.30.1: - resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.30.1: - resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} lilconfig@3.1.3: @@ -3614,6 +3574,9 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -3853,15 +3816,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} - engines: {node: '>= 18'} - - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} @@ -3880,6 +3834,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + napi-postinstall@0.2.5: resolution: {integrity: sha512-kmsgUvCRIJohHjbZ3V8avP0I1Pekw329MVAMDzVxsrkjgdnqiwvMX5XwR+hWV66vsAtZ+iM+fVnq8RTQawUmCQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -3898,34 +3857,13 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.3.0: - resolution: {integrity: sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + next@16.2.6: + resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} + engines: {node: '>=20.9.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-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': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - - next@15.3.4: - resolution: {integrity: sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 + '@playwright/test': ^1.51.1 babel-plugin-react-compiler: '*' 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 @@ -3950,10 +3888,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -3962,10 +3896,6 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -4133,10 +4063,6 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -4160,35 +4086,11 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - - postcss-js@4.0.1: - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - - postcss-load-config@4.0.2: - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' + jiti: '>=1.21.0' postcss: '>=8.0.9' tsx: ^4.8.1 yaml: ^2.4.2 @@ -4202,25 +4104,12 @@ packages: yaml: optional: true - postcss-nested@6.2.0: - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -4299,11 +4188,6 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} - react-dom@19.1.0: - resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} - peerDependencies: - react: ^19.1.0 - react-dom@19.2.6: resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: @@ -4320,25 +4204,14 @@ packages: peerDependencies: react: '>= 0.14.0' - react@19.1.0: - resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} - engines: {node: '>=0.10.0'} - react@19.2.6: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} - read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -4431,11 +4304,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rimraf@5.0.10: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true @@ -4470,9 +4338,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.26.0: - resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -4513,8 +4378,8 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.34.2: - resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: @@ -4548,9 +4413,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -4616,10 +4478,6 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -4738,22 +4596,13 @@ packages: tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} - tailwindcss@3.4.17: - resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} - engines: {node: '>=14.0.0'} - hasBin: true - - tailwindcss@4.1.11: - resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} - tapable@2.2.2: - resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} - engines: {node: '>=18'} - term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -4816,9 +4665,6 @@ packages: text-swap-case@1.2.11: resolution: {integrity: sha512-PBmC5xvZdDZ4suikydpeXH0s4JV2XHelMj9/OEXEbA3oLpdV2A+B4BspVDWVw7C2Gi5eCareqk/7EE8I1/WwgQ==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - text-title-case@1.2.11: resolution: {integrity: sha512-V1GZy0XlqdkYUQm0tqm1jqtYlXJqFVMreBCTUReOaz8d/JbozTSpZrcakIeV8+1bN7LsvfhPFhA5zREiax6YIA==} @@ -4884,6 +4730,12 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -4957,38 +4809,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo-darwin-64@2.5.5: - resolution: {integrity: sha512-RYnTz49u4F5tDD2SUwwtlynABNBAfbyT2uU/brJcyh5k6lDLyNfYKdKmqd3K2ls4AaiALWrFKVSBsiVwhdFNzQ==} - cpu: [x64] - os: [darwin] - - turbo-darwin-arm64@2.5.5: - resolution: {integrity: sha512-Tk+ZeSNdBobZiMw9aFypQt0DlLsWSFWu1ymqsAdJLuPoAH05qCfYtRxE1pJuYHcJB5pqI+/HOxtJoQ40726Btw==} - cpu: [arm64] - os: [darwin] - - turbo-linux-64@2.5.5: - resolution: {integrity: sha512-2/XvMGykD7VgsvWesZZYIIVXMlgBcQy+ZAryjugoTcvJv8TZzSU/B1nShcA7IAjZ0q7OsZ45uP2cOb8EgKT30w==} - cpu: [x64] - os: [linux] - - turbo-linux-arm64@2.5.5: - resolution: {integrity: sha512-DW+8CjCjybu0d7TFm9dovTTVg1VRnlkZ1rceO4zqsaLrit3DgHnN4to4uwyuf9s2V/BwS3IYcRy+HG9BL596Iw==} - cpu: [arm64] - os: [linux] - - turbo-windows-64@2.5.5: - resolution: {integrity: sha512-q5p1BOy8ChtSZfULuF1BhFMYIx6bevXu4fJ+TE/hyNfyHJIfjl90Z6jWdqAlyaFLmn99X/uw+7d6T/Y/dr5JwQ==} - cpu: [x64] - os: [win32] - - turbo-windows-arm64@2.5.5: - resolution: {integrity: sha512-AXbF1KmpHUq3PKQwddMGoKMYhHsy5t1YBQO8HZ04HLMR0rWv9adYlQ8kaeQJTko1Ay1anOBFTqaxfVOOsu7+1Q==} - cpu: [arm64] - os: [win32] - - turbo@2.5.5: - resolution: {integrity: sha512-eZ7wI6KjtT1eBqCnh2JPXWNUAxtoxxfi6VdBdZFvil0ychCOTxbm7YLRBi1JSt7U3c+u3CLxpoPxLdvr/Npr3A==} + turbo@2.9.16: + resolution: {integrity: sha512-NqgRQy6j6dPYcdSdv0q1g9QsZg7SWg87RERM8otw/1AtKU2yTFVClOM7cbwKzOonZr/Ek1blTBucw64L9H0Bwg==} hasBin: true tw-animate-css@1.3.4: @@ -5002,10 +4824,6 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -5034,6 +4852,13 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typescript-eslint@8.60.0: + resolution: {integrity: sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -5049,8 +4874,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.8.0: - resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -5098,9 +4923,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -5198,10 +5020,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - yaml@2.8.0: resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} engines: {node: '>= 14.6'} @@ -5224,6 +5042,12 @@ packages: peerDependencies: zod: ^3.25.28 || ^4 + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -5624,6 +5448,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.4.3': dependencies: tslib: 2.8.1 @@ -5709,18 +5538,20 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': + '@eslint-community/eslint-utils@4.7.0(eslint@9.32.0(jiti@2.7.0))': dependencies: - eslint: 8.57.1 + eslint: 9.32.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.7.0(eslint@9.32.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.32.0(jiti@2.7.0))': dependencies: - eslint: 9.32.0(jiti@2.4.2) + eslint: 9.32.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} + '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 @@ -5735,20 +5566,6 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@2.1.4': - dependencies: - ajv: 6.12.6 - debug: 4.4.1 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 @@ -5763,8 +5580,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.1': {} - '@eslint/js@9.32.0': {} '@eslint/object-schema@2.1.6': {} @@ -5786,101 +5601,107 @@ snapshots: '@humanfs/core': 0.19.1 '@humanwhocodes/retry': 0.3.1 - '@humanwhocodes/config-array@0.13.0': - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.3': {} - '@humanwhocodes/retry@0.3.1': {} '@humanwhocodes/retry@0.4.3': {} - '@img/sharp-darwin-arm64@0.34.2': + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.34.2': + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.1.0': + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.1.0': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.1.0': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.1.0': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.1.0': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.1.0': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.1.0': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.1.0': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-arm64@0.34.2': + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.34.2': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.34.2': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linux-x64@0.34.2': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.2': + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.2': + '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@img/sharp-wasm32@0.34.2': + '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.4.3 + '@emnapi/runtime': 1.10.0 optional: true - '@img/sharp-win32-arm64@0.34.2': + '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-ia32@0.34.2': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-x64@0.34.2': + '@img/sharp-win32-x64@0.34.5': optional: true '@isaacs/cliui@8.0.2': @@ -5892,10 +5713,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 - '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -6074,12 +5891,19 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -6139,11 +5963,11 @@ snapshots: - acorn - supports-color - '@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0)': + '@mdx-js/react@3.1.0(@types/react@19.2.15)(react@19.2.6)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 19.1.8 - react: 19.1.0 + '@types/react': 19.2.15 + react: 19.2.6 '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: @@ -6175,71 +5999,41 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@next/env@15.3.0': {} - - '@next/env@15.3.4': {} - - '@next/eslint-plugin-next@15.0.0': - dependencies: - fast-glob: 3.3.1 + '@next/env@16.2.6': {} - '@next/eslint-plugin-next@15.3.4': + '@next/eslint-plugin-next@16.2.6': dependencies: fast-glob: 3.3.1 - '@next/mdx@15.3.4(@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/mdx@16.2.6(@mdx-js/loader@3.1.0(acorn@8.15.0))(@mdx-js/react@3.1.0(@types/react@19.2.15)(react@19.2.6))': dependencies: source-map: 0.7.4 optionalDependencies: '@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.3.0': - optional: true - - '@next/swc-darwin-arm64@15.3.4': - optional: true - - '@next/swc-darwin-x64@15.3.0': - optional: true - - '@next/swc-darwin-x64@15.3.4': - optional: true - - '@next/swc-linux-arm64-gnu@15.3.0': - optional: true - - '@next/swc-linux-arm64-gnu@15.3.4': - optional: true + '@mdx-js/react': 3.1.0(@types/react@19.2.15)(react@19.2.6) - '@next/swc-linux-arm64-musl@15.3.0': + '@next/swc-darwin-arm64@16.2.6': optional: true - '@next/swc-linux-arm64-musl@15.3.4': + '@next/swc-darwin-x64@16.2.6': optional: true - '@next/swc-linux-x64-gnu@15.3.0': + '@next/swc-linux-arm64-gnu@16.2.6': optional: true - '@next/swc-linux-x64-gnu@15.3.4': + '@next/swc-linux-arm64-musl@16.2.6': optional: true - '@next/swc-linux-x64-musl@15.3.0': + '@next/swc-linux-x64-gnu@16.2.6': optional: true - '@next/swc-linux-x64-musl@15.3.4': + '@next/swc-linux-x64-musl@16.2.6': optional: true - '@next/swc-win32-arm64-msvc@15.3.0': + '@next/swc-win32-arm64-msvc@16.2.6': optional: true - '@next/swc-win32-arm64-msvc@15.3.4': - optional: true - - '@next/swc-win32-x64-msvc@15.3.0': - optional: true - - '@next/swc-win32-x64-msvc@15.3.4': + '@next/swc-win32-x64-msvc@16.2.6': optional: true '@nodelib/fs.scandir@2.1.5': @@ -6333,18 +6127,18 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.15)(react@19.2.6)': dependencies: - react: 19.1.0 + react: 19.2.6 optionalDependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.15 - '@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.0)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 optionalDependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.15 '@rollup/rollup-android-arm-eabi@4.44.1': optional: true @@ -6408,8 +6202,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@rushstack/eslint-patch@1.12.0': {} - '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -6422,83 +6214,96 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 - '@tailwindcss/node@4.1.11': + '@tailwindcss/node@4.3.0': dependencies: - '@ampproject/remapping': 2.3.0 - enhanced-resolve: 5.18.2 - jiti: 2.4.2 - lightningcss: 1.30.1 - magic-string: 0.30.17 + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.1 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.11 + tailwindcss: 4.3.0 - '@tailwindcss/oxide-android-arm64@4.1.11': + '@tailwindcss/oxide-android-arm64@4.3.0': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.11': + '@tailwindcss/oxide-darwin-arm64@4.3.0': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.11': + '@tailwindcss/oxide-darwin-x64@4.3.0': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.11': + '@tailwindcss/oxide-freebsd-x64@4.3.0': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.11': + '@tailwindcss/oxide-linux-x64-musl@4.3.0': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.11': + '@tailwindcss/oxide-wasm32-wasi@4.3.0': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': optional: true - '@tailwindcss/oxide@4.1.11': - dependencies: - detect-libc: 2.0.4 - tar: 7.4.3 + '@tailwindcss/oxide@4.3.0': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.11 - '@tailwindcss/oxide-darwin-arm64': 4.1.11 - '@tailwindcss/oxide-darwin-x64': 4.1.11 - '@tailwindcss/oxide-freebsd-x64': 4.1.11 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 - '@tailwindcss/oxide-linux-x64-musl': 4.1.11 - '@tailwindcss/oxide-wasm32-wasi': 4.1.11 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - - '@tailwindcss/postcss@4.1.11': + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/postcss@4.3.0': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.11 - '@tailwindcss/oxide': 4.1.11 - postcss: 8.5.6 - tailwindcss: 4.1.11 + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + postcss: 8.5.15 + tailwindcss: 4.3.0 + + '@turbo/darwin-64@2.9.16': + optional: true + + '@turbo/darwin-arm64@2.9.16': + optional: true + + '@turbo/linux-64@2.9.16': + optional: true + + '@turbo/linux-arm64@2.9.16': + optional: true + + '@turbo/windows-64@2.9.16': + optional: true + + '@turbo/windows-arm64@2.9.16': + optional: true '@tybys/wasm-util@0.9.0': dependencies: @@ -6585,23 +6390,23 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@24.0.7': + '@types/node@25.9.1': dependencies: - undici-types: 7.8.0 + undici-types: 7.24.6 '@types/prismjs@1.26.5': {} - '@types/react-dom@19.1.6(@types/react@19.1.8)': + '@types/react-dom@19.2.3(@types/react@19.2.15)': dependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.15 '@types/react-syntax-highlighter@15.5.13': dependencies: - '@types/react': 19.1.8 + '@types/react': 19.2.15 - '@types/react@19.1.8': + '@types/react@19.2.15': dependencies: - csstype: 3.1.3 + csstype: 3.2.3 '@types/stack-utils@2.0.3': {} @@ -6611,7 +6416,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.2 + '@types/node': 25.9.1 '@types/yargs-parser@21.0.3': {} @@ -6619,15 +6424,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.39.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.39.0 - '@typescript-eslint/type-utils': 8.39.0(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/utils': 8.39.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/utils': 8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.39.0 - eslint: 8.57.1 + eslint: 9.32.0(jiti@2.7.0) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -6636,43 +6441,42 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.39.0 - '@typescript-eslint/type-utils': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.39.0 - eslint: 9.32.0(jiti@2.4.2) - graphemer: 1.4.0 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/type-utils': 8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.60.0 + eslint: 9.32.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) + ts-api-utils: 2.5.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.39.0 '@typescript-eslint/types': 8.39.0 '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.39.0 debug: 4.4.1 - eslint: 8.57.1 + eslint: 9.32.0(jiti@2.7.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.39.0 - '@typescript-eslint/types': 8.39.0 - '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.39.0 - debug: 4.4.1 - eslint: 9.32.0(jiti@2.4.2) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.60.0 + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.32.0(jiti@2.7.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6686,41 +6490,61 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.60.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.8.3) + '@typescript-eslint/types': 8.60.0 + debug: 4.4.3(supports-color@8.1.1) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.39.0': dependencies: '@typescript-eslint/types': 8.39.0 '@typescript-eslint/visitor-keys': 8.39.0 + '@typescript-eslint/scope-manager@8.60.0': + dependencies: + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 + '@typescript-eslint/tsconfig-utils@8.39.0(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.39.0(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.60.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.39.0 '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.39.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) debug: 4.4.1 - eslint: 8.57.1 + eslint: 9.32.0(jiti@2.7.0) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.39.0 - '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1 - eslint: 9.32.0(jiti@2.4.2) - ts-api-utils: 2.1.0(typescript@5.8.3) + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.32.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.39.0': {} + '@typescript-eslint/types@8.60.0': {} + '@typescript-eslint/typescript-estree@8.39.0(typescript@5.8.3)': dependencies: '@typescript-eslint/project-service': 8.39.0(typescript@5.8.3) @@ -6737,24 +6561,39 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.39.0(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.60.0(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.39.0 - '@typescript-eslint/types': 8.39.0 - '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.8.3) - eslint: 8.57.1 + '@typescript-eslint/project-service': 8.60.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.8.3) + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.39.0 '@typescript-eslint/types': 8.39.0 '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.8.3) - eslint: 9.32.0(jiti@2.4.2) + eslint: 9.32.0(jiti@2.7.0) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.32.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.8.3) + eslint: 9.32.0(jiti@2.7.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6764,6 +6603,11 @@ snapshots: '@typescript-eslint/types': 8.39.0 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.60.0': + dependencies: + '@typescript-eslint/types': 8.60.0 + eslint-visitor-keys: 5.0.1 + '@typescript/vfs@1.6.4(typescript@5.8.3)': dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -6909,8 +6753,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - arg@5.0.2: {} - argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -6998,16 +6840,6 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.4.21(postcss@8.5.6): - dependencies: - browserslist: 4.25.1 - caniuse-lite: 1.0.30001726 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.1.1 - postcss: 8.5.6 - postcss-value-parser: 4.2.0 - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -7085,12 +6917,12 @@ snapshots: balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.32: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 - binary-extensions@2.3.0: {} - body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -7145,10 +6977,6 @@ snapshots: esbuild: 0.25.5 load-tsconfig: 0.2.5 - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - bytes@3.1.2: optional: true @@ -7173,8 +7001,6 @@ snapshots: callsites@3.1.0: {} - camelcase-css@2.0.1: {} - camelcase@5.3.1: {} camelcase@6.3.0: {} @@ -7206,24 +7032,10 @@ snapshots: chardet@0.7.0: {} - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - chokidar@4.0.3: dependencies: readdirp: 4.1.2 - chownr@3.0.0: {} - ci-info@3.9.0: {} cjs-module-lexer@1.4.3: {} @@ -7269,18 +7081,6 @@ snapshots: color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - optional: true - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - optional: true - colorette@2.0.20: {} combined-stream@1.0.8: @@ -7372,9 +7172,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - cssesc@3.0.0: {} - - csstype@3.1.3: {} + csstype@3.2.3: {} damerau-levenshtein@1.0.8: {} @@ -7443,30 +7241,25 @@ snapshots: detect-libc@2.0.4: {} + detect-libc@2.1.2: + optional: true + detect-newline@3.1.0: {} devlop@1.1.0: dependencies: dequal: 2.0.3 - didyoumean@1.2.2: {} - diff-sequences@29.6.3: {} dir-glob@3.0.1: dependencies: path-type: 4.0.0 - dlv@1.1.3: {} - doctrine@2.1.0: dependencies: esutils: 2.0.3 - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - dotenv@16.5.0: {} dotenv@16.6.1: {} @@ -7499,10 +7292,10 @@ snapshots: encodeurl@2.0.0: optional: true - enhanced-resolve@5.18.2: + enhanced-resolve@5.22.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.2 + tapable: 2.3.3 enquirer@2.4.1: dependencies: @@ -7669,42 +7462,22 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@15.0.0(eslint@8.57.1)(typescript@5.8.3): - dependencies: - '@next/eslint-plugin-next': 15.0.0 - '@rushstack/eslint-patch': 1.12.0 - '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/parser': 8.39.0(eslint@8.57.1)(typescript@5.8.3) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) - eslint-plugin-react: 7.37.5(eslint@8.57.1) - eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - - eslint-config-next@15.3.4(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3): + eslint-config-next@16.2.6(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3): dependencies: - '@next/eslint-plugin-next': 15.3.4 - '@rushstack/eslint-patch': 1.12.0 - '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.32.0(jiti@2.4.2) + '@next/eslint-plugin-next': 16.2.6 + eslint: 9.32.0(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.32.0(jiti@2.4.2)) - eslint-plugin-react: 7.37.5(eslint@9.32.0(jiti@2.4.2)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.32.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.7.0)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.32.0(jiti@2.7.0)) + eslint-plugin-react: 7.37.5(eslint@9.32.0(jiti@2.7.0)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.32.0(jiti@2.7.0)) + globals: 16.4.0 + typescript-eslint: 8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: + - '@typescript-eslint/parser' - eslint-import-resolver-webpack - eslint-plugin-import-x - supports-color @@ -7717,88 +7490,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1 - eslint: 8.57.1 - get-tsconfig: 4.10.1 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.14 - unrs-resolver: 1.9.2 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1 - eslint: 9.32.0(jiti@2.4.2) - get-tsconfig: 4.10.1 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.14 - unrs-resolver: 1.9.2 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.39.0(eslint@8.57.1)(typescript@5.8.3) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.7.0)): dependencies: - debug: 3.2.7 + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.32.0(jiti@2.7.0) + get-tsconfig: 4.10.1 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.16 + unrs-resolver: 1.9.2 optionalDependencies: - '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.32.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)): dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.39.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) + eslint: 9.32.0(jiti@2.7.0) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.7.0)) transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7807,9 +7525,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.32.0(jiti@2.4.2) + eslint: 9.32.0(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -7821,32 +7539,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1): - dependencies: - aria-query: 5.3.2 - array-includes: 3.1.9 - array.prototype.flatmap: 1.3.3 - ast-types-flow: 0.0.8 - axe-core: 4.10.3 - axobject-query: 4.1.0 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - eslint: 8.57.1 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - language-tags: 1.0.9 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - safe-regex-test: 1.1.0 - string.prototype.includes: 2.0.1 - - eslint-plugin-jsx-a11y@6.10.2(eslint@9.32.0(jiti@2.4.2)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.32.0(jiti@2.7.0)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -7856,7 +7555,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.32.0(jiti@2.4.2) + eslint: 9.32.0(jiti@2.7.0) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -7865,37 +7564,18 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - - eslint-plugin-react-hooks@5.2.0(eslint@9.32.0(jiti@2.4.2)): + eslint-plugin-react-hooks@7.1.1(eslint@9.32.0(jiti@2.7.0)): dependencies: - eslint: 9.32.0(jiti@2.4.2) - - eslint-plugin-react@7.37.5(eslint@8.57.1): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 8.57.1 - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 + '@babel/core': 7.27.7 + '@babel/parser': 7.27.7 + eslint: 9.32.0(jiti@2.7.0) + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color - eslint-plugin-react@7.37.5(eslint@9.32.0(jiti@2.4.2)): + eslint-plugin-react@7.37.5(eslint@9.32.0(jiti@2.7.0)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -7903,7 +7583,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.32.0(jiti@2.4.2) + eslint: 9.32.0(jiti@2.7.0) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -7917,11 +7597,6 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -7931,52 +7606,11 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@8.57.1: - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.1 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color + eslint-visitor-keys@5.0.1: {} - eslint@9.32.0(jiti@2.4.2): + eslint@9.32.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.3.1 @@ -8012,7 +7646,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.4.2 + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -8022,12 +7656,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 - espree@9.6.1: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 3.4.3 - esprima@4.0.1: {} esquery@1.6.0: @@ -8204,10 +7832,6 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -8248,12 +7872,6 @@ snapshots: mlly: 1.7.4 rollup: 4.44.1 - flat-cache@3.2.0: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - rimraf: 3.0.2 - flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -8285,8 +7903,6 @@ snapshots: forwarded@0.2.0: optional: true - fraction.js@4.3.7: {} - fresh@2.0.0: optional: true @@ -8386,12 +8002,10 @@ snapshots: globals@11.12.0: {} - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - globals@14.0.0: {} + globals@16.4.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -8489,6 +8103,12 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + highlight.js@10.7.3: {} highlightjs-vue@1.0.0: {} @@ -8581,9 +8201,6 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: - optional: true - is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -8596,10 +8213,6 @@ snapshots: dependencies: has-bigints: 1.1.0 - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -8607,7 +8220,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.7.2 + semver: 7.8.1 is-callable@1.2.7: {} @@ -8676,8 +8289,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-plain-obj@4.1.0: {} is-promise@4.0.0: @@ -9201,9 +8812,7 @@ snapshots: - supports-color - ts-node - jiti@1.21.7: {} - - jiti@2.4.2: {} + jiti@2.7.0: {} jose@6.2.3: optional: true @@ -9275,50 +8884,54 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lightningcss-darwin-arm64@1.30.1: + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: optional: true - lightningcss-darwin-x64@1.30.1: + lightningcss-darwin-x64@1.32.0: optional: true - lightningcss-freebsd-x64@1.30.1: + lightningcss-freebsd-x64@1.32.0: optional: true - lightningcss-linux-arm-gnueabihf@1.30.1: + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true - lightningcss-linux-arm64-gnu@1.30.1: + lightningcss-linux-arm64-gnu@1.32.0: optional: true - lightningcss-linux-arm64-musl@1.30.1: + lightningcss-linux-arm64-musl@1.32.0: optional: true - lightningcss-linux-x64-gnu@1.30.1: + lightningcss-linux-x64-gnu@1.32.0: optional: true - lightningcss-linux-x64-musl@1.30.1: + lightningcss-linux-x64-musl@1.32.0: optional: true - lightningcss-win32-arm64-msvc@1.30.1: + lightningcss-win32-arm64-msvc@1.32.0: optional: true - lightningcss-win32-x64-msvc@1.30.1: + lightningcss-win32-x64-msvc@1.32.0: optional: true - lightningcss@1.30.1: + lightningcss@1.32.0: dependencies: detect-libc: 2.0.4 optionalDependencies: - lightningcss-darwin-arm64: 1.30.1 - lightningcss-darwin-x64: 1.30.1 - lightningcss-freebsd-x64: 1.30.1 - lightningcss-linux-arm-gnueabihf: 1.30.1 - lightningcss-linux-arm64-gnu: 1.30.1 - lightningcss-linux-arm64-musl: 1.30.1 - lightningcss-linux-x64-gnu: 1.30.1 - lightningcss-linux-x64-musl: 1.30.1 - lightningcss-win32-arm64-msvc: 1.30.1 - lightningcss-win32-x64-msvc: 1.30.1 + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 lilconfig@3.1.3: {} @@ -9376,14 +8989,18 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.525.0(react@19.1.0): + lucide-react@0.525.0(react@19.2.6): dependencies: - react: 19.1.0 + react: 19.2.6 magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@4.0.0: dependencies: semver: 7.7.2 @@ -9880,12 +9497,6 @@ snapshots: minipass@7.1.2: {} - minizlib@3.0.2: - dependencies: - minipass: 7.1.2 - - mkdirp@3.0.1: {} - mlly@1.7.4: dependencies: acorn: 8.15.0 @@ -9905,6 +9516,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@3.3.12: {} + napi-postinstall@0.2.5: {} natural-compare@1.4.0: {} @@ -9912,85 +9525,32 @@ snapshots: negotiator@1.0.0: optional: true - next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - react: 19.1.0 - react-dom: 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): + next-themes@0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@next/env': 15.3.0 - '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.15 - busboy: 1.6.0 - caniuse-lite: 1.0.30001726 - postcss: 8.4.31 - react: 19.1.0 - 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.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.34.2 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - next@15.3.0(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + next@16.2.6(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@next/env': 15.3.0 - '@swc/counter': 0.1.3 + '@next/env': 16.2.6 '@swc/helpers': 0.5.15 - busboy: 1.6.0 + baseline-browser-mapping: 2.10.32 caniuse-lite: 1.0.30001726 postcss: 8.4.31 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) styled-jsx: 5.1.6(@babel/core@7.27.7)(react@19.2.6) optionalDependencies: - '@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.34.2 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - - next@15.3.4(@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.3.4 - '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.15 - busboy: 1.6.0 - caniuse-lite: 1.0.30001726 - postcss: 8.4.31 - react: 19.1.0 - 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.3.4 - '@next/swc-darwin-x64': 15.3.4 - '@next/swc-linux-arm64-gnu': 15.3.4 - '@next/swc-linux-arm64-musl': 15.3.4 - '@next/swc-linux-x64-gnu': 15.3.4 - '@next/swc-linux-x64-musl': 15.3.4 - '@next/swc-win32-arm64-msvc': 15.3.4 - '@next/swc-win32-x64-msvc': 15.3.4 + '@next/swc-darwin-arm64': 16.2.6 + '@next/swc-darwin-x64': 16.2.6 + '@next/swc-linux-arm64-gnu': 16.2.6 + '@next/swc-linux-arm64-musl': 16.2.6 + '@next/swc-linux-x64-gnu': 16.2.6 + '@next/swc-linux-x64-musl': 16.2.6 + '@next/swc-win32-arm64-msvc': 16.2.6 + '@next/swc-win32-x64-msvc': 16.2.6 '@opentelemetry/api': 1.9.0 - sharp: 0.34.2 + sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -10001,16 +9561,12 @@ snapshots: normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - npm-run-path@4.0.1: dependencies: path-key: 3.1.1 object-assign@4.1.1: {} - object-hash@3.0.0: {} - object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -10183,8 +9739,6 @@ snapshots: picomatch@4.0.4: {} - pify@2.3.0: {} - pify@4.0.1: {} pirates@4.0.7: {} @@ -10204,55 +9758,24 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.10 - - postcss-js@4.0.1(postcss@8.5.6): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.5.6 - - postcss-load-config@4.0.2(postcss@8.5.6): - dependencies: - lilconfig: 3.1.3 - yaml: 2.8.0 - optionalDependencies: - postcss: 8.5.6 - - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(yaml@2.8.0): + postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.20.3)(yaml@2.8.0): dependencies: lilconfig: 3.1.3 optionalDependencies: - jiti: 2.4.2 - postcss: 8.5.6 + jiti: 2.7.0 + postcss: 8.5.15 tsx: 4.20.3 yaml: 2.8.0 - postcss-nested@6.2.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - postcss-selector-parser: 6.1.2 - - postcss-selector-parser@6.1.2: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-value-parser@4.2.0: {} - postcss@8.4.31: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.6: + postcss@8.5.15: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -10268,11 +9791,11 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prism-react-renderer@2.4.1(react@19.1.0): + prism-react-renderer@2.4.1(react@19.2.6): dependencies: '@types/prismjs': 1.26.5 clsx: 2.1.1 - react: 19.1.0 + react: 19.2.6 prismjs@1.27.0: {} @@ -10327,11 +9850,6 @@ snapshots: unpipe: 1.0.0 optional: true - react-dom@19.1.0(react@19.1.0): - dependencies: - react: 19.1.0 - scheduler: 0.26.0 - react-dom@19.2.6(react@19.2.6): dependencies: react: 19.2.6 @@ -10341,24 +9859,18 @@ snapshots: react-is@18.3.1: {} - react-syntax-highlighter@15.6.1(react@19.1.0): + react-syntax-highlighter@15.6.1(react@19.2.6): dependencies: '@babel/runtime': 7.27.6 highlight.js: 10.7.3 highlightjs-vue: 1.0.0 lowlight: 1.20.0 prismjs: 1.30.0 - react: 19.1.0 + react: 19.2.6 refractor: 3.6.0 - react@19.1.0: {} - react@19.2.6: {} - read-cache@1.0.0: - dependencies: - pify: 2.3.0 - read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -10366,10 +9878,6 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - readdirp@4.1.2: {} recma-build-jsx@1.0.0: @@ -10515,10 +10023,6 @@ snapshots: rfdc@1.4.1: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rimraf@5.0.10: dependencies: glob: 10.4.5 @@ -10588,8 +10092,6 @@ snapshots: safer-buffer@2.1.2: {} - scheduler@0.26.0: {} - scheduler@0.27.0: {} semver@6.3.1: {} @@ -10650,33 +10152,36 @@ snapshots: setprototypeof@1.2.0: optional: true - sharp@0.34.2: + sharp@0.34.5: dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.1 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.2 - '@img/sharp-darwin-x64': 0.34.2 - '@img/sharp-libvips-darwin-arm64': 1.1.0 - '@img/sharp-libvips-darwin-x64': 1.1.0 - '@img/sharp-libvips-linux-arm': 1.1.0 - '@img/sharp-libvips-linux-arm64': 1.1.0 - '@img/sharp-libvips-linux-ppc64': 1.1.0 - '@img/sharp-libvips-linux-s390x': 1.1.0 - '@img/sharp-libvips-linux-x64': 1.1.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 - '@img/sharp-libvips-linuxmusl-x64': 1.1.0 - '@img/sharp-linux-arm': 0.34.2 - '@img/sharp-linux-arm64': 0.34.2 - '@img/sharp-linux-s390x': 0.34.2 - '@img/sharp-linux-x64': 0.34.2 - '@img/sharp-linuxmusl-arm64': 0.34.2 - '@img/sharp-linuxmusl-x64': 0.34.2 - '@img/sharp-wasm32': 0.34.2 - '@img/sharp-win32-arm64': 0.34.2 - '@img/sharp-win32-ia32': 0.34.2 - '@img/sharp-win32-x64': 0.34.2 + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 optional: true shebang-command@2.0.0: @@ -10717,11 +10222,6 @@ snapshots: signal-exit@4.1.0: {} - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - optional: true - sisteransi@1.0.5: {} slash@3.0.0: {} @@ -10778,8 +10278,6 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - streamsearch@1.1.0: {} - string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -10891,13 +10389,6 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.6(@babel/core@7.27.7)(react@19.1.0): - dependencies: - client-only: 0.0.1 - react: 19.1.0 - optionalDependencies: - '@babel/core': 7.27.7 - styled-jsx@5.1.6(@babel/core@7.27.7)(react@19.2.6): dependencies: client-only: 0.0.1 @@ -10933,45 +10424,9 @@ snapshots: tailwind-merge@3.3.1: {} - tailwindcss@3.4.17: - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.3 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 3.1.3 - micromatch: 4.0.8 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.1.1 - postcss: 8.5.6 - postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.0.1(postcss@8.5.6) - postcss-load-config: 4.0.2(postcss@8.5.6) - postcss-nested: 6.2.0(postcss@8.5.6) - postcss-selector-parser: 6.1.2 - resolve: 1.22.10 - sucrase: 3.35.0 - transitivePeerDependencies: - - ts-node - - tailwindcss@4.1.11: {} + tailwindcss@4.3.0: {} - tapable@2.2.2: {} - - tar@7.4.3: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 - yallist: 5.0.0 + tapable@2.3.3: {} term-size@2.2.1: {} @@ -11065,8 +10520,6 @@ snapshots: text-swap-case@1.2.11: {} - text-table@0.2.0: {} - text-title-case@1.2.11: dependencies: text-no-case: 1.2.11 @@ -11125,6 +10578,10 @@ snapshots: dependencies: typescript: 5.8.3 + ts-api-utils@2.5.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + ts-interface-checker@0.1.13: {} ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3): @@ -11215,7 +10672,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0): + tsup@8.5.0(jiti@2.7.0)(postcss@8.5.15)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0): dependencies: bundle-require: 5.1.0(esbuild@0.25.5) cac: 6.7.14 @@ -11226,7 +10683,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(yaml@2.8.0) + postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.20.3)(yaml@2.8.0) resolve-from: 5.0.0 rollup: 4.44.1 source-map: 0.8.0-beta.0 @@ -11235,7 +10692,7 @@ snapshots: tinyglobby: 0.2.14 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.6 + postcss: 8.5.15 typescript: 5.8.3 transitivePeerDependencies: - jiti @@ -11255,32 +10712,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo-darwin-64@2.5.5: - optional: true - - turbo-darwin-arm64@2.5.5: - optional: true - - turbo-linux-64@2.5.5: - optional: true - - turbo-linux-arm64@2.5.5: - optional: true - - turbo-windows-64@2.5.5: - optional: true - - turbo-windows-arm64@2.5.5: - optional: true - - turbo@2.5.5: + turbo@2.9.16: optionalDependencies: - turbo-darwin-64: 2.5.5 - turbo-darwin-arm64: 2.5.5 - turbo-linux-64: 2.5.5 - turbo-linux-arm64: 2.5.5 - turbo-windows-64: 2.5.5 - turbo-windows-arm64: 2.5.5 + '@turbo/darwin-64': 2.9.16 + '@turbo/darwin-arm64': 2.9.16 + '@turbo/linux-64': 2.9.16 + '@turbo/linux-arm64': 2.9.16 + '@turbo/windows-64': 2.9.16 + '@turbo/windows-arm64': 2.9.16 tw-animate-css@1.3.4: {} @@ -11290,8 +10729,6 @@ snapshots: type-detect@4.0.8: {} - type-fest@0.20.2: {} - type-fest@0.21.3: {} type-fest@4.41.0: {} @@ -11336,6 +10773,17 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typescript-eslint@8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/parser': 8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) + eslint: 9.32.0(jiti@2.7.0) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + typescript@5.8.3: {} ufo@1.6.1: {} @@ -11349,7 +10797,7 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.8.0: {} + undici-types@7.24.6: {} unified@11.0.5: dependencies: @@ -11431,8 +10879,6 @@ snapshots: dependencies: react: 19.2.6 - util-deprecate@1.0.2: {} - v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -11550,9 +10996,8 @@ snapshots: yallist@3.1.1: {} - yallist@5.0.0: {} - - yaml@2.8.0: {} + yaml@2.8.0: + optional: true yargs-parser@21.1.1: {} @@ -11573,6 +11018,10 @@ snapshots: zod: 4.4.3 optional: true + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + zod@3.25.76: {} zod@4.4.3: {} From bf449abbd2d7549a7207f79afe2c09e1f8d96704 Mon Sep 17 00:00:00 2001 From: e-roy Date: Thu, 28 May 2026 23:44:50 -0400 Subject: [PATCH 12/13] chore: version packages 0.2.1 Patch release for fmp-node-api 0.2.1 (axios security floor bump from ^1.6.2 to ^1.13.0, resolves to 1.16.x at install time). Clears three transitive advisories surfaced by `pnpm audit` against consumers: - Critical: form-data unsafe random boundary (GHSA-fjxv-7rqg-78g4) - High: axios DoS via missing data-size check (GHSA-4hjh-wcwx-xvwj) - Moderate: axios cloud-metadata exfiltration (GHSA-fvcv-3m26-pcqx) fmp-ai-tools 0.2.1 cascades automatically per Changesets' updateInternalDependencies=patch setting (workspace dep on fmp-node-api). fmp-node-types stays at 0.2.0 (no source change). No runtime API changes; verified via build/type-check/lint:all/test + test:live (50/50 PASS, 0 drift). Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/api/CHANGELOG.md | 11 ++++++ packages/api/package.json | 4 +- packages/tools/CHANGELOG.md | 7 ++++ packages/tools/package.json | 2 +- pnpm-lock.yaml | 74 ++++++++++++++++++++++++------------- 5 files changed, 70 insertions(+), 28 deletions(-) diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 6f78b72..3150c7f 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,16 @@ # fmp-node-api +## 0.2.1 + +### Patch Changes + +- Bump `axios` dependency floor from `^1.6.2` to `^1.13.0` (resolves to 1.16.x at install time). Clears three transitive security advisories surfaced by `pnpm audit` against consumers: + - **Critical:** `form-data` unsafe random boundary (GHSA-fjxv-7rqg-78g4) — fixed because newer axios depends on patched `form-data`. + - **High:** axios DoS via missing data-size check (GHSA-4hjh-wcwx-xvwj) — patched in axios >=1.12.0. + - **Moderate:** axios cloud-metadata exfiltration via header injection (GHSA-fvcv-3m26-pcqx) — patched in axios >=1.15.0. + + No runtime API changes; the wrapper uses the same axios surface it always has. + ## 0.2.0 ### Minor Changes diff --git a/packages/api/package.json b/packages/api/package.json index b4e624d..e5e31cd 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-api", - "version": "0.2.0", + "version": "0.2.1", "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", @@ -74,7 +74,7 @@ }, "homepage": "https://fmp-node-wrapper-docs.vercel.app", "dependencies": { - "axios": "^1.6.2" + "axios": "^1.13.0" }, "devDependencies": { "@types/jest": "^29.5.12", diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index 6050a25..85f7c8d 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -1,5 +1,12 @@ # fmp-ai-tools +## 0.2.1 + +### Patch Changes + +- Updated dependencies + - fmp-node-api@0.2.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/tools/package.json b/packages/tools/package.json index cc90497..4d0ee76 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.2.0", + "version": "0.2.1", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bced10a..b79a906 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,8 +219,8 @@ importers: packages/api: dependencies: axios: - specifier: ^1.6.2 - version: 1.10.0 + specifier: ^1.13.0 + version: 1.16.1 devDependencies: '@types/jest': specifier: ^29.5.12 @@ -1818,6 +1818,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + ai@6.0.191: resolution: {integrity: sha512-zAxvjKebQE7YkSyyNIl0OM7i6/zygnKeF+yNUjD4nWOelYrG+LpDd6RnH6mjySI4zUpZ7o4wbnmAy8jc6u98vQ==} engines: {node: '>=18'} @@ -1960,8 +1964,8 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.10.0: - resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + axios@1.16.1: + resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -2742,8 +2746,8 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -2759,8 +2763,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.3: - resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} format@0.2.2: @@ -2947,6 +2951,10 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + human-id@4.1.1: resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} hasBin: true @@ -4160,8 +4168,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} @@ -6690,6 +6699,12 @@ snapshots: acorn@8.15.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + ai@6.0.191(zod@4.4.3): dependencies: '@ai-sdk/gateway': 3.0.120(zod@4.4.3) @@ -6846,13 +6861,15 @@ snapshots: axe-core@4.10.3: {} - axios@1.10.0: + axios@1.16.1: dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.3 - proxy-from-env: 1.1.0 + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 transitivePeerDependencies: - debug + - supports-color axobject-query@4.1.0: {} @@ -7467,8 +7484,8 @@ snapshots: '@next/eslint-plugin-next': 16.2.6 eslint: 9.32.0(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.32.0(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.32.0(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.32.0(jiti@2.7.0)) @@ -7490,7 +7507,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3(supports-color@8.1.1) @@ -7501,22 +7518,22 @@ snapshots: tinyglobby: 0.2.16 unrs-resolver: 1.9.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3) eslint: 9.32.0(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7527,7 +7544,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.32.0(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.7.0))(typescript@5.8.3))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)))(eslint@9.32.0(jiti@2.7.0)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -7879,7 +7896,7 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.9: {} + follow-redirects@1.16.0: {} for-each@0.3.5: dependencies: @@ -7890,7 +7907,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.3: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -8127,6 +8144,13 @@ snapshots: toidentifier: 1.0.1 optional: true + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + human-id@4.1.1: {} human-signals@2.1.0: {} @@ -9824,7 +9848,7 @@ snapshots: ipaddr.js: 1.9.1 optional: true - proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} punycode@2.3.1: {} From 9e20bbdcea234fbc537d7ce7c610540ab09ff73a Mon Sep 17 00:00:00 2001 From: e-roy Date: Thu, 28 May 2026 23:53:12 -0400 Subject: [PATCH 13/13] ci: drop Node 18 from test matrix; bump engines to >=20.9.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI was failing on test (18.x) because Next 16 (introduced in the apps dep update) requires Node >=20.9.0, which 18.x can't satisfy. The fmp-docs build step errored with: You are using Node.js 18.20.8. For Next.js, Node.js version ">=20.9.0" is required. Node 18 reached end of life 2025-04 anyway, so dropping it is the right call independent of Next 16. New matrix: [20.x, 22.x] — current active LTS line plus the latest LTS. Root engines bumped to match the binding constraint (Next 16). Published packages don't have explicit engines and continue to work on older Node where their runtime deps allow it. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 4 +++- package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e2c669..2f10038 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,9 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x] + # Node 18 reached EOL 2025-04 and Next 16 requires >=20.9.0; matrix + # covers the current active LTS (20) and the latest LTS (22). + node-version: [20.x, 22.x] steps: - uses: actions/checkout@v4 diff --git a/package.json b/package.json index df2e9c9..a02eb24 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,6 @@ }, "packageManager": "pnpm@9.2.0", "engines": { - "node": ">=18.0.0" + "node": ">=20.9.0" } }