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..95de2d2 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,32 @@ 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: - - `providers/vercel-ai/` - Vercel AI SDK tool providers - - Tool wrapper utilities and type definitions + - AI tool implementations compatible with Vercel AI SDK, OpenAI, and more + - `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 - - 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 +228,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 +247,22 @@ 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 -- **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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b3fd5d..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 @@ -104,7 +106,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/.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 new file mode 100644 index 0000000..c1cfebe --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# 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: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 +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 (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 + +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 + +### 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). + +**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 + +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+**live-check**+version+publish. The `pnpm test:live` step is a **release gate**: it validates the real FMP API against the canonical schemas and aborts the publish on any FAIL (e.g. a renamed/removed route), so always release via `pnpm publish-packages` (the CI publish job runs the same gate as its own step). It needs `FMP_API_KEY` present at release time. + +## 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/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/README.md b/README.md index 0ee2ba4..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,43 @@ 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 + +```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 @@ -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/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/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/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..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', + }, ]} /> @@ -76,19 +81,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 +108,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 +148,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' } ] }`} @@ -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 @@ -409,19 +455,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 +482,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 +499,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 +535,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; }`} @@ -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) @@ -545,7 +602,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..8dd0433 100644 --- a/apps/docs/src/app/docs/api/financial/page.mdx +++ b/apps/docs/src/app/docs/api/financial/page.mdx @@ -59,14 +59,39 @@ 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', 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', + }, ]} /> @@ -117,14 +142,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 +156,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 +228,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 +248,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, - minorityInterest: 0, - totalEquity: 112390000000, - totalLiabilitiesAndStockholdersEquity: 352755000000, + commonStock: 73812000000, + retainedEarnings: -214000000, + additionalPaidInCapital: 0, + accumulatedOtherComprehensiveIncomeLoss: -11452000000, + otherTotalStockholdersEquity: 0, + totalStockholdersEquity: 62146000000, + totalEquity: 62146000000, 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 +337,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 +427,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 +538,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 +687,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 +776,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 +791,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 +857,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 +887,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 +960,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 +974,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 +999,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 +1046,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' } ] }`} @@ -1051,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/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/apps/docs/src/app/docs/tools/categories/page.mdx b/apps/docs/src/app/docs/tools/categories/page.mdx index 1ebf611..011dfb3 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,74 @@ 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 + +### 🎯 Analyst Tools + +Forward-looking analyst estimates, price targets, and grades. + +**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) + +**Use Cases:** +- Outlook and sentiment analysis +- Comparing price to analyst targets + +### 🧮 Valuation Tools + +Fair-value estimates and company ratings. + +**Tools:** +- `getDiscountedCashFlow` - Discounted-cash-flow fair value vs. current price +- `getCompanyRating` - FMP's current rating/score snapshot for a company + +**Use Cases:** +- Over/undervaluation assessment +- Quality/score screening + +### 📐 Technical Tools + +Technical indicators over a chosen timeframe. + +**Tools:** +- `getTechnicalIndicator` - Indicator series (SMA, EMA, RSI, etc.) at a chosen timeframe (default `limit` 50) + +**Use Cases:** +- Trend and momentum analysis for trading agents + ## 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..862eede 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,28 @@ 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 + +### 🎯 Analyst Tools +- `getAnalystEstimates` - Analyst estimates (revenue, EBITDA, net income, EPS) by period +- `getPriceTargetConsensus` - Analyst price-target consensus +- `getStockGrades` - Recent analyst grades / upgrades & downgrades + +### 🧮 Valuation Tools +- `getDiscountedCashFlow` - DCF fair value vs. current price +- `getCompanyRating` - Company rating/score snapshot + +### 📐 Technical Tools +- `getTechnicalIndicator` - Indicator series (SMA, EMA, RSI, etc.) at a chosen timeframe + [**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..c447ff4 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,28 @@ 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 + +### 🎯 Analyst Tools +- `getAnalystEstimates` - Analyst estimates (revenue, EBITDA, net income, EPS) by period +- `getPriceTargetConsensus` - Analyst price-target consensus +- `getStockGrades` - Recent analyst grades / upgrades & downgrades + +### 🧮 Valuation Tools +- `getDiscountedCashFlow` - DCF fair value vs. current price +- `getCompanyRating` - Company rating/score snapshot + +### 📐 Technical Tools +- `getTechnicalIndicator` - Indicator series (SMA, EMA, RSI, etc.) at a chosen timeframe + [**View detailed tool categories →**](/docs/tools/categories) ## Advanced Configuration 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/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/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 93517fb..f6c9d67 100644 --- a/apps/examples/openai/package.json +++ b/apps/examples/openai/package.json @@ -5,27 +5,22 @@ "scripts": { "dev": "next dev -p 3001", "build": "next build", - "start": "next start", - "lint": "next lint" + "start": "next start" }, "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" + "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 2834e32..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.0.0", - "react-dom": "^19.0.0", - "ai": "^5.0.5", - "@ai-sdk/openai": "^2.0.3", - "@ai-sdk/react": "^2.0.3", + "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": "^3.25.76" + "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/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/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 37a5ffe..a02eb24 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", @@ -39,7 +39,7 @@ "type-check": "turbo run type-check", "format": "turbo run format", "format:check": "turbo run format:check", - "publish-packages": "turbo run build lint test --filter=./packages/* && changeset version && changeset publish" + "publish-packages": "turbo run build lint test --filter=./packages/* && pnpm test:live && changeset version && changeset publish" }, "devDependencies": { "@changesets/cli": "^2.27.1", @@ -50,11 +50,11 @@ "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", "engines": { - "node": ">=18.0.0" + "node": ">=20.9.0" } } diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index f2ca419..3150c7f 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,100 @@ # 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 + +- 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 + +- 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 + +- 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 + +- 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 + +- 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/README.md b/packages/api/README.md index 66a7aa7..b667a19 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 @@ -257,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 @@ -279,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({ @@ -296,13 +296,43 @@ 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'); ``` +### 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: @@ -314,28 +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 - -### 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 +- **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 @@ -349,11 +366,11 @@ pnpm test:company # Run all endpoint tests pnpm test:endpoints -# Manual testing with real API calls -pnpm test:manual +# Inspect one endpoint's raw response against the live API +pnpm test:endpoint -# Run specific endpoint test -pnpm test:endpoint +# Validate response shapes against the live API (needs FMP_API_KEY) +pnpm test:live ``` ## Response Format @@ -433,9 +450,6 @@ import { formatLargeNumber, formatDate, formatVolume, - formatNumber, - formatTimestamp, - formatReadableDate, } from 'fmp-node-api'; // Format financial data @@ -444,7 +458,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 @@ -489,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 @@ -501,15 +513,39 @@ 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. + +### 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 @@ -548,13 +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:manual # Manual API testing -pnpm test:endpoint # Run specific endpoint test +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 @@ -580,52 +615,45 @@ 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 +├── 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 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..e5e31cd 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.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", @@ -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" @@ -73,18 +74,19 @@ }, "homepage": "https://fmp-node-wrapper-docs.vercel.app", "dependencies": { - "axios": "^1.6.2" + "axios": "^1.13.0" }, "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..09e25ea --- /dev/null +++ b/packages/api/scripts/live/manifest.ts @@ -0,0 +1,323 @@ +// 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, + FinancialScoresSchema, + KeyMetricsTTMSchema, + FinancialRatiosTTMSchema, + RevenueProductSegmentationSchema, + RevenueGeographicSegmentationSchema, + // calendar + EarningsCalendarSchema, + EarningsConfirmedSchema, + DividendsCalendarSchema, + EconomicsCalendarSchema, + IPOCalendarSchema, + SplitsCalendarSchema, + // company + CompanyProfileSchema, + ExecutiveCompensationSchema, + CompanyNotesSchema, + HistoricalEmployeeCountSchema, + SharesFloatSchema, + HistoricalSharesFloatSchema, + EarningsCallTranscriptSchema, + CompanyTranscriptDataSchema, + StockPeerSchema, + // 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, + // search + SearchResultSchema, + // analyst + AnalystEstimateSchema, + PriceTargetConsensusSchema, + PriceTargetSummarySchema, + StockGradeSchema, + GradesConsensusSchema, + // valuation + DCFValuationSchema, + CompanyRatingSchema, + // technical + TechnicalIndicatorSchema, + // 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' + | 'search' + | 'analyst' + | 'valuation' + | 'technical' + | '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') }, + { 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' }) }, + { 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') }, + { 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' }) }, + { 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() }, + + // ---- search ---- + { category: 'search', name: 'search(AAPL)', schema: SearchResultSchema, kind: 'array', call: (fmp) => 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' }) }, + { 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' }) }, + { 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 }) }, + { 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__/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__/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/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/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/__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__/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/__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/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/__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/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/endpoints/analyst.ts b/packages/api/src/endpoints/analyst.ts new file mode 100644 index 0000000..aa67341 --- /dev/null +++ b/packages/api/src/endpoints/analyst.ts @@ -0,0 +1,59 @@ +// Analyst endpoints for FMP API + +import { FMPClient } from '@/client'; +import { + APIResponse, + AnalystEstimate, + PriceTargetConsensus, + PriceTargetSummary, + StockGrade, + GradesConsensus, +} 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 }); + } + + /** + * 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/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/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/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 6613417..0004522 100644 --- a/packages/api/src/fmp.ts +++ b/packages/api/src/fmp.ts @@ -17,6 +17,10 @@ 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 { 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'; @@ -65,6 +69,10 @@ export class FMP { public readonly news: NewsEndpoints; 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; @@ -99,6 +107,10 @@ export class FMP { this.news = new NewsEndpoints(client); 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 16df88f..5b72094 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -23,6 +23,10 @@ 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 { 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'; @@ -46,3 +50,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 new file mode 100644 index 0000000..80e72c6 --- /dev/null +++ b/packages/api/src/live/validate.ts @@ -0,0 +1,140 @@ +// 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'; +import { classifyError } from '../utils/error-classifier'; + +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 }; + +/** + * Classify a transport-level outcome. Returns null when the request succeeded + * (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 { errorType } = classifyError(res.status, res.error ?? ''); + + if (errorType === 'rate-limit') { + return { outcome: 'SKIP', detail: `quota/rate limit (${res.status}): ${res.error ?? ''}`.trim(), stopRun: true }; + } + + if (errorType === 'plan-restricted') { + return { outcome: 'SKIP', detail: `plan-locked (${res.status}): ${res.error ?? ''}`.trim() }; + } + + return { outcome: 'FAIL', detail: `request failed (${res.status}): ${res.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/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 038f866..85f7c8d 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -1,5 +1,135 @@ # fmp-ai-tools +## 0.2.1 + +### Patch Changes + +- Updated dependencies + - fmp-node-api@0.2.1 + +## 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 + +- 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 + +- 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 + +- 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 + +- 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 + +- 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 4effcb8..f36973c 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -7,46 +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 - -### OpenAI Agents Compatibility +```bash +pnpm add @openai/agents zod +``` -**⚠️ Important**: This package requires `@openai/agents` version `^0.1.0` or higher due to breaking changes in the API. +- `@openai/agents`: `>=0.11.0` +- `zod`: `^4.0.0` -If you're using an older version, you'll encounter errors like: +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'; @@ -56,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(); @@ -68,24 +64,24 @@ export async function POST(req: Request) { ### OpenAI Agents ```typescript -import { Agent } from '@openai/agents'; +// 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 agent.run({ - messages: [ - { - role: 'user', - content: - '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 @@ -100,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. @@ -144,17 +150,28 @@ 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 - `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 @@ -202,6 +219,34 @@ 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) + +### 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: @@ -220,7 +265,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, }); ``` @@ -297,12 +342,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: + +```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: -- The API key is invalid or missing -- The requested data is not available -- Rate limits are exceeded -- Invalid parameters are provided +| `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 @@ -330,6 +388,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 ea72e47..4d0ee76 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.1", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { @@ -52,15 +52,25 @@ }, "homepage": "https://fmp-node-wrapper-docs.vercel.app", "dependencies": { - "@openai/agents": "^0.1.0", - "ai": "^5.0.5", "fmp-node-api": "workspace:*" }, "peerDependencies": { - "zod": "^3.25.76 || ^4.0.0" + "@openai/agents": ">=0.11.0", + "ai": ">=6.0.0", + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "@openai/agents": { + "optional": true + }, + "ai": { + "optional": true + } }, "devDependencies": { - "zod": "^3.25.76", + "@openai/agents": "^0.11.0", + "ai": "^6.0.0", + "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__/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__/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..d324350 --- /dev/null +++ b/packages/tools/src/__tests__/definitions/consistency.test.ts @@ -0,0 +1,38 @@ +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 56 uniquely-named definitions', () => { + expect(names.length).toBe(56); + expect(new Set(names).size).toBe(56); + }); + + 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/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__/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 0aa6224..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(), @@ -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__/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/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/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/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 d686e9e..0137b30 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/quote.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/quote.test.ts @@ -1,7 +1,9 @@ -import { quoteTools } from '@/providers/vercel-ai/quote'; +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/__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/__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/__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 20022cc..6152fbe 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,43 @@ 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({}); + const result = await (tool as any).execute({ symbol: 'AAPL' }); - expect(spy).toHaveBeenCalledWith({ n: 1 }); + expect(spy).toHaveBeenCalledWith({ symbol: 'AAPL' }); + expect(result).toBe('got AAPL'); }); - 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(), - }); - + it('catches a thrown execute and returns a structured error instead of throwing', async () => { 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' }, + 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.'); }, - 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' }); + const parsed = JSON.parse(result); - // Check that the tool has the expected structure for the new API - expect(tool).toBeDefined(); - expect(typeof tool).toBe('object'); + expect(parsed.error).toBe(true); + expect(parsed.type).toBe('auth'); + expect(parsed.message).toContain('API key'); }); }); 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/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/analyst.ts b/packages/tools/src/definitions/analyst.ts new file mode 100644 index 0000000..ed347c9 --- /dev/null +++ b/packages/tools/src/definitions/analyst.ts @@ -0,0 +1,69 @@ +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); + }, + }), + 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/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..4098abb --- /dev/null +++ b/packages/tools/src/definitions/company.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'; + +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)), + }), + 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/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..73a0020 --- /dev/null +++ b/packages/tools/src/definitions/financial.ts @@ -0,0 +1,134 @@ +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 })), + }), + 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/definitions/index.ts b/packages/tools/src/definitions/index.ts new file mode 100644 index 0000000..45e2b43 --- /dev/null +++ b/packages/tools/src/definitions/index.ts @@ -0,0 +1,61 @@ +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 { 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'; + +export * from './types'; + +export { + quoteDefinitions, + companyDefinitions, + financialDefinitions, + calendarDefinitions, + economicDefinitions, + etfDefinitions, + insiderDefinitions, + institutionalDefinitions, + marketDefinitions, + newsDefinitions, + screenerDefinitions, + searchDefinitions, + analystDefinitions, + valuationDefinitions, + technicalDefinitions, + 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, + ...newsDefinitions, + ...screenerDefinitions, + ...searchDefinitions, + ...analystDefinitions, + ...valuationDefinitions, + ...technicalDefinitions, + ...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/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 new file mode 100644 index 0000000..26d6cd0 --- /dev/null +++ b/packages/tools/src/definitions/quote.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'; +import type { APIResponse } from 'fmp-node-api'; + +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)), + }), + 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/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/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/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/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/calendar.ts b/packages/tools/src/providers/openai/calendar.ts deleted file mode 100644 index a6bd7b3..0000000 --- a/packages/tools/src/providers/openai/calendar.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -// 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 JSON.stringify(earningsCalendar.data, null, 2); - }, -}); - -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 JSON.stringify(economicCalendar.data, null, 2); - }, -}); diff --git a/packages/tools/src/providers/openai/company.ts b/packages/tools/src/providers/openai/company.ts deleted file mode 100644 index f37c034..0000000 --- a/packages/tools/src/providers/openai/company.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -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 JSON.stringify(companyProfile.data, null, 2); - }, -}); - -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 JSON.stringify(companySharesFloat.data, null, 2); - }, -}); - -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 JSON.stringify(companyExecutiveCompensation.data, null, 2); - }, -}); diff --git a/packages/tools/src/providers/openai/economic.ts b/packages/tools/src/providers/openai/economic.ts deleted file mode 100644 index 23825b4..0000000 --- a/packages/tools/src/providers/openai/economic.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -// 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 JSON.stringify(treasuryRates.data, null, 2); - }, -}); - -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 JSON.stringify(economicIndicators.data, null, 2); - }, -}); diff --git a/packages/tools/src/providers/openai/etf.ts b/packages/tools/src/providers/openai/etf.ts deleted file mode 100644 index 79539bd..0000000 --- a/packages/tools/src/providers/openai/etf.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -// 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 JSON.stringify(etfHoldings.data, null, 2); - }, -}); - -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 JSON.stringify(etfProfile.data, null, 2); - }, -}); diff --git a/packages/tools/src/providers/openai/financial.ts b/packages/tools/src/providers/openai/financial.ts deleted file mode 100644 index b4973aa..0000000 --- a/packages/tools/src/providers/openai/financial.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -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 JSON.stringify(balanceSheet.data, null, 2); - }, -}); - -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 JSON.stringify(incomeStatement.data, null, 2); - }, -}); - -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 JSON.stringify(cashFlowStatement.data, null, 2); - }, -}); - -export const getKeyMetrics = createOpenAITool({ - name: 'getKeyMetrics', - description: 'Get key metrics for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get key metrics for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.string().default('5').describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const keyMetrics = await fmp.financial.getKeyMetrics({ symbol, period, limit: Number(limit) }); - return JSON.stringify(keyMetrics.data, null, 2); - }, -}); - -export const getFinancialRatios = createOpenAITool({ - name: 'getFinancialRatios', - description: - 'Get financial ratios for a company including profitability, liquidity, and efficiency metrics', - inputSchema: 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 JSON.stringify(financialRatios.data, null, 2); - }, -}); - -export const getEnterpriseValue = createOpenAITool({ - name: 'getEnterpriseValue', - description: 'Get enterprise value for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get enterprise value for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.string().default('5').describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const enterpriseValue = await fmp.financial.getEnterpriseValue({ - symbol, - period, - limit: Number(limit), - }); - return JSON.stringify(enterpriseValue.data, null, 2); - }, -}); - -export const getCashflowGrowth = createOpenAITool({ - name: 'getCashflowGrowth', - description: 'Get cashflow growth for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get cashflow growth for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.string().default('5').describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const cashflowGrowth = await fmp.financial.getCashflowGrowth({ - symbol, - period, - limit: Number(limit), - }); - return JSON.stringify(cashflowGrowth.data, null, 2); - }, -}); - -export const getIncomeGrowth = createOpenAITool({ - name: 'getIncomeGrowth', - description: 'Get income growth for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get income growth for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.string().default('5').describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const incomeGrowth = await fmp.financial.getIncomeGrowth({ - symbol, - period, - limit: Number(limit), - }); - return JSON.stringify(incomeGrowth.data, null, 2); - }, -}); - -export const getBalanceSheetGrowth = createOpenAITool({ - name: 'getBalanceSheetGrowth', - description: 'Get balance sheet growth for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get balance sheet growth for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.string().default('5').describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const balanceSheetGrowth = await fmp.financial.getBalanceSheetGrowth({ - symbol, - period, - limit: Number(limit), - }); - return JSON.stringify(balanceSheetGrowth.data, null, 2); - }, -}); - -export const getFinancialGrowth = createOpenAITool({ - name: 'getFinancialGrowth', - description: 'Get financial growth for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get financial growth for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.string().default('5').describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const financialGrowth = await fmp.financial.getFinancialGrowth({ - symbol, - period, - limit: Number(limit), - }); - return JSON.stringify(financialGrowth.data, null, 2); - }, -}); - -export const getEarningsHistorical = createOpenAITool({ - name: 'getEarningsHistorical', - description: 'Get earnings historical for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get earnings historical for'), - limit: z.string().default('10').describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, limit }) => { - const fmp = getFMPClient(); - const earningsHistorical = await fmp.financial.getEarningsHistorical({ - symbol, - limit: Number(limit), - }); - return JSON.stringify(earningsHistorical.data, null, 2); - }, -}); diff --git a/packages/tools/src/providers/openai/index.ts b/packages/tools/src/providers/openai/index.ts index 191246e..8284b9b 100644 --- a/packages/tools/src/providers/openai/index.ts +++ b/packages/tools/src/providers/openai/index.ts @@ -1,169 +1,115 @@ import type { Tool } from '@openai/agents'; -import { checkOpenAIAgentsVersion } from '@/utils/version-check'; -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 { createOpenAITool } from '@/utils/openai-tool-wrapper'; + +// Re-export client configuration helpers (optional; tools default to FMP_API_KEY). +export { configureFMPClient, resetFMPClient } from '@/client'; + 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, + newsDefinitions, + screenerDefinitions, + searchDefinitions, + analystDefinitions, + valuationDefinitions, + technicalDefinitions, + 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]), +); -// 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, -}; +const pick = (defs: FMPToolDefinition[]): Tool[] => defs.map(def => byName[def.name]); -// 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[]; +// 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; +export const getStockPeers = byName.getStockPeers; +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 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; +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 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 getGradesConsensus = byName.getGradesConsensus; +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; -// 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, -]; +// 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 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); -// Check version compatibility when the module is imported -checkOpenAIAgentsVersion(); +// 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 a56f00b..0000000 --- a/packages/tools/src/providers/openai/insider.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -// 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 JSON.stringify(insiderTrading.data, null, 2); - }, -}); diff --git a/packages/tools/src/providers/openai/institutional.ts b/packages/tools/src/providers/openai/institutional.ts deleted file mode 100644 index 30ea3ea..0000000 --- a/packages/tools/src/providers/openai/institutional.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -// 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 JSON.stringify(institutionalHolders.data, null, 2); - }, -}); diff --git a/packages/tools/src/providers/openai/market.ts b/packages/tools/src/providers/openai/market.ts deleted file mode 100644 index 398ca42..0000000 --- a/packages/tools/src/providers/openai/market.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -// 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 JSON.stringify(marketPerformance.data, null, 2); - }, -}); - -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 JSON.stringify(sectorPerformance.data, null, 2); - }, -}); - -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 JSON.stringify(gainers.data, null, 2); - }, -}); - -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 JSON.stringify(losers.data, null, 2); - }, -}); - -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 JSON.stringify(mostActive.data, null, 2); - }, -}); diff --git a/packages/tools/src/providers/openai/quote.ts b/packages/tools/src/providers/openai/quote.ts deleted file mode 100644 index 504b440..0000000 --- a/packages/tools/src/providers/openai/quote.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -// 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 JSON.stringify(stockQuote.data, null, 2); - }, -}); 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 d47c050..0000000 --- a/packages/tools/src/providers/openai/senate-house.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -// 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 JSON.stringify(senateTrading.data, null, 2); - }, -}); - -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 JSON.stringify(houseTrading.data, null, 2); - }, -}); - -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 JSON.stringify(senateTradingByName.data, null, 2); - }, -}); - -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 JSON.stringify(houseTradingByName.data, null, 2); - }, -}); - -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 JSON.stringify(senateTradingRSSFeed.data, null, 2); - }, -}); - -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 JSON.stringify(houseTradingRSSFeed.data, null, 2); - }, -}); diff --git a/packages/tools/src/providers/openai/stock.ts b/packages/tools/src/providers/openai/stock.ts deleted file mode 100644 index 9257ae3..0000000 --- a/packages/tools/src/providers/openai/stock.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { z } from 'zod'; -import { createOpenAITool } from '@/utils/openai-tool-wrapper'; -import { getFMPClient } from '@/client'; - -// 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 JSON.stringify(marketCap.data, null, 2); - }, -}); - -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 JSON.stringify(stockSplits.data, null, 2); - }, -}); - -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 JSON.stringify(dividendHistory.data, null, 2); - }, -}); - -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 56f0a38..0000000 --- a/packages/tools/src/providers/vercel-ai/calendar.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { z } from 'zod'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; -import { getFMPClient } from '@/client'; - -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 = JSON.stringify(earningsCalendar.data, null, 2); - 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 = JSON.stringify(economicCalendar.data, null, 2); - 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 9b6f5d0..0000000 --- a/packages/tools/src/providers/vercel-ai/company.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { z } from 'zod'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; -import { getFMPClient } from '@/client'; - -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 = JSON.stringify(companyProfile.data, null, 2); - 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 = JSON.stringify(companySharesFloat.data, null, 2); - 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 = JSON.stringify(companyExecutiveCompensation.data, null, 2); - 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 035f8a6..0000000 --- a/packages/tools/src/providers/vercel-ai/economic.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -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 = JSON.stringify(treasuryRates.data, null, 2); - 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 = JSON.stringify(economicIndicators.data, null, 2); - 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 d2dac1b..0000000 --- a/packages/tools/src/providers/vercel-ai/etf.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -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 = JSON.stringify(etfHoldings.data, null, 2); - 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 = JSON.stringify(etfProfile.data, null, 2); - 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 a2e3617..0000000 --- a/packages/tools/src/providers/vercel-ai/financial.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -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 = JSON.stringify(balanceSheet.data, null, 2); - 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 = JSON.stringify(incomeStatement.data, null, 2); - 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 = JSON.stringify(cashFlowStatement.data, null, 2); - return response; - }, - }), - - getKeyMetrics: createTool({ - name: 'getKeyMetrics', - description: 'Get key metrics for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get key metrics for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.number().default(5).describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const keyMetrics = await fmp.financial.getKeyMetrics({ symbol, period, limit }); - const response = JSON.stringify(keyMetrics.data, null, 2); - return response; - }, - }), - - getFinancialRatios: createTool({ - name: 'getFinancialRatios', - description: 'Get financial ratios for a company', - 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 = JSON.stringify(financialRatios.data, null, 2); - return response; - }, - }), - - getEnterpriseValue: createTool({ - name: 'getEnterpriseValue', - description: 'Get enterprise value for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get enterprise value for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.number().default(5).describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const enterpriseValue = await fmp.financial.getEnterpriseValue({ symbol, period, limit }); - const response = JSON.stringify(enterpriseValue.data, null, 2); - return response; - }, - }), - - getCashflowGrowth: createTool({ - name: 'getCashflowGrowth', - description: 'Get cashflow growth for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get cashflow growth for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.number().default(5).describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const cashflowGrowth = await fmp.financial.getCashflowGrowth({ symbol, period, limit }); - const response = JSON.stringify(cashflowGrowth.data, null, 2); - return response; - }, - }), - - getIncomeGrowth: createTool({ - name: 'getIncomeGrowth', - description: 'Get income growth for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get income growth for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.number().default(5).describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const incomeGrowth = await fmp.financial.getIncomeGrowth({ symbol, period, limit }); - const response = JSON.stringify(incomeGrowth.data, null, 2); - return response; - }, - }), - - getBalanceSheetGrowth: createTool({ - name: 'getBalanceSheetGrowth', - description: 'Get balance sheet growth for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get balance sheet growth for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.number().default(5).describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const balanceSheetGrowth = await fmp.financial.getBalanceSheetGrowth({ - symbol, - period, - limit, - }); - const response = JSON.stringify(balanceSheetGrowth.data, null, 2); - return response; - }, - }), - - getFinancialGrowth: createTool({ - name: 'getFinancialGrowth', - description: 'Get financial growth for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get financial growth for'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), - limit: z.number().default(5).describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, period, limit }) => { - const fmp = getFMPClient(); - const financialGrowth = await fmp.financial.getFinancialGrowth({ symbol, period, limit }); - const response = JSON.stringify(financialGrowth.data, null, 2); - return response; - }, - }), - - getEarningsHistorical: createTool({ - name: 'getEarningsHistorical', - description: 'Get earnings historical for a company', - inputSchema: z.object({ - symbol: z.string().describe('The stock symbol to get earnings historical for'), - limit: z.number().default(10).describe('The number of periods to retrieve'), - }), - execute: async ({ symbol, limit }) => { - const fmp = getFMPClient(); - const earningsHistorical = await fmp.financial.getEarningsHistorical({ symbol, limit }); - const response = JSON.stringify(earningsHistorical.data, null, 2); - return response; - }, - }), -}; diff --git a/packages/tools/src/providers/vercel-ai/index.ts b/packages/tools/src/providers/vercel-ai/index.ts index ddb83ec..e46d9b2 100644 --- a/packages/tools/src/providers/vercel-ai/index.ts +++ b/packages/tools/src/providers/vercel-ai/index.ts @@ -1,26 +1,85 @@ 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'; -// Export individual tools for Vercel AI -export const { getCompanyProfile, getCompanySharesFloat, getCompanyExecutiveCompensation } = - companyTools; +// Re-export client configuration helpers (optional; tools default to FMP_API_KEY). +export { configureFMPClient, resetFMPClient } from '@/client'; -export const { getEarningsCalendar, getEconomicCalendar } = calendarTools; +import { + quoteDefinitions, + companyDefinitions, + financialDefinitions, + calendarDefinitions, + economicDefinitions, + etfDefinitions, + insiderDefinitions, + institutionalDefinitions, + marketDefinitions, + newsDefinitions, + screenerDefinitions, + searchDefinitions, + analystDefinitions, + valuationDefinitions, + technicalDefinitions, + senateHouseDefinitions, + stockDefinitions, + type FMPToolDefinition, +} from '@/definitions'; -export const { getTreasuryRates, getEconomicIndicators } = economicTools; +// 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)])); -export const { getETFHoldings, getETFProfile } = etfTools; +// 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 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); + +// Combine all tools into a single ToolSet +export const fmpTools: ToolSet = { + ...quoteTools, + ...companyTools, + ...financialTools, + ...calendarTools, + ...economicTools, + ...etfTools, + ...insiderTools, + ...institutionalTools, + ...marketTools, + ...newsTools, + ...screenerTools, + ...searchTools, + ...analystTools, + ...valuationTools, + ...technicalTools, + ...senateHouseTools, + ...stockTools, +}; +// Individual tools for direct import +export const { getStockQuote, getHistoricalPrice, getIntraday } = quoteTools; +export const { + getCompanyProfile, + getCompanySharesFloat, + getCompanyExecutiveCompensation, + getStockPeers, +} = companyTools; +export const { getEarningsCalendar, getEconomicCalendar } = calendarTools; +export const { getTreasuryRates, getEconomicIndicators } = economicTools; +export const { getETFHoldings, getETFProfile } = etfTools; export const { getBalanceSheet, getIncomeStatement, @@ -33,17 +92,16 @@ export const { getBalanceSheetGrowth, getFinancialGrowth, getEarningsHistorical, + getFinancialScores, + getKeyMetricsTTM, + getFinancialRatiosTTM, + getRevenueProductSegmentation, + getRevenueGeographicSegmentation, } = 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 +110,11 @@ export const { getSenateTradingRSSFeed, getHouseTradingRSSFeed, } = senateHouseTools; - +export const { getStockNews, getStockNewsBySymbol } = newsTools; +export const { screenStocks } = screenerTools; +export const { searchSymbol } = searchTools; +export const { getAnalystEstimates, getPriceTargetConsensus, getStockGrades, getGradesConsensus } = + analystTools; +export const { getDiscountedCashFlow, getCompanyRating } = valuationTools; +export const { getTechnicalIndicator } = technicalTools; 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 9dbd461..0000000 --- a/packages/tools/src/providers/vercel-ai/insider.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from 'zod'; -import { createTool } from '@/utils/aisdk-tool-wrapper'; -import { getFMPClient } from '@/client'; - -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 = JSON.stringify(insiderTrading.data, null, 2); - 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 a4af128..0000000 --- a/packages/tools/src/providers/vercel-ai/institutional.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -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 = JSON.stringify(institutionalHolders.data, null, 2); - 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 7efe869..0000000 --- a/packages/tools/src/providers/vercel-ai/market.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -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 = JSON.stringify(marketPerformance.data, null, 2); - 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 = JSON.stringify(sectorPerformance.data, null, 2); - 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 = JSON.stringify(gainers.data, null, 2); - 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 = JSON.stringify(losers.data, null, 2); - 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 = JSON.stringify(mostActive.data, null, 2); - 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 557094d..0000000 --- a/packages/tools/src/providers/vercel-ai/quote.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -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 = JSON.stringify(stockQuote.data, null, 2); - 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 2d63c9b..0000000 --- a/packages/tools/src/providers/vercel-ai/senate-house.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -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 = JSON.stringify(senateTrading.data, null, 2); - 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 = JSON.stringify(houseTrading.data, null, 2); - 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 = JSON.stringify(senateTradingByName.data, null, 2); - 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 = JSON.stringify(houseTradingByName.data, null, 2); - 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 = JSON.stringify(senateTradingRSSFeed.data, null, 2); - 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 = JSON.stringify(houseTradingRSSFeed.data, null, 2); - 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 1d9f6e5..0000000 --- a/packages/tools/src/providers/vercel-ai/stock.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { z } from 'zod'; -import { getFMPClient } from '@/client'; -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 = JSON.stringify(marketCap.data, null, 2); - 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 = JSON.stringify(stockSplits.data, null, 2); - 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 = JSON.stringify(dividendHistory.data, null, 2); - return response; - }, - }), -}; diff --git a/packages/tools/src/utils/aisdk-tool-wrapper.ts b/packages/tools/src/utils/aisdk-tool-wrapper.ts index 6a9a060..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, ToolSet } from 'ai'; +import { tool } from 'ai'; import { logApiExecutionWithTiming } from './logger'; +import { toToolError } from './format-response'; interface AISDKToolConfig { name: string; @@ -13,9 +14,17 @@ 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)); + 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); + } }, - } as ToolSet); + }); }; 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 ac0fc7f..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; @@ -12,34 +13,23 @@ 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) => { + parameters: inputSchema as z.ZodObject, + execute: async (input: unknown) => { try { - const validatedInput = inputSchema.parse(args); - return await logApiExecutionWithTiming(name, validatedInput, () => execute(validatedInput)); + const args = inputSchema.parse(input) as z.infer; + return await logApiExecutionWithTiming(name, args, () => execute(args)); } 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)}`; + // 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/version-check.ts b/packages/tools/src/utils/version-check.ts deleted file mode 100644 index f99ec31..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.1.0'; - -/** - * Gets the actual installed version of a package - */ -function getInstalledPackageVersion(packageName: string): string | null { - try { - const packagePath = path.dirname(require.resolve(packageName)); - const packageJsonPath = path.join(packagePath, '..', 'package.json'); - - if (fs.existsSync(packageJsonPath)) { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - return packageJson.version; - } - } catch (_error) { - // Package not found or other error - return null; - } - return null; -} - -/** - * Compares two semantic versions - * Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2 - */ -function compareVersions(v1: string, v2: string): number { - const parseVersion = (version: string): number[] => { - return version.split('.').map(num => parseInt(num, 10) || 0); - }; - - const v1Parts = parseVersion(v1); - const v2Parts = parseVersion(v2); - - const maxLength = Math.max(v1Parts.length, v2Parts.length); - - for (let i = 0; i < maxLength; i++) { - const v1Part = v1Parts[i] || 0; - const v2Part = v2Parts[i] || 0; - - if (v1Part < v2Part) return -1; - if (v1Part > v2Part) return 1; - } - - return 0; -} - -/** - * Checks if the installed version is less than the required version - */ -function isVersionLessThan(installedVersion: string, requiredVersion: string): boolean { - return compareVersions(installedVersion, requiredVersion) < 0; -} - -/** - * Checks if the installed version of @openai/agents is compatible - * with this package. Throws an error if incompatible. - */ -export function checkOpenAIAgentsVersion(): void { - 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..e1d5ecb 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,89 @@ # 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 + +- 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 + +- 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 + +- 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 + +- 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/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 diff --git a/packages/types/package.json b/packages/types/package.json index 9a191fb..c94d6e8 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", "description": "Shared TypeScript types for FMP Node Wrapper ecosystem (internal package)", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -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/analyst.ts b/packages/types/src/analyst.ts new file mode 100644 index 0000000..29050e1 --- /dev/null +++ b/packages/types/src/analyst.ts @@ -0,0 +1,76 @@ +// 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 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/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/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; } diff --git a/packages/types/src/company.ts b/packages/types/src/company.ts index 503fe97..0662075 100644 --- a/packages/types/src/company.ts +++ b/packages/types/src/company.ts @@ -1,105 +1,128 @@ -// 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()]); + +// 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; +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; +export type StockPeer = 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..8508618 100644 --- a/packages/types/src/financial.ts +++ b/packages/types/src/financial.ts @@ -1,522 +1,685 @@ // 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; + +// 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; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 6df9abe..1e73cbd 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -46,6 +46,18 @@ export * from './sec'; // Screener types 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/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/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; 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/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; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 304fe9d..b79a906 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,109 +133,94 @@ 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) + 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: ^3.25.76 - version: 3.25.76 + 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: ^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) + 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: ^3.25.76 - version: 3.25.76 + 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: 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 @@ -260,26 +245,26 @@ 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 typescript: specifier: ^5.3.3 version: 5.8.3 + zod: + specifier: ^3.25.76 + version: 3.25.76 packages/tools: dependencies: - '@openai/agents': - specifier: ^0.1.0 - version: 0.1.0(ws@8.18.3)(zod@3.25.76) - ai: - specifier: ^5.0.5 - version: 5.0.5(zod@3.25.76) fmp-node-api: specifier: workspace:* version: link:../api 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 @@ -288,16 +273,19 @@ 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) dotenv: specifier: ^16.5.0 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 @@ -317,10 +305,14 @@ 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: + zod: + specifier: ^3.25.76 + version: 3.25.76 devDependencies: '@types/node': specifier: ^20.11.0 @@ -328,46 +320,45 @@ 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) + 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 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==} @@ -597,9 +588,18 @@ 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==} + '@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==} @@ -762,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} @@ -778,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} @@ -802,6 +804,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'} @@ -810,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'} @@ -831,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] @@ -951,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'} @@ -1033,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'} @@ -1044,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==} @@ -1070,27 +1092,27 @@ 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==} - '@next/env@15.3.0': - resolution: {integrity: sha512-6mDmHX24nWlHOlbwUiAOmMyY7KELimmi+ed8qWcJYjqXeC+G6JzPZ3QosOAfjNwgMIzwhXBiRiCgdh8axTTdTA==} - - '@next/env@15.3.4': - resolution: {integrity: sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==} - - '@next/eslint-plugin-next@15.0.0': - resolution: {integrity: sha512-UG/Gnsq6Sc4wRhO9qk+vc/2v4OfRXH7GEH6/TGlNF5eU/vI9PIO7q+kgd65X2DxJ+qIpHWpzWwlPLmqMi1FE9A==} + '@next/env@16.2.6': + resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} - '@next/eslint-plugin-next@15.3.4': - resolution: {integrity: sha512-lBxYdj7TI8phbJcLSAqDt57nIcobEign5NYIKCiy0hXQhrUbTqLqOaSDi568U6vFg4hJfBdZYsG4iP/uKhCqgg==} + '@next/eslint-plugin-next@16.2.6': + resolution: {integrity: sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==} - '@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' @@ -1100,98 +1122,50 @@ packages: '@mdx-js/react': optional: true - '@next/swc-darwin-arm64@15.3.0': - resolution: {integrity: sha512-PDQcByT0ZfF2q7QR9d+PNj3wlNN4K6Q8JoHMwFyk252gWo4gKt7BF8Y2+KBgDjTFBETXZ/TkBEUY7NIIY7A/Kw==} + '@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-arm64@15.3.4': - resolution: {integrity: sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==} - 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==} + '@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-gnu@15.3.4': - resolution: {integrity: sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==} + '@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-arm64-musl@15.3.0': - resolution: {integrity: sha512-k8GVkdMrh/+J9uIv/GpnHakzgDQhrprJ/FbGQvwWmstaeFG06nnAoZCJV+wO/bb603iKV1BXt4gHG+s2buJqZA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-arm64-musl@15.3.4': - resolution: {integrity: sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==} - 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==} + '@next/swc-win32-arm64-msvc@16.2.6': + resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@15.3.4': - resolution: {integrity: sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==} - 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] @@ -1212,28 +1186,32 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} - '@openai/agents-core@0.1.0': - resolution: {integrity: sha512-SASFdtW71/3Fmjl1gSCIIDTqeDkRQxU7H8SqpMFeB+lbXtnNFTxR5Wt6XnEdj++dRRY8x3EbRnAx8lT7CZGioA==} + '@oclif/core@4.11.4': + resolution: {integrity: sha512-URwiQ5ALx/sJ2iH4vzXEd+H4K6NAI7LRs6Jag3hrgKEpGmaE6alfRC8qjO4GIgb6A3ACaJumqP9twi/M9ywdHQ==} + engines: {node: '>=18.0.0'} + + '@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==} @@ -1364,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==} @@ -1376,74 +1351,71 @@ 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==} - - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} '@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: @@ -1454,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==} @@ -1533,37 +1535,31 @@ 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==} '@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==} @@ -1591,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} @@ -1598,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} @@ -1621,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} @@ -1638,10 +1682,26 @@ 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: + typescript: '*' + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -1740,9 +1800,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==} @@ -1758,25 +1818,30 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agentkeepalive@4.6.0: - resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} - engines: {node: '>= 8.0.0'} + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.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'} @@ -1785,6 +1850,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 +1862,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 +1878,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==} @@ -1812,9 +1893,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==} @@ -1878,13 +1956,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'} @@ -1893,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==} @@ -1931,16 +2002,21 @@ 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} + + 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.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: @@ -1949,6 +2025,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'} @@ -1974,10 +2054,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'} @@ -2002,10 +2078,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'} @@ -2052,18 +2124,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'} @@ -2074,6 +2138,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==} @@ -2102,12 +2182,8 @@ 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==} combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} @@ -2165,13 +2241,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==} @@ -2205,6 +2276,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==} @@ -2251,6 +2331,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'} @@ -2258,9 +2342,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} @@ -2269,17 +2350,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'} @@ -2310,6 +2384,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==} @@ -2320,14 +2397,18 @@ 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: 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==} @@ -2393,19 +2474,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: @@ -2464,11 +2536,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==} @@ -2476,10 +2548,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} @@ -2492,11 +2560,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==} @@ -2512,10 +2578,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'} @@ -2562,13 +2624,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==} @@ -2586,14 +2647,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: @@ -2623,6 +2684,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==} @@ -2640,9 +2704,14 @@ packages: picomatch: optional: true - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + 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@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} @@ -2670,10 +2739,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'} @@ -2681,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: '*' @@ -2698,32 +2763,18 @@ 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==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} 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'} - 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'} @@ -2762,6 +2813,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'} @@ -2805,14 +2860,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'} @@ -2873,19 +2928,33 @@ 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==} 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'} + 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 @@ -2894,15 +2963,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: @@ -2926,6 +2992,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. @@ -2940,6 +3010,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'} @@ -2963,9 +3037,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'} @@ -2974,10 +3045,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'} @@ -3007,6 +3074,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 +3091,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'} @@ -3053,10 +3133,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'} @@ -3112,6 +3188,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==} @@ -3283,13 +3363,12 @@ packages: node-notifier: optional: true - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - jiti@2.4.2: - 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==} @@ -3320,6 +3399,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==} @@ -3364,68 +3449,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: @@ -3435,6 +3526,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 +3554,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==} @@ -3483,6 +3582,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'} @@ -3696,6 +3798,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==} @@ -3714,15 +3824,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==} @@ -3741,6 +3842,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} @@ -3759,34 +3865,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} - 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} + 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 + '@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 @@ -3801,20 +3886,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==} @@ -3825,10 +3896,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'} @@ -3837,10 +3904,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'} @@ -3880,24 +3943,16 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - 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 + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} - 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 @@ -4012,9 +4067,9 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} @@ -4039,30 +4094,6 @@ 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'} - 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'} @@ -4081,25 +4112,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: @@ -4150,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==} @@ -4160,8 +4179,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: @@ -4174,14 +4193,14 @@ 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==} + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: - react: ^19.1.0 + react: ^19.2.6 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4194,21 +4213,14 @@ packages: peerDependencies: react: '>= 0.14.0' - react@19.1.0: - resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + 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'} @@ -4258,6 +4270,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'} @@ -4286,14 +4302,16 @@ 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'} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} rimraf@5.0.10: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} @@ -4329,8 +4347,8 @@ 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==} semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -4341,6 +4359,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'} @@ -4364,8 +4387,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: @@ -4399,9 +4422,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==} @@ -4409,6 +4429,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'} @@ -4447,10 +4479,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'} @@ -4459,10 +4487,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'} @@ -4475,6 +4499,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 +4541,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'} @@ -4569,22 +4605,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'} @@ -4593,8 +4620,68 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + 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-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==} @@ -4614,6 +4701,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'} @@ -4629,9 +4720,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==} @@ -4651,6 +4739,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==} @@ -4681,9 +4775,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,43 +4807,19 @@ 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'} 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: @@ -4756,10 +4833,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'} @@ -4788,6 +4861,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'} @@ -4800,14 +4880,11 @@ 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==} - 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==} @@ -4855,9 +4932,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'} @@ -4875,19 +4949,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==} @@ -4912,10 +4976,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 +4995,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==} @@ -4954,10 +5029,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'} @@ -4975,52 +5046,61 @@ 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-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==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} 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': {} @@ -5050,7 +5130,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 @@ -5206,7 +5286,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 @@ -5360,12 +5440,28 @@ 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 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 @@ -5451,18 +5547,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 @@ -5477,20 +5575,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 @@ -5505,8 +5589,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.1': {} - '@eslint/js@9.32.0': {} '@eslint/object-schema@2.1.6': {} @@ -5516,6 +5598,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': @@ -5523,101 +5610,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.2.4': optional: true - '@img/sharp-libvips-darwin-arm64@1.1.0': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.1.0': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.1.0': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.1.0': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.1.0': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.1.0': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.1.0': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.1.0': + '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true - '@img/sharp-linux-arm64@0.34.2': + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.34.2': + '@img/sharp-linux-arm@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.34.2': + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.34.2': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.2': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.2': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-wasm32@0.34.2': - dependencies: - '@emnapi/runtime': 1.4.3 + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@img/sharp-win32-arm64@0.34.2': + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 optional: true - '@img/sharp-win32-ia32@0.34.2': + '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-x64@0.34.2': + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': optional: true '@isaacs/cliui@8.0.2': @@ -5629,10 +5722,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 @@ -5811,12 +5900,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 @@ -5876,26 +5972,31 @@ 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.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 @@ -5907,71 +6008,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) + '@mdx-js/react': 3.1.0(@types/react@19.2.15)(react@19.2.6) - '@next/swc-darwin-arm64@15.3.0': + '@next/swc-darwin-arm64@16.2.6': optional: true - '@next/swc-darwin-arm64@15.3.4': + '@next/swc-darwin-x64@16.2.6': optional: true - '@next/swc-darwin-x64@15.3.0': + '@next/swc-linux-arm64-gnu@16.2.6': optional: true - '@next/swc-darwin-x64@15.3.4': + '@next/swc-linux-arm64-musl@16.2.6': optional: true - '@next/swc-linux-arm64-gnu@15.3.0': + '@next/swc-linux-x64-gnu@16.2.6': optional: true - '@next/swc-linux-arm64-gnu@15.3.4': + '@next/swc-linux-x64-musl@16.2.6': optional: true - '@next/swc-linux-arm64-musl@15.3.0': + '@next/swc-win32-arm64-msvc@16.2.6': optional: true - '@next/swc-linux-arm64-musl@15.3.4': - optional: true - - '@next/swc-linux-x64-gnu@15.3.0': - optional: true - - '@next/swc-linux-x64-gnu@15.3.4': - optional: true - - '@next/swc-linux-x64-musl@15.3.0': - optional: true - - '@next/swc-linux-x64-musl@15.3.4': - optional: true - - '@next/swc-win32-arm64-msvc@15.3.0': - optional: true - - '@next/swc-win32-arm64-msvc@15.3.4': - optional: true - - '@next/swc-win32-x64-msvc@15.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': @@ -5988,48 +6059,73 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@openai/agents-core@0.1.0(ws@8.18.3)(zod@3.25.76)': + '@oclif/core@4.11.4': dependencies: - debug: 4.4.1 - openai: 5.19.1(ws@8.18.3)(zod@3.25.76) + 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.11.5(ws@8.18.3)(zod@4.4.3)': + dependencies: + 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 @@ -6040,18 +6136,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 @@ -6115,8 +6211,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@rushstack/eslint-patch@1.12.0': {} - '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -6127,85 +6221,98 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@standard-schema/spec@1.0.0': {} - - '@swc/counter@0.1.3': {} + '@standard-schema/spec@1.1.0': {} '@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: @@ -6282,17 +6389,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 @@ -6301,23 +6399,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': {} @@ -6327,7 +6425,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.2 + '@types/node': 25.9.1 '@types/yargs-parser@21.0.3': {} @@ -6335,15 +6433,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 @@ -6352,43 +6450,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 @@ -6397,7 +6494,16 @@ 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 + + '@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 @@ -6407,36 +6513,47 @@ snapshots: '@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) @@ -6453,24 +6570,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: + '@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.7.0))(typescript@5.8.3)': dependencies: - '@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)) '@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 + eslint: 9.32.0(jiti@2.7.0) 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.60.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)) - '@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-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 @@ -6480,6 +6612,18 @@ 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) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-android-arm-eabi@1.9.2': @@ -6541,9 +6685,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: @@ -6557,25 +6699,24 @@ snapshots: acorn@8.15.0: {} - agentkeepalive@4.6.0: + agent-base@6.0.2: dependencies: - humanize-ms: 1.2.1 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - 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: @@ -6584,16 +6725,30 @@ 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: 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 +6757,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: @@ -6609,8 +6768,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 @@ -6698,29 +6855,21 @@ 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 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: {} @@ -6783,22 +6932,24 @@ snapshots: balanced-match@1.0.2: {} + 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.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 @@ -6813,6 +6964,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 @@ -6839,10 +6994,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 @@ -6867,8 +7018,6 @@ snapshots: callsites@3.1.0: {} - camelcase-css@2.0.1: {} - camelcase@5.3.1: {} camelcase@6.3.0: {} @@ -6900,24 +7049,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: {} @@ -6926,6 +7061,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: @@ -6948,17 +7098,7 @@ 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: dependencies: @@ -7049,9 +7189,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: {} @@ -7081,6 +7219,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 @@ -7114,30 +7258,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: {} @@ -7161,6 +7300,8 @@ snapshots: emittery@0.13.1: {} + emoji-regex@10.4.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -7168,16 +7309,18 @@ 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: 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 @@ -7336,42 +7479,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(@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)) + 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 @@ -7384,88 +7507,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): + 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.1 - eslint: 8.57.1 + 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.14 + tinyglobby: 0.2.16 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) + 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-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-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.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)) - 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): - 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 + '@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-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) + 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: - - 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-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 @@ -7474,9 +7542,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(@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 @@ -7488,13 +7556,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): + 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 @@ -7504,7 +7572,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 8.57.1 + eslint: 9.32.0(jiti@2.7.0) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -7513,56 +7581,18 @@ snapshots: 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-react-hooks@7.1.1(eslint@9.32.0(jiti@2.7.0)): 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: 9.32.0(jiti@2.4.2) - 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-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)): - 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 @@ -7570,7 +7600,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 @@ -7584,11 +7614,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 @@ -7598,52 +7623,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 @@ -7679,7 +7663,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.4.2 + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -7689,12 +7673,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: @@ -7745,13 +7723,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: @@ -7776,33 +7754,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 @@ -7846,6 +7826,9 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.2: + optional: true + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -7862,9 +7845,9 @@ snapshots: optionalDependencies: picomatch: 4.0.2 - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 file-entry-cache@8.0.0: dependencies: @@ -7880,7 +7863,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 @@ -7906,12 +7889,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 @@ -7919,7 +7896,7 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.9: {} + follow-redirects@1.16.0: {} for-each@0.3.5: dependencies: @@ -7930,17 +7907,7 @@ 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 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - form-data@4.0.4: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -7950,16 +7917,9 @@ snapshots: 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 - fraction.js@4.3.7: {} - fresh@2.0.0: optional: true @@ -7997,6 +7957,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 @@ -8057,12 +8019,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 @@ -8160,34 +8120,46 @@ 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: {} + 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 + 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: {} - 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 @@ -8208,6 +8180,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -8223,6 +8197,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 @@ -8248,9 +8225,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 @@ -8263,10 +8237,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 @@ -8274,7 +8244,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.7.2 + semver: 7.8.1 is-callable@1.2.7: {} @@ -8297,6 +8267,8 @@ snapshots: is-decimal@2.0.1: {} + is-docker@2.2.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -8305,6 +8277,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: @@ -8333,8 +8313,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-plain-obj@4.1.0: {} is-promise@4.0.0: @@ -8387,6 +8365,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: {} @@ -8421,7 +8403,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: @@ -8854,9 +8836,10 @@ snapshots: - supports-color - ts-node - jiti@1.21.7: {} + jiti@2.7.0: {} - jiti@2.4.2: {} + jose@6.2.3: + optional: true joycon@3.1.1: {} @@ -8879,6 +8862,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: {} @@ -8919,55 +8908,68 @@ 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: {} 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 +8988,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: @@ -9003,14 +9013,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 @@ -9445,7 +9459,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 @@ -9485,6 +9499,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 @@ -9501,12 +9521,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 @@ -9526,6 +9540,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@3.3.12: {} + napi-postinstall@0.2.5: {} natural-compare@1.4.0: {} @@ -9533,85 +9549,48 @@ 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.4(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + 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.4 - '@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.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) + 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.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 - 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: {} 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: {} @@ -9665,25 +9644,14 @@ snapshots: dependencies: mimic-fn: 2.1.0 - openai@4.104.0(ws@8.18.3)(zod@3.25.76): + onetime@7.0.0: 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 + mimic-function: 5.0.1 - 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: @@ -9793,7 +9761,7 @@ snapshots: picomatch@4.0.2: {} - pify@2.3.0: {} + picomatch@4.0.4: {} pify@4.0.1: {} @@ -9814,55 +9782,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 @@ -9878,11 +9815,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: {} @@ -9911,13 +9848,13 @@ snapshots: ipaddr.js: 1.9.1 optional: true - proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} punycode@2.3.1: {} pure-rand@6.1.0: {} - qs@6.14.0: + qs@6.15.2: dependencies: side-channel: 1.1.0 optional: true @@ -9929,38 +9866,34 @@ 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 - react-dom@19.1.0(react@19.1.0): + react-dom@19.2.6(react@19.2.6): dependencies: - react: 19.1.0 - scheduler: 0.26.0 + react: 19.2.6 + scheduler: 0.27.0 react-is@16.13.1: {} 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: {} - - read-cache@1.0.0: - dependencies: - pify: 2.3.0 + react@19.2.6: {} read-yaml-file@1.1.0: dependencies: @@ -9969,10 +9902,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: @@ -10082,6 +10011,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 @@ -10106,11 +10038,14 @@ 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: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 + rfdc@1.4.1: {} rimraf@5.0.10: dependencies: @@ -10144,7 +10079,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 @@ -10181,20 +10116,22 @@ snapshots: safer-buffer@2.1.2: {} - scheduler@0.26.0: {} + scheduler@0.27.0: {} semver@6.3.1: {} semver@7.7.2: {} + semver@7.8.1: {} + 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 @@ -10239,33 +10176,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: @@ -10306,15 +10246,22 @@ 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: {} + 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: @@ -10347,9 +10294,6 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 - statuses@2.0.1: - optional: true - statuses@2.0.2: optional: true @@ -10358,8 +10302,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 @@ -10377,6 +10319,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 +10393,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: {} @@ -10456,10 +10413,10 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.6(@babel/core@7.27.7)(react@19.1.0): + styled-jsx@5.1.6(@babel/core@7.27.7)(react@19.2.6): dependencies: client-only: 0.0.1 - react: 19.1.0 + react: 19.2.6 optionalDependencies: '@babel/core': 7.27.7 @@ -10483,53 +10440,17 @@ 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: {} - 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: {} - - tapable@2.2.2: {} + tailwindcss@4.3.0: {} - 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: {} @@ -10539,7 +10460,98 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - text-table@0.2.0: {} + 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-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: @@ -10558,6 +10570,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 @@ -10571,8 +10588,6 @@ snapshots: toidentifier@1.0.1: optional: true - tr46@0.0.3: {} - tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -10587,6 +10602,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): @@ -10650,6 +10669,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,9 +10692,11 @@ 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): + 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 @@ -10670,7 +10707,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 @@ -10679,7 +10716,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 @@ -10687,6 +10724,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 @@ -10694,32 +10736,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: {} @@ -10729,8 +10753,6 @@ snapshots: type-detect@4.0.8: {} - type-fest@0.20.2: {} - type-fest@0.21.3: {} type-fest@4.41.0: {} @@ -10775,6 +10797,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: {} @@ -10786,11 +10819,9 @@ 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: {} + undici-types@7.24.6: {} unified@11.0.5: dependencies: @@ -10868,11 +10899,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 - - util-deprecate@1.0.2: {} + react: 19.2.6 v8-to-istanbul@9.3.0: dependencies: @@ -10897,17 +10926,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 @@ -10959,8 +10979,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 +10999,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: @@ -10988,9 +11020,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: {} @@ -11006,10 +11037,17 @@ 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-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 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/**"],