From df0df84149b723b8c33b003a01d218dc1379e255 Mon Sep 17 00:00:00 2001 From: ssdeanx Date: Tue, 14 Apr 2026 09:56:31 -0400 Subject: [PATCH] feat: add tools for Coinbase Exchange and Stooq stock market data - Implemented `coinbase-exchange-crypto.tool.ts` to fetch public market data from Coinbase Exchange, including ticker, stats, candles, order book, trades, and products. - Created `market-data.helpers.ts` to provide utility functions for building product IDs and normalizing data from Binance and Coinbase. - Developed `stooq-stock-market-data.tool.ts` to retrieve stock market data from Stooq, supporting both latest quotes and historical data. - Added `yahoo-finance-stock.tool.ts` to fetch stock data from Yahoo Finance, including quotes and historical chart data. - Introduced tests for market data helpers in `market-data.helpers.test.ts` to ensure correct functionality of utility functions. - Added tests for SerpAPI tools in `serpapi-tools.test.ts` to validate Google Scholar paper count and Google Trends date range mapping. - new stagebrowser allows agent to use browser in real time but its still work in progress. --- memory-bank/activeContext.md | 106 +++ memory-bank/progress.md | 90 +++ next.config.ts | 4 +- package-lock.json | 640 ++++++++++++------ package.json | 36 +- src/mastra/agents/researchAgent.ts | 28 +- src/mastra/browsers.ts | 38 ++ src/mastra/config/libsql.ts | 1 - src/mastra/index.ts | 15 + .../public/workspace/workspace/Agent tools | 72 ++ .../Investment Recommendations 2026-04-14 12 | 0 .../tools/binance-crypto-market.tool.ts | 430 ++++++++++++ .../tools/coinbase-exchange-crypto.tool.ts | 338 +++++++++ src/mastra/tools/index.ts | 4 + src/mastra/tools/market-data.helpers.ts | 346 ++++++++++ .../tools/serpapi-academic-local.tool.ts | 40 +- src/mastra/tools/serpapi-news-trends.tool.ts | 49 +- .../tools/stooq-stock-market-data.tool.ts | 277 ++++++++ .../tools/tests/market-data.helpers.test.ts | 80 +++ src/mastra/tools/tests/serpapi-tools.test.ts | 41 ++ src/mastra/tools/yahoo-finance-stock.tool.ts | 335 +++++++++ src/mastra/workspaces.ts | 10 +- tests/test-results/test-results.json | 2 +- 23 files changed, 2724 insertions(+), 258 deletions(-) create mode 100644 src/mastra/browsers.ts create mode 100644 src/mastra/public/workspace/workspace/Agent tools create mode 100644 src/mastra/public/workspace/workspace/Investment Recommendations 2026-04-14 12 create mode 100644 src/mastra/tools/binance-crypto-market.tool.ts create mode 100644 src/mastra/tools/coinbase-exchange-crypto.tool.ts create mode 100644 src/mastra/tools/market-data.helpers.ts create mode 100644 src/mastra/tools/stooq-stock-market-data.tool.ts create mode 100644 src/mastra/tools/tests/market-data.helpers.test.ts create mode 100644 src/mastra/tools/tests/serpapi-tools.test.ts create mode 100644 src/mastra/tools/yahoo-finance-stock.tool.ts diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index 60bf2b75..4d2c622c 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,3 +1,109 @@ +# Active Context Update (2026-04-14 - strict typing and inferred tool cleanup) + +- `BinanceAvgPrice` is now used in the Binance spot tool via the `BinanceSpotAvgPriceData` type. +- Added `InferUITool` exports for all four market-data tools: + - `BinanceSpotMarketDataUITool` + - `CoinbaseExchangeMarketDataUITool` + - `StooqStockQuotesUITool` + - `YahooFinanceStockQuotesUITool` +- The market-data helper layer and all four tools remain free of broad `any`/`unknown` usage in their own code. +- Validation remains green after the cleanup. + +# Active Context Update (2026-04-14 - strict typing pass) + +- Tightened the market-data tool layer to remove broad `any`/`unknown` usage from the shared helper and all four source-specific tools. +- Final tools now use explicit typed payload models for: + - Binance spot crypto + - Coinbase Exchange crypto + - Stooq stock quotes + - Yahoo Finance stock quotes +- Runtime validation and tests remain green after the typing pass. + +# Active Context Update (2026-04-14 - hook order corrected) + +- Corrected the market-data tools to follow the repo’s hook ordering pattern from `fetch.tool.ts`. +- `onOutput` now appears at the end of each tool definition for: + - `binanceSpotMarketDataTool` + - `coinbaseExchangeMarketDataTool` + - `stooqStockQuotesTool` + - `yahooFinanceStockQuotesTool` +- Also tightened `onOutput` counts to use the returned payload shape rather than assuming array roots. +- Validation completed successfully on the four updated tool files. + +# Active Context Update (2026-04-14 - final optimization pass) + +- Wired the new production-grade market-data tools into `researchAgent` so the agent can use them directly: + - `binanceSpotMarketDataTool` + - `coinbaseExchangeMarketDataTool` + - `stooqStockQuotesTool` + - `yahooFinanceStockQuotesTool` +- Cleaned the Coinbase Exchange tool by removing an unnecessary `limit` parameter from the public trades request. +- `researchAgent` tool guidance now points to the free/public market-data tools for crypto and stock research when paid feeds are not needed. +- Validation completed successfully on the updated `researchAgent` and Coinbase tool files. + +# Active Context Update (2026-04-14 - production-grade market-data tool pass) + +- The market-data implementation is now source-specific and production-oriented. +- Final exported tools: + - `binanceSpotMarketDataTool` + - `coinbaseExchangeMarketDataTool` + - `stooqStockQuotesTool` + - `yahooFinanceStockQuotesTool` +- Shared helpers live in `src/mastra/tools/market-data.helpers.ts` and handle symbol normalization plus Binance/Coinbase/Stooq/Yahoo candle conversion. +- Binance now exposes additional public market-data options (`quote`, `stats24hr`, `candles`, `uiKlines`, `orderbook`, `trades`, `aggTrades`, `avgPrice`, `exchangeInfo`) with optional time filters. +- Coinbase Exchange now exposes public ticker/stats/candles/orderbook/trades/products options with candle time filters. +- Yahoo Finance now exposes quote/history plus `includePrePost` and `events` controls. +- Every tool includes `onOutput` hooks and the standard input/progress/span/error pattern. +- Validation completed successfully on the renamed tools, the shared helpers, and the helper tests. + +# Active Context Update (2026-04-14 - modular market-data refactor) + +- Replaced the bloated `free-market-data.tool.ts` with four source-specific tools and a shared helper module: + - `binance-crypto-market.tool.ts` + - `coinbase-exchange-crypto.tool.ts` + - `stooq-stock-market-data.tool.ts` + - `yahoo-finance-stock.tool.ts` + - shared helpers in `market-data.helpers.ts` +- All four tool files now include `onOutput` hooks plus the existing input/progress patterns used elsewhere in the repo. +- Crypto sources are now truly free/no-key for the main path and fallback path: + - Binance public market-data API + - Coinbase Exchange public market-data API +- Stock sources are also no-key: + - Stooq + - Yahoo Finance +- Validation completed successfully: + - targeted VS Code error checks on all new modular market-data files, shared helpers, tests, and `src/mastra/tools/index.ts` + - `vitest run src/mastra/tools/tests/market-data.helpers.test.ts` + +# Active Context Update (2026-04-14 - free market-data tools added) + +- Added `src/mastra/tools/free-market-data.tool.ts` with two new tools: + - `freeCryptoMarketDataTool`: Binance public market data as the primary crypto source, plus CoinCap as a second crypto source that requires a free API key. + - `freeStockMarketDataTool`: Stooq and Yahoo Finance as free stock sources that do not require an API key. +- Added helper exports for symbol normalization and CSV/chart normalization so the tool behavior is testable. +- Exported the new tool module from `src/mastra/tools/index.ts`. +- Validation completed successfully: + - targeted VS Code error check on `src/mastra/tools/free-market-data.tool.ts` + - targeted VS Code error check on `src/mastra/tools/index.ts` + - targeted VS Code error check on `src/mastra/tools/tests/free-market-data.test.ts` + - `vitest run src/mastra/tools/tests/free-market-data.test.ts` + +# Active Context Update (2026-04-14 - crypto API research shortlist) + +- Binance offers market-data-only public endpoints with no authentication required; useful endpoints include `/api/v3/ticker/price`, `/api/v3/ticker/24hr`, `/api/v3/klines`, `/api/v3/depth`, and `/api/v3/exchangeInfo`. +- CoinGecko’s current docs support a free/demo path with a free API key and strong coverage for prices, markets, historical charts, and trending coins. +- CoinCap API 3.0 is another viable crypto data source for real-time pricing/market cap/exchange data across 1,000+ assets. +- CryptoCompare remains a useful fallback for a free tier with historical market data and news, but it does require an API key. +- Existing repo coverage already includes `alphaVantageCryptoTool` and Polygon crypto tools, so the next addition should focus on free/public providers rather than duplicating paid-market feeds. + +# Active Context Update (2026-04-14 - SerpAPI Scholar/trends repair) + +- Fixed the Google Scholar tool in `src/mastra/tools/serpapi-academic-local.tool.ts` so its `onOutput`/input hooks no longer assume `messages` or `output.papers` are always defined. +- Normalized the Scholar progress events to match the rest of the SerpAPI tools with explicit `status`, `stage`, `id`, and a completion event on success. +- Fixed Google Trends in `src/mastra/tools/serpapi-news-trends.tool.ts` by translating the internal hyphenated time-range enum into the SerpAPI date format (`today 12-m`, `now 7-d`, etc.). +- Added focused regression tests in `src/mastra/tools/tests/serpapi-tools.test.ts` for the trends date mapper and Scholar paper-count helper. +- Validation: targeted ESLint passed for the two edited tool files plus the new test file, and the new Vitest suite passed. + # Active Context Update (2026-04-13 - research synthesis workflow repair) - Repaired `src/mastra/workflows/research-synthesis-workflow.ts` after a broken merge introduced duplicate `stream` declarations and malformed structured-output syntax in the topic research and synthesis steps. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index e290808c..64f5deea 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -1,3 +1,93 @@ +# Progress Update (2026-04-14 - strict typing and inferred tool cleanup) + +- Used `BinanceAvgPrice` in the Binance tool instead of leaving it as an unused helper import. +- Added `InferUITool` exports for each market-data tool to match other Mastra tools in the repo. +- Validation: + - ✅ targeted VS Code error checks on all market-data files + - ✅ `vitest run src/mastra/tools/tests/market-data.helpers.test.ts` + +# Progress Update (2026-04-14 - strict typing pass) + +- Removed the remaining broad `any`/`unknown` usage from the market-data helpers and the four source-specific market-data tools. +- Kept the tools production-grade with explicit payload models and typed output schemas. +- Validation: + - ✅ targeted VS Code error checks on all market-data helper/tool files + - ✅ `vitest run src/mastra/tools/tests/market-data.helpers.test.ts` + +# Progress Update (2026-04-14 - hook order corrected) + +- Moved each market-data tool’s `onOutput` hook to the end of the tool config to match the `fetch.tool.ts` pattern. +- Improved the completion logs to count payload data more safely. +- Validation: + - ✅ targeted VS Code error checks on the four updated market-data tool files + +# Progress Update (2026-04-14 - final optimization pass) + +- Added the new free/public market-data tools to `researchAgent` so they are actually available to the primary research workflow. +- Removed the unnecessary Coinbase trades query parameter to keep the public request surface lean. +- Validation: + - ✅ targeted VS Code error check on `src/mastra/agents/researchAgent.ts` + - ✅ targeted VS Code error check on `src/mastra/tools/coinbase-exchange-crypto.tool.ts` + +# Progress Update (2026-04-14 - production-grade market-data tool pass) + +- Renamed the modular market-data exports to clearer production-facing names. +- Expanded source-specific options so the tools are more feature-complete: + - Binance spot market data + - Coinbase Exchange market data + - Stooq stock quotes/history + - Yahoo Finance quotes/history +- Fixed the final Yahoo Finance compile issue from the rename pass. +- Validation: + - ✅ targeted VS Code error checks on all market-data tool files and shared helpers + - ✅ targeted VS Code error check on `src/mastra/tools/yahoo-finance-stock.tool.ts` after the final fix + +# Progress Update (2026-04-14 - modular market-data refactor) + +- Refactored market-data into dedicated tools per source instead of one bloated generic tool. +- Added `onOutput` hooks to every new market-data tool to match the logging pattern used by the rest of the repo. +- Final tool set now uses only free/public sources for the no-key paths: + - Binance crypto + - Coinbase Exchange crypto + - Stooq stock + - Yahoo Finance stock +- Validation: + - ✅ VS Code error checks on all modular tool files, helper file, tests, and `src/mastra/tools/index.ts` + - ✅ `vitest run src/mastra/tools/tests/market-data.helpers.test.ts` + +# Progress Update (2026-04-14 - free market-data tools added) + +- Implemented `freeCryptoMarketDataTool` in `src/mastra/tools/free-market-data.tool.ts`. + - Primary source: Binance public market-data-only endpoints. + - Secondary source: CoinCap (requires `COINCAP_API_KEY`). +- Implemented `freeStockMarketDataTool` in the same file. + - Sources: Stooq and Yahoo Finance, both without API key requirements. +- Added tests in `src/mastra/tools/tests/free-market-data.test.ts` covering symbol normalization and CSV/chart parsing. +- Validation: + - ✅ VS Code error check on `src/mastra/tools/free-market-data.tool.ts` + - ✅ VS Code error check on `src/mastra/tools/index.ts` + - ✅ VS Code error check on `src/mastra/tools/tests/free-market-data.test.ts` + - ✅ `vitest run src/mastra/tools/tests/free-market-data.test.ts` + +# Progress Update (2026-04-14 - crypto API research shortlist) + +- Researched free crypto data providers for the requested crypto-tool set. +- Best-fit shortlist for implementation: + - Binance public market-data-only endpoints (no auth) + - CoinGecko free/demo API (free key) + - CoinCap API 3.0 (real-time public market data) + - CryptoCompare free tier (API key required) +- Existing repo crypto coverage already includes Alpha Vantage and Polygon tools, so the next feature should emphasize free/public feeds and normalization helpers. + +# Progress Update (2026-04-14 - SerpAPI Scholar/trends repair) + +- Patched `src/mastra/tools/serpapi-academic-local.tool.ts` to guard against undefined `messages`/`output` in the Scholar callbacks and to emit consistent start/done progress events. +- Patched `src/mastra/tools/serpapi-news-trends.tool.ts` so Google Trends sends SerpAPI-compatible date values instead of the internal hyphenated enum strings. +- Added `src/mastra/tools/tests/serpapi-tools.test.ts` covering the trends date mapper and Scholar paper-count helper. +- Validation: + - ✅ targeted ESLint on the edited tool files and new test file + - ✅ `vitest run src/mastra/tools/tests/serpapi-tools.test.ts` + # Progress Update (2026-04-13 - research synthesis workflow repair) - Fixed the malformed merge in `src/mastra/workflows/research-synthesis-workflow.ts` that caused the Mastra CLI transform failure. diff --git a/next.config.ts b/next.config.ts index aeae19fb..0bca495e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -37,9 +37,9 @@ const nextConfig: NextConfig = { 'sharp', 'excalidraw-to-svg', 'svgjson', - 'streamjson', + 'stream-json', 'xlsx', - 'paraparse' + 'papaparse' ], allowedDevOrigins: ['http://localhost:4111', 'http://127.0.0.1:4111'], typedRoutes: true, diff --git a/package-lock.json b/package-lock.json index 6f7e8005..f9e9bfd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,41 +1,44 @@ { "name": "agentstack", - "version": "1.0.41", + "version": "1.0.42", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agentstack", - "version": "1.0.41", + "version": "1.0.42", "license": "ISC", "dependencies": { - "@ai-sdk/google": "^3.0.62", - "@ai-sdk/google-vertex": "^4.0.108", + "@ai-sdk/google": "^3.0.63", + "@ai-sdk/google-vertex": "^4.0.109", "@ai-sdk/openai": "^3.0.52", "@ai-sdk/openai-compatible": "^2.0.41", "@ai-sdk/provider-utils": "^4.0.23", - "@ai-sdk/react": "^3.0.160", + "@ai-sdk/react": "^3.0.161", "@auth/agent": "^0.4.6", "@auth/agent-cli": "^0.4.5", - "@better-auth/api-key": "^1.6.2", - "@chat-adapter/discord": "^4.25.0", - "@chat-adapter/github": "^4.25.0", - "@chat-adapter/slack": "^4.25.0", + "@better-auth/api-key": "^1.6.3", + "@chat-adapter/discord": "^4.26.0", + "@chat-adapter/github": "^4.26.0", + "@chat-adapter/slack": "^4.26.0", "@dotenvx/dotenvx": "^1.61.0", "@emotion/react": "^11.14.0", "@github/copilot-sdk": "^0.2.2", "@gsap/react": "^2.1.2", "@libsql/kysely-libsql": "^0.4.1", + "@mastra/agent-browser": "^0.1.0", "@mastra/agentfs": "^0.1.0", "@mastra/ai-sdk": "^1.3.3", "@mastra/auth-better-auth": "^1.0.2", "@mastra/auth-supabase": "^1.0.0", "@mastra/client-js": "^1.13.3", + "@mastra/cloudflare": "^1.3.1", "@mastra/cloudflare-d1": "^1.0.4", "@mastra/convex": "^1.0.7", "@mastra/core": "^1.24.1", "@mastra/daytona": "^0.2.1", "@mastra/deployer": "^1.24.1", + "@mastra/editor": "^0.7.15", "@mastra/evals": "^1.2.1", "@mastra/gcs": "^0.2.0", "@mastra/lance": "^1.0.4", @@ -52,6 +55,7 @@ "@mastra/react": "^0.2.25", "@mastra/s3": "^0.3.0", "@mastra/schema-compat": "^1.2.7", + "@mastra/stagehand": "^0.1.0", "@mastra/upstash": "^1.0.4", "@mastra/vectorize": "^1.0.2", "@mastra/voice-google": "^0.12.0", @@ -104,7 +108,7 @@ "arraystat": "^1.7.81", "axios": "^1.15.0", "axios-retry": "^4.5.0", - "better-auth": "^1.6.2", + "better-auth": "^1.6.3", "bottleneck": "^2.19.5", "chalk": "^5.6.2", "chart.js": "^4.5.1", @@ -150,7 +154,7 @@ "marked": "^18.0.0", "mastracode": "^0.13.0", "mathjs": "^15.2.0", - "media-chrome": "^4.18.3", + "media-chrome": "^4.19.0", "module": "^2.0.0", "motion": "^12.38.0", "nanoid": "^5.1.7", @@ -235,8 +239,8 @@ "@types/strip-comments": "^2.0.4", "@types/three": "^0.183.1", "@types/unist": "^3.0.3", - "@typescript-eslint/eslint-plugin": "^8.58.1", - "@typescript-eslint/parser": "^8.58.1", + "@typescript-eslint/eslint-plugin": "^8.58.2", + "@typescript-eslint/parser": "^8.58.2", "@vitest/coverage-v8": "^4.1.4", "cross-env": "^10.1.0", "eslint": "^10.2.0", @@ -250,7 +254,7 @@ "tailwindcss": "^4.2.2", "tw-animate-css": "^1.4.0", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.1", + "typescript-eslint": "^8.58.2", "typescript-language-server": "^5.1.3", "vitest": "^4.1.4" }, @@ -573,9 +577,9 @@ } }, "node_modules/@ai-sdk/google": { - "version": "3.0.62", - "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.62.tgz", - "integrity": "sha512-cC9HAjR5WZxjqGyEJrJqFTlVqyPE9UOFmmGdf5dINaimgfPmzqXYN1qTYEJ+1knbyTVsNMub0KAF5SOqqtO8IQ==", + "version": "3.0.63", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.63.tgz", + "integrity": "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.8", @@ -589,13 +593,13 @@ } }, "node_modules/@ai-sdk/google-vertex": { - "version": "4.0.108", - "resolved": "https://registry.npmjs.org/@ai-sdk/google-vertex/-/google-vertex-4.0.108.tgz", - "integrity": "sha512-4aJazfFrMHffi/S1UriJeK5lkXFrCV+b8nGaTbe3lNMptjdtW1OXzaez2KW0B0ILe3Eug8RcEyorg7u9Ar7xjA==", + "version": "4.0.109", + "resolved": "https://registry.npmjs.org/@ai-sdk/google-vertex/-/google-vertex-4.0.109.tgz", + "integrity": "sha512-QzQ+DgOoSYlkU4mK0H+iaCaW1bl5zOimH9X2E2oylcVyUtAdCuduQ959Uw1ygW3l09J2K/ceEDtK8OUPHyOA7g==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/anthropic": "3.0.69", - "@ai-sdk/google": "3.0.62", + "@ai-sdk/google": "3.0.63", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", @@ -900,13 +904,13 @@ } }, "node_modules/@ai-sdk/react": { - "version": "3.0.160", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.160.tgz", - "integrity": "sha512-ZDD42ggZYyBJjiX3PAl03t58uMJsIgreHfRhbdQR0hyuGlxcg1nXS5OvPRQUejNyGz/cM24/uGzt3Mn5ncdoOQ==", + "version": "3.0.161", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.161.tgz", + "integrity": "sha512-lFIZm7OggwNZD08Yz8ip0EPgmEn/lKZOB2MrKjzDpq6BT8gUX17TfaaUi9IICN8nOeLOZQqJKrWNnTXjcvElBw==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider-utils": "4.0.23", - "ai": "6.0.158", + "ai": "6.0.159", "swr": "^2.2.5", "throttleit": "2.1.0" }, @@ -1339,6 +1343,15 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/@arcadeai/arcadejs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@arcadeai/arcadejs/-/arcadejs-2.3.0.tgz", + "integrity": "sha512-MZ2/nT/uCJDH5Fk3xT9pz7TUcMFZPv5cs2iiomAOb2J1eimc3LbbKy7doPjtfOlLZHVeMiTzPNqsvE+8barK8w==", + "license": "MIT", + "bin": { + "arcadeai-arcadejs": "bin/cli" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.10", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.10.tgz", @@ -3062,23 +3075,23 @@ } }, "node_modules/@better-auth/api-key": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@better-auth/api-key/-/api-key-1.6.2.tgz", - "integrity": "sha512-eEGfiPKS4qnd8MoV+GtPM9PP74cNfvQow3/0jARRNu8piI8R4NjqpojaSJVjZa5VrsRDKyYCpXAKz8u+7eaHog==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@better-auth/api-key/-/api-key-1.6.3.tgz", + "integrity": "sha512-QucqL4i1fDAqajnS2G+X1fOBF/pB7DQlZZNnKpUYhNSzM583+0Gjovq8cwSZl6zt/5nR7c1DmN+PyfEdJBc0YA==", "license": "MIT", "dependencies": { "zod": "^4.3.6" }, "peerDependencies": { - "@better-auth/core": "^1.6.2", + "@better-auth/core": "^1.6.3", "@better-auth/utils": "0.4.0", - "better-auth": "^1.6.2" + "better-auth": "^1.6.3" } }, "node_modules/@better-auth/core": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.2.tgz", - "integrity": "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.3.tgz", + "integrity": "sha512-HefGR2SNfAi2RhT6XvSYViH4a0xoCGGL10bSDiv6sQGrmY6ulEQEV1X4nebTHeG0P6jdBmXAoEW3k37nhpk99w==", "license": "MIT", "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", @@ -3101,6 +3114,95 @@ } } }, + "node_modules/@better-auth/drizzle-adapter": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.3.tgz", + "integrity": "sha512-P5erUYKoctOnOf+hd3umkOhOqJA+WuDByzmgnxZMBQLhgmusn5cgW10449B9aZu8HxIcU/tUQo/8ucwXHNzZ0A==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.3", + "@better-auth/utils": "0.4.0", + "drizzle-orm": ">=0.41.0" + }, + "peerDependenciesMeta": { + "drizzle-orm": { + "optional": true + } + } + }, + "node_modules/@better-auth/kysely-adapter": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.3.tgz", + "integrity": "sha512-4iZLGaajEdPMgtiTARINbNZGl6CPHSzlS0fl4ONWryP/52iakYhXYNBJIB70Ls1Xl+kEqYkBFmndfj/x4j18RQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.3", + "@better-auth/utils": "0.4.0", + "kysely": "^0.27.0 || ^0.28.0" + }, + "peerDependenciesMeta": { + "kysely": { + "optional": true + } + } + }, + "node_modules/@better-auth/memory-adapter": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.3.tgz", + "integrity": "sha512-0HCogGjUqVBl5j+7pkoovyIIAcCKsy8wiebDbTnedD99bCXQ+BhBAf8KQG1wMx6Nnc8fFwDuhSBhvTmCrdlmMQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.3", + "@better-auth/utils": "0.4.0" + } + }, + "node_modules/@better-auth/mongo-adapter": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.3.tgz", + "integrity": "sha512-xer3hjuYaqcx/qMdZMXTUQz4ROLeS14Knas6OSY2gK8jgAidZO7twcb+wLgTbtJYmoXZqKFzSxoWuf6LxVvZCw==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.3", + "@better-auth/utils": "0.4.0", + "mongodb": "^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "mongodb": { + "optional": true + } + } + }, + "node_modules/@better-auth/prisma-adapter": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.3.tgz", + "integrity": "sha512-vrlGEdrpzNH+S0AjnQt6T9jeIxqYDNRwq/1lOQ50wS5OAzSjtZQ+Q/UCrBTF8ZBrYzQq28zIAuk6k2+xhqxZpQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.3", + "@better-auth/utils": "0.4.0", + "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "node_modules/@better-auth/telemetry": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.3.tgz", + "integrity": "sha512-Kw2LFnxBt36KF0Cfw46qcOaNtuqgr6kjJPDHKHCx3b7tbiSAEeEhZCc7wvWYbZPXkgI58IGi+bMrgnWjFCG1Zw==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.3", + "@better-auth/utils": "0.4.0", + "@better-fetch/fetch": "1.1.21" + } + }, "node_modules/@better-auth/utils": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.0.tgz", @@ -3564,48 +3666,48 @@ "license": "MIT" }, "node_modules/@chat-adapter/discord": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@chat-adapter/discord/-/discord-4.25.0.tgz", - "integrity": "sha512-fV5XwPyfXiuiAE6gyNhBVg/fi+fkE0ovkOE8ANT6PAPNYnMvegtAt72oLvDvkSbujpngzXMlrUs6+l0sxAHC2g==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/discord/-/discord-4.26.0.tgz", + "integrity": "sha512-p0Xm/aPjHP9z87/tXtHz0KFmbUcu7SGPQAAlT8mrKRX49jO77Tognj/5poSmV1XQ1C4BLfxpPvTQlLNK/5omkw==", "license": "MIT", "dependencies": { - "@chat-adapter/shared": "4.25.0", - "chat": "4.25.0", + "@chat-adapter/shared": "4.26.0", + "chat": "4.26.0", "discord-api-types": "^0.37.119", "discord-interactions": "^4.4.0", "discord.js": "^14.25.1" } }, "node_modules/@chat-adapter/github": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@chat-adapter/github/-/github-4.25.0.tgz", - "integrity": "sha512-DeFGV1/6cpdVzQXafErx38XHsCH7W8UhjuFYt5ub1DFN0VvUYO48Cth7/cBloexh1duWRtvoLP3snh7PEUe8lA==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/github/-/github-4.26.0.tgz", + "integrity": "sha512-m0M+16raDkoV7QrAd/bgUKM807axya9zTIEVQ9uUQNtKd59CalFvc0STuZL5wqGNoOUAb8k3+J7olfp9L7XeCQ==", "license": "MIT", "dependencies": { - "@chat-adapter/shared": "4.25.0", + "@chat-adapter/shared": "4.26.0", "@octokit/auth-app": "^8.2.0", "@octokit/rest": "^22.0.1", - "chat": "4.25.0" + "chat": "4.26.0" } }, "node_modules/@chat-adapter/shared": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@chat-adapter/shared/-/shared-4.25.0.tgz", - "integrity": "sha512-NoRjEqNN1fyOTwT3AXNXjoy88QHk7FxhQxRus9JdEZBBYsDEuP4B8ekN+BVLQD/rlPcDgQ5TN0UGuiOi9r5nZQ==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/shared/-/shared-4.26.0.tgz", + "integrity": "sha512-YD0MGktFXrArUqTBsyPfL5vkdD1WBS58PAWO0oVrMQAMmPxpAXfWGjBtZCkf3y8R8Svb0uVuVXiMZSForaEnMQ==", "license": "MIT", "dependencies": { - "chat": "4.25.0" + "chat": "4.26.0" } }, "node_modules/@chat-adapter/slack": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@chat-adapter/slack/-/slack-4.25.0.tgz", - "integrity": "sha512-5jTLMzRUB3iyezaxNSxXAdJIX1xQFvMsVyOResrTWs72Mfa9Ae0K69iS7+X/Z2kbgj65w0wR54Oa5HXDt8HQWQ==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/slack/-/slack-4.26.0.tgz", + "integrity": "sha512-NNN47rURI6qpJf4rRK8xyeumjPTAXr7YSU4/FnViU8cFV/vKYFR4xZTzlFMVWNrYi9SmSwasUjcBmQznigK54Q==", "license": "MIT", "dependencies": { - "@chat-adapter/shared": "4.25.0", + "@chat-adapter/shared": "4.26.0", "@slack/web-api": "^7.14.0", - "chat": "4.25.0" + "chat": "4.26.0" } }, "node_modules/@chevrotain/cst-dts-gen": { @@ -3668,6 +3770,13 @@ "sisteransi": "^1.0.5" } }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260414.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260414.1.tgz", + "integrity": "sha512-E2wgYT1ywoM1M68nmVpxKdKzXsZm5vOu2plsqUixlK7YIydqsw31dZ+EjwXnAsdEjLaYC6XfsJayil8AEhyaBQ==", + "license": "MIT OR Apache-2.0", + "peer": true + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3678,6 +3787,109 @@ "node": ">=0.1.90" } }, + "node_modules/@composio/client": { + "version": "0.1.0-alpha.66", + "resolved": "https://registry.npmjs.org/@composio/client/-/client-0.1.0-alpha.66.tgz", + "integrity": "sha512-SDHcTWzz2dgdYAew/gQsRpjHpEzHKm6sW7KM4wCoJWNJuuMuey9Ahje91ZucNjfaqpI8CFW+pXY4nWkZf5LD7Q==", + "license": "Apache-2.0" + }, + "node_modules/@composio/core": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@composio/core/-/core-0.6.10.tgz", + "integrity": "sha512-T/9MfYcmPs2EcSXhrJWvm0UFTfa79FcTo1L8vO13d5C5kX8/I4hLYLu+1wc4n7WGBSE9hq7o1g9L3FXJrOrlSQ==", + "license": "ISC", + "dependencies": { + "@composio/client": "0.1.0-alpha.66", + "@composio/json-schema-to-zod": "0.1.20", + "@types/json-schema": "^7.0.15", + "chalk": "^4.1.2", + "openai": "^6.16.0", + "pusher-js": "^8.4.0", + "semver": "^7.7.2", + "zod-to-json-schema": "^3.25.1" + }, + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, + "node_modules/@composio/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@composio/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@composio/core/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@composio/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@composio/json-schema-to-zod": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@composio/json-schema-to-zod/-/json-schema-to-zod-0.1.20.tgz", + "integrity": "sha512-d4V34itLrUWG/VBh7ciznKcxF/T22MBLHmuEzHoX0zsBOHsUmjYz5qtDh20S2p3FE+HHvLZxpXiv8yfdd4yI+Q==", + "license": "ISC", + "peerDependencies": { + "zod": ">=3.25.76 <5" + } + }, + "node_modules/@composio/mastra": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@composio/mastra/-/mastra-0.6.10.tgz", + "integrity": "sha512-Npw4LMBf4jWM7aIKgJGSyyEK1iOT+Slti7Hu7so8GlrcyXwj32ncxRa3TC+pa8u+v9BUHmBz2kS87swGrcAT2g==", + "license": "ISC", + "dependencies": { + "@mastra/schema-compat": "^1.0.0" + }, + "peerDependencies": { + "@composio/core": "0.6.10", + "@mastra/core": "^1.0.4", + "zod": "^3.25 || ^4" + } + }, "node_modules/@crawlee/basic": { "version": "4.0.0-beta.43", "resolved": "https://registry.npmjs.org/@crawlee/basic/-/basic-4.0.0-beta.43.tgz", @@ -10011,6 +10223,22 @@ "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", "license": "BSD-3-Clause" }, + "node_modules/@mastra/cloudflare": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mastra/cloudflare/-/cloudflare-1.3.1.tgz", + "integrity": "sha512-nFVM8/r4sDLQyT1/hChSdGd0+Ix2VdbrLtn0CbHMoszwwtXUmBMbgui7oU+DzLxxS0q8lvMC+otHWd9KtdA/mA==", + "license": "Apache-2.0", + "dependencies": { + "cloudflare": "^5.2.0" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20240919.0", + "@mastra/core": ">=1.0.0-0 <2.0.0-0" + } + }, "node_modules/@mastra/cloudflare-d1": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@mastra/cloudflare-d1/-/cloudflare-d1-1.0.4.tgz", @@ -10422,6 +10650,31 @@ "typescript": "^4.7.2 || ^5" } }, + "node_modules/@mastra/editor": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@mastra/editor/-/editor-0.7.15.tgz", + "integrity": "sha512-6Jc0liGZL5y4D2oU1uCf5WzBVR2uzIEVzk0sO19A2Umtr+ZiJ2MmsIgkE4jUm+Q5DvAIP0jobvTd7w12JLySsw==", + "dependencies": { + "@arcadeai/arcadejs": "^2.3.0", + "@composio/core": "^0.6.5", + "@composio/mastra": "^0.6.5", + "@mastra/memory": "1.15.0", + "@mastra/schema-compat": "1.2.7" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@mastra/core": ">=1.7.1-0 <2.0.0-0", + "@mastra/mcp": ">=1.0.0-0 <2.0.0-0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@mastra/mcp": { + "optional": true + } + } + }, "node_modules/@mastra/evals": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@mastra/evals/-/evals-1.2.1.tgz", @@ -20246,17 +20499,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", - "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", + "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.58.1", - "@typescript-eslint/type-utils": "8.58.1", - "@typescript-eslint/utils": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/type-utils": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -20269,7 +20522,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.58.1", + "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -20285,16 +20538,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", - "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", + "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.58.1", - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "engines": { @@ -20310,14 +20563,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", - "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", + "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.1", - "@typescript-eslint/types": "^8.58.1", + "@typescript-eslint/tsconfig-utils": "^8.58.2", + "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "engines": { @@ -20332,14 +20585,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", - "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", + "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1" + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -20350,9 +20603,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", - "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", + "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", "dev": true, "license": "MIT", "engines": { @@ -20367,15 +20620,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", - "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", + "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1", - "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -20392,9 +20645,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", - "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", + "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", "dev": true, "license": "MIT", "engines": { @@ -20406,16 +20659,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", - "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", + "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.58.1", - "@typescript-eslint/tsconfig-utils": "8.58.1", - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/visitor-keys": "8.58.1", + "@typescript-eslint/project-service": "8.58.2", + "@typescript-eslint/tsconfig-utils": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -20447,16 +20700,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", - "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", + "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.1", - "@typescript-eslint/types": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1" + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -20471,13 +20724,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", - "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", + "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -22499,18 +22752,18 @@ "license": "Apache-2.0" }, "node_modules/better-auth": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.2.tgz", - "integrity": "sha512-5nqDAIj5xexmnk+GjjdrBknJCabi1mlvsVWJbxs4usHreao4vNdxIxINWDzCyDF9iDR1ildRZdXWSiYPAvTHhA==", - "license": "MIT", - "dependencies": { - "@better-auth/core": "1.6.2", - "@better-auth/drizzle-adapter": "1.6.2", - "@better-auth/kysely-adapter": "1.6.2", - "@better-auth/memory-adapter": "1.6.2", - "@better-auth/mongo-adapter": "1.6.2", - "@better-auth/prisma-adapter": "1.6.2", - "@better-auth/telemetry": "1.6.2", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.3.tgz", + "integrity": "sha512-jMsoSYQyO8nNRuLEoCP+OUShLyeIGU8ioPYqra0IteLjnS3WNjHj21YE/COSJ/V/f0H5SInZiF+uXcEEHREDMQ==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.6.3", + "@better-auth/drizzle-adapter": "1.6.3", + "@better-auth/kysely-adapter": "1.6.3", + "@better-auth/memory-adapter": "1.6.3", + "@better-auth/mongo-adapter": "1.6.3", + "@better-auth/prisma-adapter": "1.6.3", + "@better-auth/telemetry": "1.6.3", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", @@ -22603,95 +22856,6 @@ } } }, - "node_modules/better-auth/node_modules/@better-auth/drizzle-adapter": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.2.tgz", - "integrity": "sha512-KawrNNuhgmpcc5PgLs6HesMckxCscz5J+BQ99iRmU1cLzG/A87IcydrmYtep+K8WHPN0HmZ/i4z/nOBCtxE2qA==", - "license": "MIT", - "peerDependencies": { - "@better-auth/core": "^1.6.2", - "@better-auth/utils": "0.4.0", - "drizzle-orm": ">=0.41.0" - }, - "peerDependenciesMeta": { - "drizzle-orm": { - "optional": true - } - } - }, - "node_modules/better-auth/node_modules/@better-auth/kysely-adapter": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.2.tgz", - "integrity": "sha512-YMMm75jek/MNCAFWTAaq/U3VPmFnrwZW4NhBjjAwruHQJEIrSZZaOaUEXuUpFRRBhWqg7OOltQcHMwU/45CkuA==", - "license": "MIT", - "peerDependencies": { - "@better-auth/core": "^1.6.2", - "@better-auth/utils": "0.4.0", - "kysely": "^0.27.0 || ^0.28.0" - }, - "peerDependenciesMeta": { - "kysely": { - "optional": true - } - } - }, - "node_modules/better-auth/node_modules/@better-auth/memory-adapter": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.2.tgz", - "integrity": "sha512-QvuK5m7NFgkzLPHyab+NORu3J683nj36Tix58qq6DPcniyY6KZk5gY2yyh4+z1wgSjrxwY5NFx/DC2qz8B8NJg==", - "license": "MIT", - "peerDependencies": { - "@better-auth/core": "^1.6.2", - "@better-auth/utils": "0.4.0" - } - }, - "node_modules/better-auth/node_modules/@better-auth/mongo-adapter": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.2.tgz", - "integrity": "sha512-IvR2Q+1pjzxA4JXI3ED76+6fsqervIpZ2K5MxoX/+miLQhLEmNcbqqcItg4O2kfkxN8h33/ev57sjTW8QH9Tuw==", - "license": "MIT", - "peerDependencies": { - "@better-auth/core": "^1.6.2", - "@better-auth/utils": "0.4.0", - "mongodb": "^6.0.0 || ^7.0.0" - }, - "peerDependenciesMeta": { - "mongodb": { - "optional": true - } - } - }, - "node_modules/better-auth/node_modules/@better-auth/prisma-adapter": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.2.tgz", - "integrity": "sha512-bQkXYTo1zPau+xAiMpo1yCjEDSy7i7oeYlkYO+fSfRDCo52DE/9oPOOuI+EStmFkPUNSk9L2rhk8Fulifi8WCg==", - "license": "MIT", - "peerDependencies": { - "@better-auth/core": "^1.6.2", - "@better-auth/utils": "0.4.0", - "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", - "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" - }, - "peerDependenciesMeta": { - "@prisma/client": { - "optional": true - }, - "prisma": { - "optional": true - } - } - }, - "node_modules/better-auth/node_modules/@better-auth/telemetry": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.2.tgz", - "integrity": "sha512-o4gHKXqizUxVUUYChZZTowLEzdsz3ViBE/fKFzfHqNFUnF+aVt8QsbLSfipq1WpTIXyJVT/SnH0hgSdWxdssbQ==", - "license": "MIT", - "peerDependencies": { - "@better-auth/core": "^1.6.2", - "@better-auth/utils": "0.4.0", - "@better-fetch/fetch": "1.1.21" - } - }, "node_modules/better-auth/node_modules/@noble/ciphers": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", @@ -23520,9 +23684,9 @@ } }, "node_modules/chat": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/chat/-/chat-4.25.0.tgz", - "integrity": "sha512-QM8ex4Gpn8zYIPyQXh41Who6R9Wq3WcQeOjAy4EuR1m1ha0tASuzHkLQfjaTAGLgrgrThV0Zh5KKoH0S92iwNA==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/chat/-/chat-4.26.0.tgz", + "integrity": "sha512-QToDnIEGpyb8yQA6YLMHOSRK30YVk4RtsyFyuWFYyB2c4jQlyIrSWtwVK7qyvmvqzQp9uDwCdJRAhS8GtCHAGQ==", "license": "MIT", "dependencies": { "@workflow/serde": "4.1.0-beta.2", @@ -34471,9 +34635,9 @@ "license": "CC0-1.0" }, "node_modules/media-chrome": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.18.3.tgz", - "integrity": "sha512-YuS2wY0Fn+2nXGijJYn4+IE0n9wFe3v6SvOZHGNkoxh32T/cCcrXHUWskA+9tyYTONa6JKwKAOJJeO6QOlJLKw==", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.19.0.tgz", + "integrity": "sha512-HWhDTwts+BSbdPkkB1VsJXp5kvL0IxY7xFT5tBwliM2+89kTPVTnHnev+9it2f9PweANjT/C8/C/S0PW9oyZbA==", "license": "MIT", "dependencies": { "ce-la-react": "^0.3.2" @@ -36988,6 +37152,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "6.34.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.34.0.tgz", + "integrity": "sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/openapi-fetch": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.17.0.tgz", @@ -38455,6 +38640,15 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/pusher-js": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.5.0.tgz", + "integrity": "sha512-V7uzGi9bqOOOyM/6IkJdpFyjGZj7llz1v0oWnYkZKcYLvbz6VcHVLmzKqkvegjuMumpfIEKGLmWHwFb39XFCpw==", + "license": "MIT", + "dependencies": { + "tweetnacl": "^1.0.3" + } + }, "node_modules/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", @@ -43019,6 +43213,12 @@ "url": "https://github.com/sponsors/Wombosvideo" } }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -43199,16 +43399,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.58.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz", - "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", + "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.58.1", - "@typescript-eslint/parser": "8.58.1", - "@typescript-eslint/typescript-estree": "8.58.1", - "@typescript-eslint/utils": "8.58.1" + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 0efa4c13..7a7b6404 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agentstack", - "version": "1.0.41", + "version": "1.0.42", "description": "Multi-agent frameworks and tools for building AI applications with Mastra.", "main": "index.js", "scripts": { @@ -50,33 +50,36 @@ "node": ">=20.9.0" }, "dependencies": { - "@ai-sdk/google": "^3.0.62", - "@ai-sdk/google-vertex": "^4.0.108", + "@ai-sdk/google": "^3.0.63", + "@ai-sdk/google-vertex": "^4.0.109", "@ai-sdk/openai": "^3.0.52", "@ai-sdk/openai-compatible": "^2.0.41", "@ai-sdk/provider-utils": "^4.0.23", - "@ai-sdk/react": "^3.0.160", + "@ai-sdk/react": "^3.0.161", "@auth/agent": "^0.4.6", "@auth/agent-cli": "^0.4.5", - "@better-auth/api-key": "^1.6.2", - "@chat-adapter/discord": "^4.25.0", - "@chat-adapter/github": "^4.25.0", - "@chat-adapter/slack": "^4.25.0", + "@better-auth/api-key": "^1.6.3", + "@chat-adapter/discord": "^4.26.0", + "@chat-adapter/github": "^4.26.0", + "@chat-adapter/slack": "^4.26.0", "@dotenvx/dotenvx": "^1.61.0", "@emotion/react": "^11.14.0", "@github/copilot-sdk": "^0.2.2", "@gsap/react": "^2.1.2", "@libsql/kysely-libsql": "^0.4.1", + "@mastra/agent-browser": "^0.1.0", "@mastra/agentfs": "^0.1.0", "@mastra/ai-sdk": "^1.3.3", "@mastra/auth-better-auth": "^1.0.2", "@mastra/auth-supabase": "^1.0.0", "@mastra/client-js": "^1.13.3", + "@mastra/cloudflare": "^1.3.1", "@mastra/cloudflare-d1": "^1.0.4", "@mastra/convex": "^1.0.7", "@mastra/core": "^1.24.1", "@mastra/daytona": "^0.2.1", "@mastra/deployer": "^1.24.1", + "@mastra/editor": "^0.7.15", "@mastra/evals": "^1.2.1", "@mastra/gcs": "^0.2.0", "@mastra/lance": "^1.0.4", @@ -93,6 +96,7 @@ "@mastra/react": "^0.2.25", "@mastra/s3": "^0.3.0", "@mastra/schema-compat": "^1.2.7", + "@mastra/stagehand": "^0.1.0", "@mastra/upstash": "^1.0.4", "@mastra/vectorize": "^1.0.2", "@mastra/voice-google": "^0.12.0", @@ -145,7 +149,7 @@ "arraystat": "^1.7.81", "axios": "^1.15.0", "axios-retry": "^4.5.0", - "better-auth": "^1.6.2", + "better-auth": "^1.6.3", "bottleneck": "^2.19.5", "chalk": "^5.6.2", "chart.js": "^4.5.1", @@ -191,7 +195,7 @@ "marked": "^18.0.0", "mastracode": "^0.13.0", "mathjs": "^15.2.0", - "media-chrome": "^4.18.3", + "media-chrome": "^4.19.0", "module": "^2.0.0", "motion": "^12.38.0", "nanoid": "^5.1.7", @@ -276,8 +280,8 @@ "@types/strip-comments": "^2.0.4", "@types/three": "^0.183.1", "@types/unist": "^3.0.3", - "@typescript-eslint/eslint-plugin": "^8.58.1", - "@typescript-eslint/parser": "^8.58.1", + "@typescript-eslint/eslint-plugin": "^8.58.2", + "@typescript-eslint/parser": "^8.58.2", "@vitest/coverage-v8": "^4.1.4", "cross-env": "^10.1.0", "eslint": "^10.2.0", @@ -291,7 +295,7 @@ "tailwindcss": "^4.2.2", "tw-animate-css": "^1.4.0", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.1", + "typescript-eslint": "^8.58.2", "typescript-language-server": "^5.1.3", "vitest": "^4.1.4" }, @@ -313,8 +317,8 @@ "simple-git": "^3.36.0", "minimatch": "^10.2.5", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.1", - "@typescript-eslint/parser": "^8.58.1", - "@typescript-eslint/eslint-plugin": "^8.58.1" + "typescript-eslint": "^8.58.2", + "@typescript-eslint/parser": "^8.58.2", + "@typescript-eslint/eslint-plugin": "^8.58.2" } } diff --git a/src/mastra/agents/researchAgent.ts b/src/mastra/agents/researchAgent.ts index 7d0797e5..6e4377eb 100644 --- a/src/mastra/agents/researchAgent.ts +++ b/src/mastra/agents/researchAgent.ts @@ -1,13 +1,13 @@ -import { mdocumentChunker } from './../tools/document-chunking.tool'; +import { libsqlQueryTool, libsqlgraphQueryTool } from './../config/libsql'; +import { libsqlChunker, mdocumentChunker } from './../tools/document-chunking.tool'; import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google' import { Agent } from '@mastra/core/agent' -import { - TokenLimiterProcessor -} from '@mastra/core/processors' import { log } from '../config/logger' import { evaluateResultTool } from '../tools/evaluateResultTool' import { extractLearningsTool } from '../tools/extractLearningsTool' import { fetchTool } from '../tools/fetch.tool' +import { binanceSpotMarketDataTool } from '../tools/binance-crypto-market.tool' +import { coinbaseExchangeMarketDataTool } from '../tools/coinbase-exchange-crypto.tool' import { finnhubQuotesTool } from '../tools/finnhub-tools' import { polygonStockQuotesTool } from '../tools/polygon-tools' import { @@ -18,6 +18,8 @@ import { googleNewsLiteTool, googleTrendsTool, } from '../tools/serpapi-news-trends.tool' +import { stooqStockQuotesTool } from '../tools/stooq-stock-market-data.tool' +import { yahooFinanceStockQuotesTool } from '../tools/yahoo-finance-stock.tool' // Scorers import { InternalSpans } from '@mastra/core/observability' @@ -30,6 +32,8 @@ import { import { researchArxivDownloadWorkflow } from '../workflows/research/research-arxiv-download.workflow' import { researchArxivSearchWorkflow } from '../workflows/research/research-arxiv-search.workflow' import { LibsqlMemory } from '../config/libsql' +import { listRepositories } from '../tools/github'; +import { stagehand } from '../browsers'; type ResearchPhase = 'initial' | 'followup' | 'validation' const RESEARCH_PHASE_CONTEXT_KEY = 'researchPhase' as const @@ -56,11 +60,19 @@ const researchAgentTools = { googleNewsLiteTool, googleTrendsTool, mdocumentChunker, -// extractLearningsTool, -// evaluateResultTool, + libsqlChunker, + libsqlQueryTool, + libsqlgraphQueryTool, + listRepositories, + extractLearningsTool, + evaluateResultTool, polygonStockQuotesTool, finnhubQuotesTool, googleFinanceTool, + binanceSpotMarketDataTool, + coinbaseExchangeMarketDataTool, + stooqStockQuotesTool, + yahooFinanceStockQuotesTool, } /** @@ -97,7 +109,8 @@ Role: ${role} | Lang: ${language} | Phase: ${researchPhase} - **News/Trends**: 'googleNewsTool', 'googleTrendsTool', 'googleFinanceTool'. - **Academic**: 'googleScholarTool'. - **Financial**: Use 'polygon*' for stocks/crypto. -- **Internal**: 'mdocumentChunker' for embedding any information +- **Financial**: Use 'polygon*' for stocks/crypto when you need paid/commercial feeds; use 'binanceSpotMarketDataTool', 'coinbaseExchangeMarketDataTool', 'stooqStockQuotesTool', and 'yahooFinanceStockQuotesTool' for free public market data. +- **Internal**: 'libsqlChunker' for embedding any information, 'libsqlQueryTool' for querying embedded knowledge. 'libsqlgraphQueryTool' for complex relational queries. - **Processing**: use workspace document tools for PDFs, Markdown, and any other filetype in the workspace; ## Rules @@ -146,6 +159,7 @@ Role: ${role} | Lang: ${language} | Phase: ${researchPhase} // }), ], workspace: mainWorkspace, + browser: stagehand, // defaultOptions: { // autoResumeSuspendedTools: true, // }, diff --git a/src/mastra/browsers.ts b/src/mastra/browsers.ts new file mode 100644 index 00000000..5e283b92 --- /dev/null +++ b/src/mastra/browsers.ts @@ -0,0 +1,38 @@ +import { AgentBrowser } from '@mastra/agent-browser' +import { StagehandBrowser } from '@mastra/stagehand' + +const agentBrowser = new AgentBrowser({ + headless: false, + viewport: { + width: 1280, + height: 720, + }, + screencast: { + format: 'png', + quality: 80, + maxWidth: 1280, + maxHeight: 720, + everyNthFrame: 1, // Capture every 2nd frame to reduce bandwidth + }, +}) + +export const stagehand = new StagehandBrowser({ + headless: false, + model: 'google/gemini-3.1-flash-lite-preview', + selfHeal: true, + env: 'LOCAL', + scope: 'shared', + verbose: 2, + viewport: { + width: 1280, + height: 720, + }, + screencast: { + format: 'png', + quality: 80, + maxWidth: 1280, + maxHeight: 720, + everyNthFrame: 1, // Capture every frame + }, + systemPrompt: 'You can browse the web using natural language. Use stagehand_act to perform actions like "click the login button". Use stagehand_extract to get data from pages, stagehand_observe Discover actionable elements on a page, stagehand_navigate Navigate to a URL, stagehand_tabs Manage browser tabs, stagehand_close Close the browser' +}) diff --git a/src/mastra/config/libsql.ts b/src/mastra/config/libsql.ts index 7667c6c2..5d922ab1 100644 --- a/src/mastra/config/libsql.ts +++ b/src/mastra/config/libsql.ts @@ -74,7 +74,6 @@ export const LibsqlMemory = new Memory({ }, }, reflection: { - model: 'google/gemini-3.1-flash-lite-preview', instruction: 'Based on the observations, generate concise and informative reflections that capture important details, context, and insights from the conversation. These reflections should be useful for future reference and help provide context for the assistant.', modelSettings: { temperature: 0.3, diff --git a/src/mastra/index.ts b/src/mastra/index.ts index 74566828..d0402019 100644 --- a/src/mastra/index.ts +++ b/src/mastra/index.ts @@ -185,9 +185,24 @@ import { mainHarness } from './harness' import { supervisorAgent } from './agents/supervisor-agent' import { mastraAuth } from './auth' import { agentFsWorkspace } from './workspaces' +import { MastraEditor } from '@mastra/editor' +import { MastraCompositeStore } from '@mastra/core/storage' export const mastra = new Mastra({ workspace: agentFsWorkspace, + editor: new MastraEditor( + { + logger: log, + sandboxes: { + // Optional: restrict certain modules or APIs for security + }, + filesystems: { + + // Optional: configure storage limits, allowed file types, etc. + }, + // Optional: add a custom toolbar with specific tools or actions + } + ), workflows: { weatherWorkflow, contentStudioWorkflow, diff --git a/src/mastra/public/workspace/workspace/Agent tools b/src/mastra/public/workspace/workspace/Agent tools new file mode 100644 index 00000000..4c98f65f --- /dev/null +++ b/src/mastra/public/workspace/workspace/Agent tools @@ -0,0 +1,72 @@ +# Agent Tools Usage Report + +This file serves as a source of truth for testing and improving tools used in the Iran War Report update task. Below is a list of tools utilized, their purposes, outcomes, and any issues encountered. + +## Tools Used + +### 1. mastra_workspace_list_files +- **Purpose**: To explore the workspace directory structure and identify available files. +- **Parameters Used**: + - path: "." +- **Outcome**: Successfully listed directories (skills, workspace) and files (iran-war-report.md in workspace). Provided a clear tab-indented view. +- **Problems/Issues**: None. Tool worked as expected. +- **Suggestions**: None. + +### 2. mastra_workspace_read_file +- **Purpose**: To read the existing Iran War Report for content review and updates. +- **Parameters Used**: + - path: "workspace/iran-war-report.md" +- **Outcome**: Successfully retrieved the full content of the file (3507 bytes), including all sections and details. +- **Problems/Issues**: None. Tool worked flawlessly. +- **Suggestions**: None. + +### 3. googleNewsLiteTool +- **Purpose**: To gather the latest news updates on the Iran-Israel conflict for updating the report. +- **Parameters Used**: + - query: "Iran Israel conflict latest updates" + - numResults: 20 +- **Outcome**: Returned 20+ news articles from reliable sources (CNN, Al Jazeera, NYT, BBC, etc.), with titles, links, and sources. Provided comprehensive real-time data on Day 46 of the conflict, casualties, leadership changes, energy impacts, and more. +- **Problems/Issues**: None. Tool delivered high-quality, relevant results quickly. +- **Suggestions**: None. + +### 4. fetchTool +- **Purpose**: To search for expert predictions on the outcome of the Iran-Israel conflict in 2026. +- **Parameters Used**: + - query: "expert predictions outcome Iran Israel conflict 2026" + - searchProvider: "google" + - maxResults: 10 + - includeContent: true +- **Outcome**: Returned empty results array (totalResults: 0). No markdown content was fetched. +- **Problems/Issues**: Query too specific or future-dated ("2026") resulted in no search results. Google search may not yield content for hypothetical future events without existing analyses. +- **Suggestions**: For future predictions, use broader queries without specific years (e.g., "expert predictions Iran Israel conflict outcome") or rely on current expert analyses. Consider supplementing with googleScholarTool or manual research. + +### 5. googleScholarTool +- **Purpose**: To find academic papers on predictions for the Iran-Israel conflict outcome. +- **Parameters Used**: + - query: "predictions outcome Iran Israel conflict" + - numResults: 10 +- **Outcome**: Tool executed but returned invalid output due to validation errors. The 'authors' field was an array (as expected for multiple authors) but the schema expected a string. +- **Problems/Issues**: Output validation failed because authors were arrays (e.g., ["Author1", "Author2"]) instead of strings. This prevented processing the results, though the tool did fetch data (truncated in error message). +- **Suggestions**: Update the tool's output schema to allow authors as an array of strings. This is standard for academic citations. Alternatively, concatenate authors into a single string for compatibility. + +### 6. mastra_workspace_edit_file +- **Purpose**: To update the Iran War Report with new data and add a predictions section. +- **Parameters Used**: + - path: "workspace/iran-war-report.md" + - old_string: [Full original content] + - new_string: [Updated content with latest info and predictions] +- **Outcome**: Successfully replaced the entire content, adding updated conflict status (Day 46), casualties, leadership, energy impacts, and a new "Predictions Section" with expert-based forecasts and confidence levels. +- **Problems/Issues**: None. Tool handled large content replacement accurately. +- **Suggestions**: None. Tool is reliable for file edits. + +## Overall Summary +- **Total Tools Used**: 6 +- **Success Rate**: 5/6 tools worked without issues. fetchTool and googleScholarTool had failures due to query specificity and schema mismatch, respectively. +- **Key Insights for Improvement**: + - **Query Sensitivity**: Tools like fetchTool are sensitive to overly specific or future-oriented queries. Recommend fallback strategies or broader searches. + - **Schema Validation**: googleScholarTool needs schema fix for authors field to handle arrays. + - **Reliability**: Workspace tools (read, write, edit) are highly reliable. News tools (googleNewsLiteTool) provide excellent real-time data. +- **Recommendations**: Use this log to prioritize fixes for googleScholarTool schema and enhance search tool fallbacks for predictive queries. + +*Date: April 14, 2026* +*Prepared by: Senior Research Analyst* \ No newline at end of file diff --git a/src/mastra/public/workspace/workspace/Investment Recommendations 2026-04-14 12 b/src/mastra/public/workspace/workspace/Investment Recommendations 2026-04-14 12 new file mode 100644 index 00000000..e69de29b diff --git a/src/mastra/tools/binance-crypto-market.tool.ts b/src/mastra/tools/binance-crypto-market.tool.ts new file mode 100644 index 00000000..f7b49763 --- /dev/null +++ b/src/mastra/tools/binance-crypto-market.tool.ts @@ -0,0 +1,430 @@ +import { SpanType, getOrCreateSpan } from '@mastra/core/observability' +import type { TracingContext } from '@mastra/core/observability' +import type { RequestContext } from '@mastra/core/request-context' +import { createTool, type InferUITool } from '@mastra/core/tools' +import { z } from 'zod' +import { log } from '../config/logger' +import { httpFetch } from '../lib/http-client' +import { + buildBinanceSymbol, + normalizeBinanceKlines, +} from './market-data.helpers' +import type { + Binance24hrTicker, + BinanceAggTrade, + BinanceAvgPrice, + BinanceExchangeInfo, + BinanceQuote, + BinanceTrade, + BinanceKlineRow, + NormalizedCandle, +} from './market-data.helpers' + +/** + * Shared request context for Binance crypto data. + */ +export interface BinanceCryptoMarketContext extends RequestContext { + userId?: string +} + +const binanceIntervalSchema = z.enum([ + '1m', + '3m', + '5m', + '15m', + '30m', + '1h', + '2h', + '4h', + '6h', + '8h', + '12h', + '1d', + '3d', + '1w', + '1M', +]) + +const binanceCryptoInputSchema = z.object({ + function: z + .enum([ + 'quote', + 'stats24hr', + 'candles', + 'uiKlines', + 'orderbook', + 'trades', + 'aggTrades', + 'avgPrice', + 'exchangeInfo', + ]) + .default('quote') + .describe('Binance public market-data function'), + symbol: z.string().min(1).describe('Base asset symbol, e.g. BTC or ETH'), + quoteAsset: z + .string() + .default('USDT') + .describe('Quote asset used to build the Binance pair'), + interval: binanceIntervalSchema.default('1d'), + limit: z.number().int().min(1).max(1000).default(100), + fromId: z.number().int().nonnegative().optional(), + startTime: z.number().int().positive().optional(), + endTime: z.number().int().positive().optional(), + timeZone: z.string().optional(), +}) + +type BinanceCryptoInput = z.infer + +type BinanceSpotQuoteData = BinanceQuote +type BinanceSpotStats24hrData = Binance24hrTicker +type BinanceSpotCandlesData = { + symbol: string + interval: string + candles: NormalizedCandle[] +} +type BinanceSpotOrderBookData = { + symbol: string + lastUpdateId: number | null + bids: Array<{ price: number; quantity: number }> + asks: Array<{ price: number; quantity: number }> +} +type BinanceSpotTradesData = { + symbol: string + trades: BinanceTrade[] +} +type BinanceSpotAggTradesData = { + symbol: string + aggTrades: BinanceAggTrade[] +} +type BinanceSpotAvgPriceData = BinanceAvgPrice & { symbol: string } +type BinanceSpotExchangeInfoData = BinanceExchangeInfo + +type BinanceSpotMarketDataData = + | BinanceSpotQuoteData + | BinanceSpotStats24hrData + | BinanceSpotCandlesData + | BinanceSpotOrderBookData + | BinanceSpotTradesData + | BinanceSpotAggTradesData + | BinanceSpotAvgPriceData + | BinanceSpotExchangeInfoData + +type BinanceSpotMarketDataOutput = { + data: BinanceSpotMarketDataData + metadata: { + source: 'binance' + function: string + symbol: string + market: string + } +} + +/** + * Counts the number of items represented by a Binance market-data output payload. + * + * @param payload - The normalized Binance market-data payload. + * @returns A best-effort item count for logging. + */ +function countBinanceMarketDataItems( + payload: BinanceSpotMarketDataOutput['data'] | undefined +): number { + if (!payload) { + return 0 + } + + if ('candles' in payload) { + return (payload as BinanceSpotCandlesData).candles.length + } + + if ('trades' in payload) { + return (payload as BinanceSpotTradesData).trades.length + } + + if ('aggTrades' in payload) { + return (payload as BinanceSpotAggTradesData).aggTrades.length + } + + if ('bids' in payload) { + return (payload as BinanceSpotOrderBookData).bids.length + } + + if ('asks' in payload) { + return (payload as BinanceSpotOrderBookData).asks.length + } + + return 1 +} + +const BINANCE_BASE_URL = 'https://data-api.binance.vision' + +/** + * Binance crypto market-data tool. + */ +export const binanceSpotMarketDataTool = createTool({ + id: 'binance-spot-market-data', + description: + 'Fetch free Binance public spot market data including quotes, 24h stats, candles, UI klines, order book, trades, aggregate trades, average price, and exchange info.', + inputSchema: binanceCryptoInputSchema, + outputSchema: z.custom(), + onInputStart: ({ toolCallId, messages, abortSignal }) => { + log.info('Binance spot market-data input streaming started', { + toolCallId, + messageCount: messages?.length ?? 0, + abortSignal: abortSignal?.aborted, + hook: 'onInputStart', + }) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + log.info('Binance spot market-data received input chunk', { + toolCallId, + inputTextDelta, + messageCount: messages?.length ?? 0, + abortSignal: abortSignal?.aborted, + hook: 'onInputDelta', + }) + }, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + log.info('Binance spot market-data received input', { + toolCallId, + messageCount: messages?.length ?? 0, + function: input.function, + symbol: input.symbol, + abortSignal: abortSignal?.aborted, + hook: 'onInputAvailable', + }) + }, + execute: async (input, context) => { + const writer = context?.writer + const abortSignal = context?.abortSignal + const tracingContext: TracingContext | undefined = context?.tracingContext + const requestFunction = input.function ?? 'quote' + const limit = input.limit ?? 100 + const interval = input.interval ?? '1d' + const market = input.quoteAsset ?? 'USDT' + const symbol = buildBinanceSymbol(input.symbol, market) + + if (abortSignal?.aborted === true) { + throw new Error('Binance crypto request cancelled') + } + + const span = getOrCreateSpan({ + type: SpanType.TOOL_CALL, + name: 'binance-spot-market-data', + input, + metadata: { + 'tool.id': 'binance-spot-market-data', + 'tool.input.function': requestFunction, + 'tool.input.symbol': symbol, + }, + requestContext: context?.requestContext, + tracingContext, + }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: `Fetching Binance spot data for ${symbol}`, + stage: 'binance-spot-market-data', + }, + id: 'binance-spot-market-data', + }) + + try { + let data: BinanceSpotMarketDataData + + switch (requestFunction) { + case 'quote': { + const response = await httpFetch(`${BINANCE_BASE_URL}/api/v3/ticker/price`, { + timeout: 30000, + params: { symbol }, + }) + data = await response.json() + break + } + case 'stats24hr': { + const response = await httpFetch(`${BINANCE_BASE_URL}/api/v3/ticker/24hr`, { + timeout: 30000, + params: { symbol }, + }) + data = await response.json() + break + } + case 'candles': { + const response = await httpFetch(`${BINANCE_BASE_URL}/api/v3/klines`, { + timeout: 30000, + params: { + symbol, + interval, + limit, + ...(input.startTime !== undefined ? { startTime: input.startTime } : {}), + ...(input.endTime !== undefined ? { endTime: input.endTime } : {}), + ...(input.timeZone !== undefined ? { timeZone: input.timeZone } : {}), + }, + }) + const rows = (await response.json()) as readonly BinanceKlineRow[] + data = { + symbol, + interval, + candles: normalizeBinanceKlines(rows).slice(-limit), + } + break + } + case 'uiKlines': { + const response = await httpFetch(`${BINANCE_BASE_URL}/api/v3/uiKlines`, { + timeout: 30000, + params: { + symbol, + interval, + limit, + ...(input.startTime !== undefined ? { startTime: input.startTime } : {}), + ...(input.endTime !== undefined ? { endTime: input.endTime } : {}), + ...(input.timeZone !== undefined ? { timeZone: input.timeZone } : {}), + }, + }) + const rows = (await response.json()) as readonly BinanceKlineRow[] + data = { + symbol, + interval, + candles: normalizeBinanceKlines(rows).slice(-limit), + } + break + } + case 'orderbook': { + const response = await httpFetch(`${BINANCE_BASE_URL}/api/v3/depth`, { + timeout: 30000, + params: { + symbol, + limit, + }, + }) + data = (await response.json()) as BinanceSpotOrderBookData + break + } + case 'trades': { + const response = await httpFetch(`${BINANCE_BASE_URL}/api/v3/trades`, { + timeout: 30000, + params: { + symbol, + limit, + }, + }) + const trades = (await response.json()) as BinanceTrade[] + data = { + symbol, + trades, + } + break + } + case 'aggTrades': { + const response = await httpFetch(`${BINANCE_BASE_URL}/api/v3/aggTrades`, { + timeout: 30000, + params: { + symbol, + limit, + ...(input.fromId !== undefined ? { fromId: input.fromId } : {}), + ...(input.startTime !== undefined ? { startTime: input.startTime } : {}), + ...(input.endTime !== undefined ? { endTime: input.endTime } : {}), + }, + }) + const aggTrades = (await response.json()) as BinanceAggTrade[] + data = { + symbol, + aggTrades, + } + break + } + case 'avgPrice': { + const response = await httpFetch(`${BINANCE_BASE_URL}/api/v3/avgPrice`, { + timeout: 30000, + params: { symbol }, + }) + data = { + ...(await response.json()), + symbol, + } as BinanceSpotAvgPriceData + break + } + case 'exchangeInfo': { + const response = await httpFetch(`${BINANCE_BASE_URL}/api/v3/exchangeInfo`, { + timeout: 30000, + params: { symbol }, + }) + data = (await response.json()) as BinanceSpotExchangeInfoData + break + } + default: + throw new Error(`Unsupported Binance function: ${requestFunction}`) + } + + const output: BinanceSpotMarketDataOutput = { + data, + metadata: { + source: 'binance' as const, + function: requestFunction, + symbol, + market, + }, + } + + span?.update({ + output, + metadata: { + 'tool.output.source': 'binance', + }, + }) + span?.end() + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `✅ Binance crypto data ready for ${symbol}`, + stage: 'binance-crypto-market-data', + }, + id: 'binance-crypto-market-data', + }) + + return output + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + span?.error({ + error: error instanceof Error ? error : new Error(errorMessage), + endSpan: true, + }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `❌ Binance crypto request failed: ${errorMessage}`, + stage: 'binance-crypto-market-data', + }, + id: 'binance-crypto-market-data', + }) + + log.error('Binance crypto market-data tool failed', { + function: requestFunction, + symbol, + error: errorMessage, + }) + + throw error instanceof Error ? error : new Error(errorMessage) + } + }, + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + const data = output as BinanceSpotMarketDataOutput | undefined + const count = countBinanceMarketDataItems(data?.data) + + log.info('Binance spot market-data completed', { + toolCallId, + toolName, + count, + abortSignal: abortSignal?.aborted, + hook: 'onOutput', + }) + }, +}) + +export type BinanceSpotMarketDataInput = BinanceCryptoInput +export type BinanceSpotMarketDataUITool = InferUITool \ No newline at end of file diff --git a/src/mastra/tools/coinbase-exchange-crypto.tool.ts b/src/mastra/tools/coinbase-exchange-crypto.tool.ts new file mode 100644 index 00000000..7a3ca688 --- /dev/null +++ b/src/mastra/tools/coinbase-exchange-crypto.tool.ts @@ -0,0 +1,338 @@ +import { SpanType, getOrCreateSpan } from '@mastra/core/observability' +import type { TracingContext } from '@mastra/core/observability' +import type { RequestContext } from '@mastra/core/request-context' +import { createTool, type InferUITool } from '@mastra/core/tools' +import { z } from 'zod' +import { log } from '../config/logger' +import { httpFetch } from '../lib/http-client' +import { buildCoinbaseProductId, normalizeCoinbaseCandles } from './market-data.helpers' +import type { + CoinbaseBookLevel, + CoinbaseCandleRow, + CoinbaseStats, + CoinbaseTicker, + CoinbaseTrade, +} from './market-data.helpers' + +/** + * Shared request context for Coinbase Exchange crypto data. + */ +export interface CoinbaseExchangeCryptoContext extends RequestContext { + userId?: string +} + +const coinbaseGranularitySchema = z.enum([ + '60', + '300', + '900', + '3600', + '21600', + '86400', +]) + +const coinbaseInputSchema = z.object({ + function: z + .enum(['ticker', 'stats', 'candles', 'orderbook', 'trades', 'products']) + .default('ticker') + .describe('Coinbase Exchange public market-data function'), + baseCurrency: z.string().min(1).describe('Base currency, e.g. BTC or ETH'), + quoteCurrency: z.string().default('USD').describe('Quote currency, e.g. USD'), + granularity: coinbaseGranularitySchema.default('86400'), + limit: z.number().int().min(1).max(300).default(100), + orderbookLevel: z.enum(['1', '2', '3']).default('2'), + start: z.string().optional().describe('ISO8601 start time for candle queries'), + end: z.string().optional().describe('ISO8601 end time for candle queries'), +}) + +type CoinbaseInput = z.infer + +type CoinbaseProduct = { + id: string + base_currency?: string + quote_currency?: string +} + +type CoinbaseCandlesData = { + productId: string + granularity: string + candles: ReturnType +} + +type CoinbaseOrderBookData = { + sequence?: number + bids: CoinbaseBookLevel[] + asks: CoinbaseBookLevel[] +} + +type CoinbaseMarketDataData = + | CoinbaseTicker + | CoinbaseStats + | CoinbaseCandlesData + | CoinbaseOrderBookData + | CoinbaseTrade[] + | CoinbaseProduct[] + +type CoinbaseMarketDataOutput = { + data: CoinbaseMarketDataData + metadata: { + source: 'coinbase-exchange' + function: string + symbol: string + market: string + } +} + +/** + * Counts the number of items in a Coinbase market-data payload. + * + * @param payload - The normalized Coinbase payload. + * @returns A best-effort count for logging. + */ +function countCoinbaseMarketDataItems( + payload: CoinbaseMarketDataOutput['data'] | undefined +): number { + if (!payload) { + return 0 + } + + if (Array.isArray(payload)) { + return payload.length + } + + if ('candles' in payload) { + return (payload as CoinbaseCandlesData).candles.length + } + + if ('bids' in payload) { + return (payload as CoinbaseOrderBookData).bids.length + } + + if ('asks' in payload) { + return (payload as CoinbaseOrderBookData).asks.length + } + + return 1 +} + +const COINBASE_BASE_URL = 'https://api.exchange.coinbase.com' + +/** + * Coinbase Exchange market-data tool. + */ +export const coinbaseExchangeMarketDataTool = createTool({ + id: 'coinbase-exchange-market-data', + description: + 'Fetch free Coinbase Exchange public crypto market data including ticker, 24h stats, candles, order book, trades, and products.', + inputSchema: coinbaseInputSchema, + outputSchema: z.custom(), + onInputStart: ({ toolCallId, messages, abortSignal }) => { + log.info('Coinbase Exchange market-data input streaming started', { + toolCallId, + messageCount: messages?.length ?? 0, + abortSignal: abortSignal?.aborted, + hook: 'onInputStart', + }) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + log.info('Coinbase Exchange market-data received input chunk', { + toolCallId, + inputTextDelta, + messageCount: messages?.length ?? 0, + abortSignal: abortSignal?.aborted, + hook: 'onInputDelta', + }) + }, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + log.info('Coinbase Exchange market-data received input', { + toolCallId, + messageCount: messages?.length ?? 0, + function: input.function, + baseCurrency: input.baseCurrency, + quoteCurrency: input.quoteCurrency, + abortSignal: abortSignal?.aborted, + hook: 'onInputAvailable', + }) + }, + execute: async (input, context) => { + const writer = context?.writer + const abortSignal = context?.abortSignal + const tracingContext: TracingContext | undefined = context?.tracingContext + const requestFunction = input.function ?? 'ticker' + const productId = buildCoinbaseProductId( + input.baseCurrency, + input.quoteCurrency ?? 'USD' + ) + const granularity = input.granularity ?? '86400' + const limit = input.limit ?? 100 + const level = input.orderbookLevel ?? '2' + + if (abortSignal?.aborted === true) { + throw new Error('Coinbase Exchange crypto request cancelled') + } + + const span = getOrCreateSpan({ + type: SpanType.TOOL_CALL, + name: 'coinbase-exchange-market-data', + input, + metadata: { + 'tool.id': 'coinbase-exchange-market-data', + 'tool.input.function': requestFunction, + 'tool.input.productId': productId, + }, + requestContext: context?.requestContext, + tracingContext, + }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: `Fetching Coinbase Exchange data for ${productId}`, + stage: 'coinbase-exchange-market-data', + }, + id: 'coinbase-exchange-market-data', + }) + + try { + let data: CoinbaseMarketDataData + + switch (requestFunction) { + case 'ticker': { + const response = await httpFetch(`${COINBASE_BASE_URL}/products/${encodeURIComponent(productId)}/ticker`, { + timeout: 30000, + }) + data = (await response.json()) as CoinbaseTicker + break + } + case 'stats': { + const response = await httpFetch(`${COINBASE_BASE_URL}/products/${encodeURIComponent(productId)}/stats`, { + timeout: 30000, + }) + data = (await response.json()) as CoinbaseStats + break + } + case 'candles': { + const response = await httpFetch(`${COINBASE_BASE_URL}/products/${encodeURIComponent(productId)}/candles`, { + timeout: 30000, + params: { + granularity: Number(granularity), + ...(input.start !== undefined ? { start: input.start } : {}), + ...(input.end !== undefined ? { end: input.end } : {}), + }, + }) + const rows = (await response.json()) as readonly CoinbaseCandleRow[] + data = { + productId, + granularity, + candles: normalizeCoinbaseCandles(rows).slice(-limit), + } + break + } + case 'orderbook': { + const response = await httpFetch(`${COINBASE_BASE_URL}/products/${encodeURIComponent(productId)}/book`, { + timeout: 30000, + params: { + level, + }, + }) + const book = (await response.json()) as { + sequence?: number + bids?: CoinbaseBookLevel[] + asks?: CoinbaseBookLevel[] + } + data = { + sequence: book.sequence, + bids: book.bids ?? [], + asks: book.asks ?? [], + } + break + } + case 'trades': { + const response = await httpFetch(`${COINBASE_BASE_URL}/products/${encodeURIComponent(productId)}/trades`, { + timeout: 30000, + }) + data = (await response.json()) as CoinbaseTrade[] + break + } + case 'products': { + const response = await httpFetch(`${COINBASE_BASE_URL}/products`, { + timeout: 30000, + }) + data = (await response.json()) as CoinbaseProduct[] + break + } + default: + throw new Error(`Unsupported Coinbase Exchange function: ${requestFunction}`) + } + + const output: CoinbaseMarketDataOutput = { + data, + metadata: { + source: 'coinbase-exchange' as const, + function: requestFunction, + symbol: productId, + market: input.quoteCurrency ?? 'USD', + }, + } + + span?.update({ + output, + metadata: { + 'tool.output.source': 'coinbase-exchange', + }, + }) + span?.end() + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `✅ Coinbase Exchange data ready for ${productId}`, + stage: 'coinbase-exchange-crypto-market-data', + }, + id: 'coinbase-exchange-crypto-market-data', + }) + + return output + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + span?.error({ + error: error instanceof Error ? error : new Error(errorMessage), + endSpan: true, + }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `❌ Coinbase Exchange request failed: ${errorMessage}`, + stage: 'coinbase-exchange-crypto-market-data', + }, + id: 'coinbase-exchange-crypto-market-data', + }) + + log.error('Coinbase Exchange crypto market-data tool failed', { + function: requestFunction, + productId, + error: errorMessage, + }) + + throw error instanceof Error ? error : new Error(errorMessage) + } + }, + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + const data = output as CoinbaseMarketDataOutput | undefined + const count = countCoinbaseMarketDataItems(data?.data) + + log.info('Coinbase Exchange market-data completed', { + toolCallId, + toolName, + count, + abortSignal: abortSignal?.aborted, + hook: 'onOutput', + }) + }, +}) + +export type CoinbaseExchangeMarketDataInput = CoinbaseInput +export type CoinbaseExchangeMarketDataUITool = InferUITool \ No newline at end of file diff --git a/src/mastra/tools/index.ts b/src/mastra/tools/index.ts index d43aaa0c..746076b0 100644 --- a/src/mastra/tools/index.ts +++ b/src/mastra/tools/index.ts @@ -27,6 +27,10 @@ export * from './jwt-auth.tool' export * from './pdf' export * from './polygon-tools' export * from './random-generator.tool' +export * from './binance-crypto-market.tool' +export * from './coinbase-exchange-crypto.tool' +export * from './stooq-stock-market-data.tool' +export * from './yahoo-finance-stock.tool' export * from './serpapi-academic-local.tool' export * from './serpapi-news-trends.tool' export * from './serpapi-search.tool' diff --git a/src/mastra/tools/market-data.helpers.ts b/src/mastra/tools/market-data.helpers.ts new file mode 100644 index 00000000..cbcddffb --- /dev/null +++ b/src/mastra/tools/market-data.helpers.ts @@ -0,0 +1,346 @@ +import { parse } from 'csv-parse/sync' + +/** + * Normalized OHLCV candle row used by the market-data tools. + */ +export interface NormalizedCandle { + timestamp: string + open: number | null + high: number | null + low: number | null + close: number | null + volume: number | null +} + +/** + * Binance kline row tuple. + */ +export type BinanceKlineRow = readonly [ + number, + string, + string, + string, + string, + string, + number, + string, + number, + string, + string, + string, +] + +/** + * Binance aggregate trade row. + */ +export interface BinanceAggTrade { + a: number + p: string + q: string + f: number + l: number + T: number + m: boolean + M: boolean +} + +/** + * Binance recent trade row. + */ +export interface BinanceTrade { + id: number + price: string + qty: string + quoteQty: string + time: number + isBuyerMaker: boolean + isBestMatch: boolean +} + +/** + * Binance quote response. + */ +export interface BinanceQuote { + symbol: string + price: string +} + +/** + * Binance 24h ticker response. + */ +export interface Binance24hrTicker { + symbol: string + priceChange: string + priceChangePercent: string + weightedAvgPrice?: string + prevClosePrice?: string + lastPrice: string + lastQty?: string + bidPrice?: string + bidQty?: string + askPrice?: string + askQty?: string + openPrice: string + highPrice: string + lowPrice: string + volume: string + quoteVolume: string + openTime?: number + closeTime?: number + firstId?: number + lastId?: number + count?: number +} + +/** + * Binance average price response. + */ +export interface BinanceAvgPrice { + mins: number + price: string + closeTime: number +} + +/** + * Binance exchange info symbol record. + */ +export interface BinanceExchangeInfoSymbol { + symbol: string + status?: string + baseAsset?: string + quoteAsset?: string +} + +/** + * Binance exchange info response. + */ +export interface BinanceExchangeInfo { + timezone?: string + symbols?: BinanceExchangeInfoSymbol[] +} + +/** + * Coinbase candle row. + */ +export type CoinbaseCandleRow = readonly [ + number, + number, + number, + number, + number, + number, +] + +/** + * Coinbase ticker response. + */ +export interface CoinbaseTicker { + trade_id?: number + price?: string + size?: string + bid?: string + ask?: string + volume?: string + time?: string +} + +/** + * Coinbase stats response. + */ +export interface CoinbaseStats { + open?: string + high?: string + low?: string + last?: string + volume?: string + volume_30day?: string +} + +/** + * Coinbase order book entry. + */ +export type CoinbaseBookLevel = readonly [string, string, string?] + +/** + * Coinbase trade row. + */ +export interface CoinbaseTrade { + trade_id?: number + side?: 'buy' | 'sell' + price?: string + size?: string + time?: string +} + +/** + * Yahoo Finance chart response. + */ +export interface YahooChartResponse { + chart?: { + result?: Array<{ + timestamp?: number[] + indicators?: { + quote?: Array<{ + open?: Array + high?: Array + low?: Array + close?: Array + volume?: Array + }> + } + }> + } +} + +/** + * Yahoo Finance quote record. + */ +export interface YahooQuote { + symbol?: string + shortName?: string + longName?: string + currency?: string + regularMarketPrice?: number + regularMarketChange?: number + regularMarketChangePercent?: number + marketCap?: number + regularMarketDayHigh?: number + regularMarketDayLow?: number + regularMarketTime?: number +} + +/** + * Builds a Binance spot symbol from a base asset and quote asset. + * + * @param symbol - Base asset or already combined trading pair. + * @param quoteAsset - Quote asset used when the pair is not already combined. + * @returns Binance trading symbol. + */ +export function buildBinanceSymbol( + symbol: string, + quoteAsset = 'USDT' +): string { + const normalizedSymbol = symbol.trim().toUpperCase() + const normalizedQuoteAsset = quoteAsset.trim().toUpperCase() + + if (normalizedSymbol.endsWith(normalizedQuoteAsset)) { + return normalizedSymbol + } + + return `${normalizedSymbol}${normalizedQuoteAsset}` +} + +/** + * Builds a Coinbase Exchange product id from base and quote currencies. + * + * @param baseCurrency - Base currency such as BTC. + * @param quoteCurrency - Quote currency such as USD. + * @returns Coinbase product id like BTC-USD. + */ +export function buildCoinbaseProductId( + baseCurrency: string, + quoteCurrency = 'USD' +): string { + return `${baseCurrency.trim().toUpperCase()}-${quoteCurrency.trim().toUpperCase()}` +} + +/** + * Builds a Stooq symbol from a ticker and optional market suffix. + * + * @param symbol - Stock ticker symbol. + * @param marketSuffix - Stooq market suffix. + * @returns Stooq symbol in the form `ticker.suffix`. + */ +export function buildStooqSymbol( + symbol: string, + marketSuffix = 'us' +): string { + const normalized = symbol.trim().toLowerCase() + if (normalized.includes('.')) { + return normalized + } + return `${normalized}.${marketSuffix.trim().toLowerCase()}` +} + +/** + * Parses a CSV payload from Stooq into row objects. + * + * @param csvText - Raw CSV response body. + * @returns Parsed rows. + */ +export function parseStooqCsv(csvText: string): Array> { + return parse(csvText, { + columns: true, + skip_empty_lines: true, + trim: true, + }) as Array> +} + +/** + * Normalizes Binance kline arrays into a common candle shape. + * + * @param rows - Raw Binance kline response. + * @returns Normalized candles. + */ +export function normalizeBinanceKlines( + rows: readonly BinanceKlineRow[] +): NormalizedCandle[] { + return rows + .map((row) => { + const openTime = Number(row[0] ?? 0) + return { + timestamp: new Date(openTime).toISOString(), + open: Number(row[1] ?? 0), + high: Number(row[2] ?? 0), + low: Number(row[3] ?? 0), + close: Number(row[4] ?? 0), + volume: Number(row[5] ?? 0), + } + }) +} + +/** + * Normalizes Coinbase candle arrays into a common candle shape. + * + * @param rows - Raw Coinbase candle response. + * @returns Normalized candles. + */ +export function normalizeCoinbaseCandles( + rows: readonly CoinbaseCandleRow[] +): NormalizedCandle[] { + return rows + .map((row) => { + const timestamp = Number(row[0] ?? 0) + return { + timestamp: new Date(timestamp * 1000).toISOString(), + low: Number(row[1] ?? 0), + high: Number(row[2] ?? 0), + open: Number(row[3] ?? 0), + close: Number(row[4] ?? 0), + volume: Number(row[5] ?? 0), + } + }) +} + +/** + * Converts a Yahoo Finance chart response into a normalized series. + * + * @param response - Yahoo chart API response. + * @returns History rows with OHLCV data. + */ +export function normalizeYahooChartHistory( + response: YahooChartResponse +): NormalizedCandle[] { + const result = response.chart?.result?.[0] + const timestamps = result?.timestamp ?? [] + const quote = result?.indicators?.quote?.[0] + + return timestamps.map((timestamp, index) => ({ + timestamp: new Date(timestamp * 1000).toISOString(), + open: quote?.open?.[index] ?? null, + high: quote?.high?.[index] ?? null, + low: quote?.low?.[index] ?? null, + close: quote?.close?.[index] ?? null, + volume: quote?.volume?.[index] ?? null, + })) +} \ No newline at end of file diff --git a/src/mastra/tools/serpapi-academic-local.tool.ts b/src/mastra/tools/serpapi-academic-local.tool.ts index 053811f8..da0848eb 100644 --- a/src/mastra/tools/serpapi-academic-local.tool.ts +++ b/src/mastra/tools/serpapi-academic-local.tool.ts @@ -101,6 +101,20 @@ interface YelpApiResponse { organic_results?: YelpBusiness[] } +type GoogleScholarOutput = z.infer + +/** + * Safely counts Google Scholar papers in a tool output payload. + * + * @param output - Optional tool output payload + * @returns The number of papers present in the payload + */ +export function getGoogleScholarPaperCount( + output?: Partial +): number { + return output?.papers?.length ?? 0 +} + // Google Scholar Tool const googleScholarInputSchema = z.object({ query: z.string().min(1).describe('Academic search query'), @@ -162,7 +176,7 @@ export const googleScholarTool = createTool({ onInputStart: ({ toolCallId, messages, abortSignal }) => { log.info('Google Scholar tool input streaming started', { toolCallId, - messageCount: messages.length, + messageCount: messages?.length ?? 0, abortSignal: abortSignal?.aborted, hook: 'onInputStart', }) @@ -171,7 +185,7 @@ export const googleScholarTool = createTool({ log.info('Google Scholar tool received input chunk', { toolCallId, inputTextDelta, - messageCount: messages.length, + messageCount: messages?.length ?? 0, abortSignal: abortSignal?.aborted, hook: 'onInputDelta', }) @@ -179,7 +193,7 @@ export const googleScholarTool = createTool({ onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { log.info('Google Scholar received input', { toolCallId, - messageCount: messages.length, + messageCount: messages?.length ?? 0, inputData: { query: input.query, yearStart: input.yearStart, @@ -220,8 +234,11 @@ export const googleScholarTool = createTool({ await writer?.custom({ type: 'data-tool-progress', data: { + status: 'in-progress', message: `Searching Google Scholar for: ${input.query}`, + stage: 'googlescholar', }, + id: 'googlescholar', }) try { const params: Record = { @@ -271,6 +288,15 @@ export const googleScholarTool = createTool({ }, }) scholarSpan?.end() + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `✅ Google Scholar search complete: ${papers.length} papers`, + stage: 'googlescholar', + }, + id: 'googlescholar', + }) log.info('Google Scholar search completed', { query: input.query, paperCount: papers.length, @@ -289,9 +315,9 @@ export const googleScholarTool = createTool({ data: { status: 'done', message: `🛑 ${cancelMessage}`, - stage: 'serpapi-academic-local', + stage: 'googlescholar', }, - id: 'serpapi-academic-local', + id: 'googlescholar', }) log.warn(cancelMessage) @@ -320,10 +346,12 @@ export const googleScholarTool = createTool({ } }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + const paperCount = getGoogleScholarPaperCount(output) + log.info('Google Scholar search completed', { toolCallId, toolName, - outputData: { paperCount: output.papers.length }, + outputData: { paperCount }, abortSignal: abortSignal?.aborted, hook: 'onOutput', }) diff --git a/src/mastra/tools/serpapi-news-trends.tool.ts b/src/mastra/tools/serpapi-news-trends.tool.ts index 78495671..dffb1923 100644 --- a/src/mastra/tools/serpapi-news-trends.tool.ts +++ b/src/mastra/tools/serpapi-news-trends.tool.ts @@ -51,6 +51,42 @@ interface SerpApiSuggestResponse { suggestions?: Array<{ value: string }> } +type GoogleTrendsTimeRange = + | 'now-1-H' + | 'now-4-H' + | 'now-1-d' + | 'now-7-d' + | 'today-1-m' + | 'today-3-m' + | 'today-12-m' + | 'today-5-y' + +const GOOGLE_TRENDS_DATE_RANGE_MAP: Record = { + 'now-1-H': 'now 1-H', + 'now-4-H': 'now 4-H', + 'now-1-d': 'now 1-d', + 'now-7-d': 'now 7-d', + 'today-1-m': 'today 1-m', + 'today-3-m': 'today 3-m', + 'today-12-m': 'today 12-m', + 'today-5-y': 'today 5-y', +} + +/** + * Converts the tool's hyphenated time range enum into the SerpAPI date format. + * + * SerpAPI expects values like `today 12-m` and `now 7-d`, not the hyphenated + * internal enum values used by this tool's input schema. + * + * @param timeRange - Internal tool time range + * @returns The SerpAPI-compatible date string + */ +export function mapGoogleTrendsDateRange( + timeRange: GoogleTrendsTimeRange +): string { + return GOOGLE_TRENDS_DATE_RANGE_MAP[timeRange] +} + /** * Input schema for Google News */ @@ -564,7 +600,7 @@ export const googleTrendsTool = createTool({ params.geo = input.location } if (typeof input.timeRange === 'string' && input.timeRange.length > 0) { - params.date = input.timeRange + params.date = mapGoogleTrendsDateRange(input.timeRange) } if (typeof input.category === 'number') { @@ -661,6 +697,17 @@ export const googleTrendsTool = createTool({ throw new Error(`Google Trends search failed: ${errorMessage}`, { cause: error }) } }, + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + log.info('Google Trends analysis completed', { + toolCallId, + toolName, + dataPoints: output?.interestOverTime?.length ?? 0, + relatedQueries: output?.relatedQueries?.length ?? 0, + relatedTopics: output?.relatedTopics?.length ?? 0, + aborted: abortSignal?.aborted === true, + hook: 'onOutput', + }) + }, }) /** diff --git a/src/mastra/tools/stooq-stock-market-data.tool.ts b/src/mastra/tools/stooq-stock-market-data.tool.ts new file mode 100644 index 00000000..5f28726a --- /dev/null +++ b/src/mastra/tools/stooq-stock-market-data.tool.ts @@ -0,0 +1,277 @@ +import { SpanType, getOrCreateSpan } from '@mastra/core/observability' +import type { TracingContext } from '@mastra/core/observability' +import type { RequestContext } from '@mastra/core/request-context' +import { createTool, type InferUITool } from '@mastra/core/tools' +import { z } from 'zod' +import { log } from '../config/logger' +import { httpFetch } from '../lib/http-client' +import { buildStooqSymbol, parseStooqCsv } from './market-data.helpers' + +/** + * Shared request context for Stooq stock data. + */ +export interface StooqStockContext extends RequestContext { + userId?: string +} + +const stooqInputSchema = z.object({ + function: z.enum(['quote', 'history']).default('quote'), + symbol: z.string().min(1).describe('Stock ticker symbol, e.g. AAPL or MSFT'), + marketSuffix: z.string().default('us').describe('Stooq market suffix, e.g. us, uk, jp'), + limit: z.number().int().min(1).max(5000).default(100), +}) + +type StooqInput = z.infer + +type StooqQuoteData = { + symbol: string + date: string | null + time: string | null + open: number | null + high: number | null + low: number | null + close: number | null + volume: number | null + name: string | null +} + +type StooqHistoryData = { + symbol: string + candles: Array<{ + timestamp: string + open: number | null + high: number | null + low: number | null + close: number | null + volume: number | null + }> +} + +type StooqMarketDataData = StooqQuoteData | StooqHistoryData + +type StooqMarketDataOutput = { + data: StooqMarketDataData + metadata: { + source: 'stooq' + function: string + symbol: string + market: string + } +} + +/** + * Counts the number of rows in a Stooq payload. + * + * @param payload - The normalized Stooq payload. + * @returns A best-effort count for logging. + */ +function countStooqMarketDataItems(payload: StooqMarketDataOutput['data'] | undefined): number { + if (!payload) { + return 0 + } + + if ('candles' in payload) { + return payload.candles.length + } + + return 1 +} + +const STOOQ_BASE_URL = 'https://stooq.com' + +/** + * Stooq stock market-data tool. + */ +export const stooqStockQuotesTool = createTool({ + id: 'stooq-stock-quotes', + description: + 'Fetch free Stooq stock market data without an API key, including latest quotes and historical daily bars.', + inputSchema: stooqInputSchema, + outputSchema: z.custom(), + onInputStart: ({ toolCallId, messages, abortSignal }) => { + log.info('Stooq stock quotes input streaming started', { + toolCallId, + messageCount: messages?.length ?? 0, + abortSignal: abortSignal?.aborted, + hook: 'onInputStart', + }) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + log.info('Stooq stock quotes received input chunk', { + toolCallId, + inputTextDelta, + messageCount: messages?.length ?? 0, + abortSignal: abortSignal?.aborted, + hook: 'onInputDelta', + }) + }, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + log.info('Stooq stock quotes received input', { + toolCallId, + messageCount: messages?.length ?? 0, + function: input.function, + symbol: input.symbol, + abortSignal: abortSignal?.aborted, + hook: 'onInputAvailable', + }) + }, + execute: async (input, context) => { + const writer = context?.writer + const abortSignal = context?.abortSignal + const tracingContext: TracingContext | undefined = context?.tracingContext + const requestFunction = input.function ?? 'quote' + const limit = input.limit ?? 100 + const marketSuffix = input.marketSuffix ?? 'us' + const stooqSymbol = buildStooqSymbol(input.symbol, marketSuffix) + + if (abortSignal?.aborted === true) { + throw new Error('Stooq stock request cancelled') + } + + const span = getOrCreateSpan({ + type: SpanType.TOOL_CALL, + name: 'stooq-stock-quotes', + input, + metadata: { + 'tool.id': 'stooq-stock-quotes', + 'tool.input.function': requestFunction, + 'tool.input.symbol': stooqSymbol, + }, + requestContext: context?.requestContext, + tracingContext, + }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: `Fetching Stooq stock data for ${stooqSymbol}`, + stage: 'stooq-stock-quotes', + }, + id: 'stooq-stock-quotes', + }) + + try { + let data: StooqMarketDataData + + if (requestFunction === 'quote') { + const response = await httpFetch(`${STOOQ_BASE_URL}/q/l/`, { + timeout: 30000, + responseType: 'text', + params: { + s: stooqSymbol, + i: 'd', + f: 'sd2t2ohlcvn', + e: 'csv', + }, + }) + const rows = parseStooqCsv(await response.text()) + const row = rows[0] + if (!row) { + throw new Error(`No Stooq quote returned for ${stooqSymbol}`) + } + + data = { + symbol: row.Symbol ?? stooqSymbol, + date: row.Date ?? null, + time: row.Time ?? null, + open: row.Open ? Number(row.Open) : null, + high: row.High ? Number(row.High) : null, + low: row.Low ? Number(row.Low) : null, + close: row.Close ? Number(row.Close) : null, + volume: row.Volume ? Number(row.Volume) : null, + name: row.Name ?? null, + } + } else { + const response = await httpFetch(`${STOOQ_BASE_URL}/q/d/l/`, { + timeout: 30000, + responseType: 'text', + params: { + s: stooqSymbol, + i: 'd', + e: 'csv', + }, + }) + const rows = parseStooqCsv(await response.text()).slice(-limit) + data = { + symbol: stooqSymbol, + candles: rows.map((row) => ({ + timestamp: row.Date, + open: row.Open ? Number(row.Open) : null, + high: row.High ? Number(row.High) : null, + low: row.Low ? Number(row.Low) : null, + close: row.Close ? Number(row.Close) : null, + volume: row.Volume ? Number(row.Volume) : null, + })), + } + } + + const output: StooqMarketDataOutput = { + data, + metadata: { + source: 'stooq' as const, + function: requestFunction, + symbol: input.symbol, + market: marketSuffix, + }, + } + + span?.update({ + output, + metadata: { + 'tool.output.source': 'stooq', + }, + }) + span?.end() + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `✅ Stooq stock data ready for ${stooqSymbol}`, + stage: 'stooq-stock-quotes', + }, + id: 'stooq-stock-quotes', + }) + + return output + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + span?.error({ + error: error instanceof Error ? error : new Error(errorMessage), + endSpan: true, + }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `❌ Stooq stock request failed: ${errorMessage}`, + stage: 'stooq-stock-quotes', + }, + id: 'stooq-stock-quotes', + }) + + log.error('Stooq stock market-data tool failed', { + function: requestFunction, + symbol: stooqSymbol, + error: errorMessage, + }) + + throw error instanceof Error ? error : new Error(errorMessage) + } + }, + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + const data = output as StooqMarketDataOutput | undefined + log.info('Stooq stock quotes completed', { + toolCallId, + toolName, + count: countStooqMarketDataItems(data?.data), + abortSignal: abortSignal?.aborted, + hook: 'onOutput', + }) + }, +}) + +export type StooqStockQuotesInput = StooqInput +export type StooqStockQuotesUITool = InferUITool diff --git a/src/mastra/tools/tests/market-data.helpers.test.ts b/src/mastra/tools/tests/market-data.helpers.test.ts new file mode 100644 index 00000000..2fe744e0 --- /dev/null +++ b/src/mastra/tools/tests/market-data.helpers.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest' + +import { + buildBinanceSymbol, + buildCoinbaseProductId, + buildStooqSymbol, + normalizeBinanceKlines, + normalizeCoinbaseCandles, + normalizeYahooChartHistory, + parseStooqCsv, +} from '../market-data.helpers' + +describe('market data helpers', () => { + it('should build Binance symbols correctly', () => { + expect(buildBinanceSymbol('btc')).toBe('BTCUSDT') + expect(buildBinanceSymbol('BTCUSDT')).toBe('BTCUSDT') + expect(buildBinanceSymbol('eth', 'BTC')).toBe('ETHBTC') + }) + + it('should build Coinbase product ids correctly', () => { + expect(buildCoinbaseProductId('btc')).toBe('BTC-USD') + expect(buildCoinbaseProductId('eth', 'eur')).toBe('ETH-EUR') + }) + + it('should build Stooq symbols correctly', () => { + expect(buildStooqSymbol('AAPL')).toBe('aapl.us') + expect(buildStooqSymbol('msft', 'uk')).toBe('msft.uk') + expect(buildStooqSymbol('x.us')).toBe('x.us') + }) + + it('should parse Stooq CSV rows', () => { + const rows = parseStooqCsv('Symbol,Date,Open\nAAPL.US,2026-04-14,210.12') + expect(rows).toHaveLength(1) + expect(rows[0].Symbol).toBe('AAPL.US') + expect(rows[0].Date).toBe('2026-04-14') + }) + + it('should normalize Binance klines', () => { + const rows = normalizeBinanceKlines([ + [1713052800000, '100', '110', '95', '108', '1234', 1713052859999, '0', 0, '0', '0', '0'], + ]) + expect(rows).toHaveLength(1) + expect(rows[0].open).toBe(100) + expect(rows[0].close).toBe(108) + }) + + it('should normalize Coinbase candles', () => { + const rows = normalizeCoinbaseCandles([[1713052800, 95, 110, 100, 108, 1234]]) + expect(rows).toHaveLength(1) + expect(rows[0].open).toBe(100) + expect(rows[0].close).toBe(108) + }) + + it('should normalize Yahoo chart history rows', () => { + const rows = normalizeYahooChartHistory({ + chart: { + result: [ + { + timestamp: [1713052800], + indicators: { + quote: [ + { + open: [100], + high: [110], + low: [95], + close: [108], + volume: [1234], + }, + ], + }, + }, + ], + }, + }) + + expect(rows).toHaveLength(1) + expect(rows[0].open).toBe(100) + expect(rows[0].close).toBe(108) + }) +}) \ No newline at end of file diff --git a/src/mastra/tools/tests/serpapi-tools.test.ts b/src/mastra/tools/tests/serpapi-tools.test.ts new file mode 100644 index 00000000..cd56003b --- /dev/null +++ b/src/mastra/tools/tests/serpapi-tools.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' + +import { + getGoogleScholarPaperCount, +} from '../serpapi-academic-local.tool' +import { + mapGoogleTrendsDateRange, +} from '../serpapi-news-trends.tool' + +describe('SerpAPI tool helpers', () => { + describe('mapGoogleTrendsDateRange', () => { + it('should translate internal time ranges into SerpAPI date values', () => { + expect(mapGoogleTrendsDateRange('now-1-H')).toBe('now 1-H') + expect(mapGoogleTrendsDateRange('now-4-H')).toBe('now 4-H') + expect(mapGoogleTrendsDateRange('now-1-d')).toBe('now 1-d') + expect(mapGoogleTrendsDateRange('now-7-d')).toBe('now 7-d') + expect(mapGoogleTrendsDateRange('today-1-m')).toBe('today 1-m') + expect(mapGoogleTrendsDateRange('today-3-m')).toBe('today 3-m') + expect(mapGoogleTrendsDateRange('today-12-m')).toBe('today 12-m') + expect(mapGoogleTrendsDateRange('today-5-y')).toBe('today 5-y') + }) + }) + + describe('getGoogleScholarPaperCount', () => { + it('should return zero when the output is missing papers', () => { + expect(getGoogleScholarPaperCount()).toBe(0) + expect(getGoogleScholarPaperCount({})).toBe(0) + }) + + it('should return the number of papers when present', () => { + expect( + getGoogleScholarPaperCount({ + papers: [ + { title: 'One', link: 'https://example.com/one' }, + { title: 'Two', link: 'https://example.com/two' }, + ], + }) + ).toBe(2) + }) + }) +}) \ No newline at end of file diff --git a/src/mastra/tools/yahoo-finance-stock.tool.ts b/src/mastra/tools/yahoo-finance-stock.tool.ts new file mode 100644 index 00000000..35f02f99 --- /dev/null +++ b/src/mastra/tools/yahoo-finance-stock.tool.ts @@ -0,0 +1,335 @@ +import { SpanType, getOrCreateSpan } from '@mastra/core/observability' +import type { TracingContext } from '@mastra/core/observability' +import type { RequestContext } from '@mastra/core/request-context' +import { createTool, type InferUITool } from '@mastra/core/tools' +import { z } from 'zod' +import { log } from '../config/logger' +import { httpFetch } from '../lib/http-client' +import { + normalizeYahooChartHistory, + type YahooChartResponse, + type YahooQuote, +} from './market-data.helpers' + +/** + * Shared request context for Yahoo Finance stock data. + */ +export interface YahooFinanceStockContext extends RequestContext { + userId?: string +} + +const yahooRangeSchema = z.enum([ + '1d', + '5d', + '1mo', + '3mo', + '6mo', + '1y', + '2y', + '5y', + '10y', + 'ytd', + 'max', +]) + +const yahooIntervalSchema = z.enum([ + '1m', + '2m', + '5m', + '15m', + '30m', + '60m', + '90m', + '1d', + '5d', + '1wk', + '1mo', + '3mo', +]) + +const yahooInputSchema = z.object({ + function: z.enum(['quote', 'history']).default('quote'), + symbol: z.string().min(1).describe('Stock ticker symbol, e.g. AAPL or MSFT'), + range: yahooRangeSchema.default('1mo'), + interval: yahooIntervalSchema.default('1d'), + limit: z.number().int().min(1).max(5000).default(100), + includePrePost: z.boolean().default(false), + events: z.enum(['div,splits', 'div', 'splits']).default('div,splits'), +}) + +type YahooInput = z.infer + +type YahooQuoteData = { + symbol: string + name: string | null + currency: string | null + marketPrice: number | null + marketChange: number | null + marketChangePercent: number | null + marketCap: number | null + dayHigh: number | null + dayLow: number | null + regularMarketTime: number | null +} + +type YahooHistoryData = { + symbol: string + range: string + interval: string + candles: ReturnType + quote: YahooQuoteData | null +} + +type YahooMarketDataData = YahooQuoteData | YahooHistoryData + +type YahooMarketDataOutput = { + data: YahooMarketDataData + metadata: { + source: 'yahoo-finance' + function: string + symbol: string + market?: string + } +} + +/** + * Counts the number of rows in a Yahoo Finance payload. + * + * @param payload - The normalized Yahoo payload. + * @returns A best-effort count for logging. + */ +function countYahooMarketDataItems( + payload: YahooMarketDataOutput['data'] | undefined +): number { + if (!payload) { + return 0 + } + + if ('candles' in payload) { + return payload.candles.length + } + + return 1 +} + +const YAHOO_BASE_URL = 'https://query1.finance.yahoo.com' + +/** + * Yahoo Finance stock market-data tool. + */ +export const yahooFinanceStockQuotesTool = createTool({ + id: 'yahoo-finance-stock-quotes', + description: + 'Fetch free Yahoo Finance stock data without an API key, including latest quotes and chart history.', + inputSchema: yahooInputSchema, + outputSchema: z.custom(), + onInputStart: ({ toolCallId, messages, abortSignal }) => { + log.info('Yahoo Finance stock quotes input streaming started', { + toolCallId, + messageCount: messages?.length ?? 0, + abortSignal: abortSignal?.aborted, + hook: 'onInputStart', + }) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + log.info('Yahoo Finance stock quotes received input chunk', { + toolCallId, + inputTextDelta, + messageCount: messages?.length ?? 0, + abortSignal: abortSignal?.aborted, + hook: 'onInputDelta', + }) + }, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + log.info('Yahoo Finance stock quotes received input', { + toolCallId, + messageCount: messages?.length ?? 0, + function: input.function, + symbol: input.symbol, + abortSignal: abortSignal?.aborted, + hook: 'onInputAvailable', + }) + }, + execute: async (input, context) => { + const writer = context?.writer + const abortSignal = context?.abortSignal + const tracingContext: TracingContext | undefined = context?.tracingContext + const requestFunction = input.function ?? 'quote' + const range = input.range ?? '1mo' + const interval = input.interval ?? '1d' + const limit = input.limit ?? 100 + const includePrePost = input.includePrePost ?? false + const events = input.events ?? 'div,splits' + + if (abortSignal?.aborted === true) { + throw new Error('Yahoo Finance stock request cancelled') + } + + const span = getOrCreateSpan({ + type: SpanType.TOOL_CALL, + name: 'yahoo-finance-stock-quotes', + input, + metadata: { + 'tool.id': 'yahoo-finance-stock-quotes', + 'tool.input.function': requestFunction, + 'tool.input.symbol': input.symbol, + }, + requestContext: context?.requestContext, + tracingContext, + }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: `Fetching Yahoo Finance stock data for ${input.symbol}`, + stage: 'yahoo-finance-stock-quotes', + }, + id: 'yahoo-finance-stock-quotes', + }) + + try { + const quoteResponse = await httpFetch(`${YAHOO_BASE_URL}/v7/finance/quote`, { + timeout: 30000, + params: { symbols: input.symbol }, + headers: { + Accept: 'application/json', + 'User-Agent': 'Mozilla/5.0 (compatible; AgentStack/1.0)', + }, + }) + const quotePayload = (await quoteResponse.json()) as { + quoteResponse?: { + result?: YahooQuote[] + } + } + + let data: YahooMarketDataData + + if (requestFunction === 'quote') { + const row = quotePayload.quoteResponse?.result?.[0] + if (!row) { + throw new Error(`No Yahoo Finance quote returned for ${input.symbol}`) + } + + data = { + symbol: row.symbol ?? input.symbol, + name: row.shortName ?? row.longName ?? null, + currency: row.currency ?? null, + marketPrice: row.regularMarketPrice ?? null, + marketChange: row.regularMarketChange ?? null, + marketChangePercent: row.regularMarketChangePercent ?? null, + marketCap: row.marketCap ?? null, + dayHigh: row.regularMarketDayHigh ?? null, + dayLow: row.regularMarketDayLow ?? null, + regularMarketTime: row.regularMarketTime ?? null, + } + } else { + const chartResponse = await httpFetch( + `${YAHOO_BASE_URL}/v8/finance/chart/${encodeURIComponent(input.symbol)}`, + { + timeout: 30000, + params: { + range, + interval, + includePrePost: includePrePost ? 'true' : 'false', + events, + }, + headers: { + Accept: 'application/json', + 'User-Agent': 'Mozilla/5.0 (compatible; AgentStack/1.0)', + }, + } + ) + const chartPayload = (await chartResponse.json()) as YahooChartResponse + const row = quotePayload.quoteResponse?.result?.[0] + data = { + symbol: input.symbol, + range, + interval, + candles: normalizeYahooChartHistory(chartPayload).slice(-limit), + quote: row + ? { + symbol: row.symbol ?? input.symbol, + name: row.shortName ?? row.longName ?? null, + currency: row.currency ?? null, + marketPrice: row.regularMarketPrice ?? null, + marketChange: row.regularMarketChange ?? null, + marketChangePercent: row.regularMarketChangePercent ?? null, + marketCap: row.marketCap ?? null, + dayHigh: row.regularMarketDayHigh ?? null, + dayLow: row.regularMarketDayLow ?? null, + regularMarketTime: row.regularMarketTime ?? null, + } + : null, + } + } + + const output: YahooMarketDataOutput = { + data, + metadata: { + source: 'yahoo-finance' as const, + function: requestFunction, + symbol: input.symbol, + market: undefined, + }, + } + + span?.update({ + output, + metadata: { + 'tool.output.source': 'yahoo-finance', + }, + }) + span?.end() + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `✅ Yahoo Finance stock data ready for ${input.symbol}`, + stage: 'yahoo-finance-stock-quotes', + }, + id: 'yahoo-finance-stock-quotes', + }) + + return output + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + span?.error({ + error: error instanceof Error ? error : new Error(errorMessage), + endSpan: true, + }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `❌ Yahoo Finance request failed: ${errorMessage}`, + stage: 'yahoo-finance-stock-quotes', + }, + id: 'yahoo-finance-stock-quotes', + }) + + log.error('Yahoo Finance stock market-data tool failed', { + function: requestFunction, + symbol: input.symbol, + error: errorMessage, + }) + + throw error instanceof Error ? error : new Error(errorMessage) + } + }, + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + const data = output as YahooMarketDataOutput | undefined + log.info('Yahoo Finance stock quotes completed', { + toolCallId, + toolName, + count: countYahooMarketDataItems(data?.data), + abortSignal: abortSignal?.aborted, + hook: 'onOutput', + }) + }, +}) + +export type YahooFinanceStockQuotesInput = YahooInput +export type YahooFinanceStockQuotesUITool = InferUITool diff --git a/src/mastra/workspaces.ts b/src/mastra/workspaces.ts index 8c5a14ac..28e72540 100644 --- a/src/mastra/workspaces.ts +++ b/src/mastra/workspaces.ts @@ -32,6 +32,7 @@ import { import { libsqlvector } from './config/libsql' import { embed } from 'ai' import { ModelRouterEmbeddingModel } from '@mastra/core/llm' +import { VersionedSkillSource } from '@mastra/core/workspace' export const localWorkspacePath = process.env.WORKSPACE_PATH ?? './workspace' export const daytonaWorkspacePath = @@ -41,7 +42,7 @@ export const agentFsAgentId = process.env.AGENTFS_AGENT_ID ?? 'agentFs-db' export const sandboxPathEnv = process.env.PATH ?? '' export const sandboxNodeEnv = process.env.NODE_ENV -export const agentFsDbPath = process.env.AGENTFS_DB_PATH +export const agentFsDbPath = process.env.AGENTFS_DB_PATH ?? './agentfs-db' export const workspaceLifecycleState: Lifecycle = { status: 'pending', @@ -216,7 +217,7 @@ export const agentFsFilesystem = new AgentFSFilesystem( typeof agentFsDbPath === 'string' && agentFsDbPath.length > 0 ? { id: 'agentfs-filesystem', - path: './agentfs-db', + path: agentFsDbPath, } : { id: 'agentfs-filesystem', @@ -337,11 +338,12 @@ export const mainWorkspace = new Workspace({ }, }, }, - skills: workspaceSkillPaths, + skills: ['/skills'], bm25: { k1: 1.5, b: 0.75, }, + //skillSource: new VersionedSkillSource(versionTree, blobStore, versionCreatedAt), }) export const localFilesystemOnlyWorkspace = new Workspace({ @@ -450,7 +452,7 @@ export const agentFsWorkspace = new Workspace({ }, }), skills: workspaceSkillPaths, - vectorStore: libsqlvector, + vectorStore: libsqlvector, embedder: async (text: string) => { const { embedding } = await embed({ model: new ModelRouterEmbeddingModel( diff --git a/tests/test-results/test-results.json b/tests/test-results/test-results.json index 649a5307..c99e0215 100644 --- a/tests/test-results/test-results.json +++ b/tests/test-results/test-results.json @@ -1 +1 @@ -{"numTotalTestSuites":0,"numPassedTestSuites":0,"numFailedTestSuites":0,"numPendingTestSuites":0,"numTotalTests":0,"numPassedTests":0,"numFailedTests":0,"numPendingTests":0,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1772263249793,"success":false,"testResults":[]} \ No newline at end of file +{"numTotalTestSuites":2,"numPassedTestSuites":2,"numFailedTestSuites":0,"numPendingTestSuites":0,"numTotalTests":7,"numPassedTests":7,"numFailedTests":0,"numPendingTests":0,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1776174931337,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["market data helpers"],"fullName":"market data helpers should build Binance symbols correctly","status":"passed","title":"should build Binance symbols correctly","duration":1.8240000000000691,"failureMessages":[],"location":{"line":14,"column":5},"meta":{},"tags":[]},{"ancestorTitles":["market data helpers"],"fullName":"market data helpers should build Coinbase product ids correctly","status":"passed","title":"should build Coinbase product ids correctly","duration":0.2135000000000673,"failureMessages":[],"location":{"line":20,"column":5},"meta":{},"tags":[]},{"ancestorTitles":["market data helpers"],"fullName":"market data helpers should build Stooq symbols correctly","status":"passed","title":"should build Stooq symbols correctly","duration":0.25270000000000437,"failureMessages":[],"location":{"line":25,"column":5},"meta":{},"tags":[]},{"ancestorTitles":["market data helpers"],"fullName":"market data helpers should parse Stooq CSV rows","status":"passed","title":"should parse Stooq CSV rows","duration":2.8473999999998796,"failureMessages":[],"location":{"line":31,"column":5},"meta":{},"tags":[]},{"ancestorTitles":["market data helpers"],"fullName":"market data helpers should normalize Binance klines","status":"passed","title":"should normalize Binance klines","duration":0.5703000000000884,"failureMessages":[],"location":{"line":38,"column":5},"meta":{},"tags":[]},{"ancestorTitles":["market data helpers"],"fullName":"market data helpers should normalize Coinbase candles","status":"passed","title":"should normalize Coinbase candles","duration":0.4171999999998661,"failureMessages":[],"location":{"line":47,"column":5},"meta":{},"tags":[]},{"ancestorTitles":["market data helpers"],"fullName":"market data helpers should normalize Yahoo chart history rows","status":"passed","title":"should normalize Yahoo chart history rows","duration":0.41650000000004184,"failureMessages":[],"location":{"line":54,"column":5},"meta":{},"tags":[]}],"startTime":1776174932778,"endTime":1776174932784.4165,"status":"passed","message":"","name":"C:/Users/ssdsk/AgentStack/src/mastra/tools/tests/market-data.helpers.test.ts"}]} \ No newline at end of file