feat: implement playground app with AI SDK v6 tool execution

- Create new Next.js app at apps/playground for testing TPMJS tools
- Implement AI SDK v6 patterns with DefaultChatTransport and UIMessage format
- Create template tool package at packages/tools/hello with hello-world and hello-name tools
- Use tool() and jsonSchema() helpers to avoid Zod 4 conversion issues with OpenAI
- Add static tool loading system with switch statement (Next.js/webpack compatible)
- Implement chat interface with tool call visualization showing inputs/outputs
- Support multi-step tool execution with stepCountIs(5)
- Stream responses with toUIMessageStreamResponse() for full tool support
- Add sidebar showing available tools (static list)
- Use parts-based message rendering for text and tool calls
- Integrate firecrawl-aisdk tools (scrape, crawl, search)
- Add theme toggle in header (defaults to light mode)
- Fix responsive layout with max-width for message bubbles
- Use biome-ignore comments for legitimate any types in tool loading

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-12-04 02:51:02 +10:00
parent fcd6667357
commit 635fc96cac
33 changed files with 2817 additions and 5 deletions

522
OPENAI_SCHEMA_ERROR.md Normal file
View file

@ -0,0 +1,522 @@
# OpenAI Schema Validation Error - AI SDK v6
## ✅ RESOLVED
**Solution:** Use `tool()` and `jsonSchema()` from AI SDK instead of Zod for tool definitions.
## Error Message
```
Error [AI_APICallError]: Invalid schema for function 'helloWorld': schema must be a JSON Schema of 'type: "object"', got 'type: "None"'.
```
## Context
Building a Next.js playground app to test AI SDK v6 tool execution with OpenAI's GPT-4o-mini model. The error occurs when OpenAI validates the tool schema sent in the API request.
## Root Cause
Zod 4.0.0 generates JSON Schema with `allOf` + `$ref` at the root level instead of a direct `type: "object"`. OpenAI's API requires a JSON Schema with `type: "object"` at the root, so it rejects Zod 4 schemas with `type: "None"` error.
## Environment
- **AI SDK Version**: `ai@6.0.0-beta.124`
- **OpenAI Provider**: `@ai-sdk/openai@3.0.0-beta.74`
- **OpenAI Library**: `openai@^6.9.1`
- **Zod Version**: `zod@^4.0.0`
- **Next.js Version**: `next@^16.0.4`
- **Node.js**: Latest
- **TypeScript**: Strict mode enabled
## Tool Definition
Located at: `packages/tools/hello/src/index.ts`
```typescript
import { z } from 'zod';
/**
* Hello World Tool
* Returns a simple "Hello, World!" greeting
*
* This is a proper AI SDK v6 tool that can be used with streamText()
*/
export const helloWorldTool = {
description: 'Returns a simple "Hello, World!" greeting message',
parameters: z.object({
// OpenAI requires at least one optional parameter, can't be completely empty
includeTimestamp: z.boolean().optional().describe('Whether to include a timestamp in the response'),
}),
execute: async ({ includeTimestamp = true }: { includeTimestamp?: boolean }) => {
const response: any = {
message: 'Hello, World!',
};
if (includeTimestamp) {
response.timestamp = new Date().toISOString();
}
return response;
},
};
/**
* Hello Name Tool
* Returns a personalized greeting with the provided name
*
* This is a proper AI SDK v6 tool that can be used with streamText()
*/
export const helloNameTool = {
description: 'Returns a personalized greeting with the provided name',
parameters: z.object({
name: z.string().describe('The name of the person to greet'),
}),
execute: async ({ name }: { name: string }) => {
return {
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
};
},
};
```
## Tool Loading
Located at: `apps/playground/src/lib/tool-loader.ts`
```typescript
// Static imports for tools (required for Next.js/webpack)
import { helloWorldTool, helloNameTool } from '@tpmjs/hello';
import { scrapeTool, crawlTool, searchTool } from 'firecrawl-aisdk';
/**
* Load a specific TPMJS tool by package name
*/
export async function loadTpmjsTool(packageName: string): Promise<any> {
try {
// Map package names to their tool functions
switch (packageName) {
case '@tpmjs/hello':
// Hello has multiple tools, return all of them
return {
helloWorld: helloWorldTool,
helloName: helloNameTool,
};
case 'firecrawl-aisdk':
// Firecrawl has multiple tools, return all of them
return {
scrapeTool,
crawlTool,
searchTool,
};
default:
throw new Error(`Unknown tool package: ${packageName}`);
}
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to load tool from package ${packageName}: ${error.message}`);
}
throw new Error(`Failed to load tool from package ${packageName}: Unknown error`);
}
}
/**
* Load all installed TPMJS tools
*/
export async function loadAllTools(): Promise<Record<string, any>> {
const installedTools = ['@tpmjs/hello', 'firecrawl-aisdk'];
const tools: Record<string, any> = {};
for (const packageName of installedTools) {
try {
const tool = await loadTpmjsTool(packageName);
// If the tool returns an object with multiple tools (like firecrawl), spread them
if (tool && typeof tool === 'object' && !tool.description) {
Object.assign(tools, tool);
} else {
// Single tool - use a cleaned name (remove hyphens, camelCase)
const toolName = packageName.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase()).replace(/-/g, '');
tools[toolName] = tool;
}
} catch (error) {
console.error(`Failed to load tool ${packageName}:`, error);
// Continue loading other tools even if one fails
}
}
return tools;
}
```
## API Route
Located at: `apps/playground/src/app/api/chat/route.ts`
```typescript
import { loadAllTools } from '~/lib/tool-loader';
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { NextRequest } from 'next/server';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { messages } = body;
if (!messages || !Array.isArray(messages)) {
return new Response(JSON.stringify({ error: 'Invalid request: messages array required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// Load all available tools
const tools = await loadAllTools();
console.log('Loaded tools:', Object.keys(tools));
// Create system message
const systemMessage = {
role: 'system' as const,
content: `You are a helpful AI assistant that can use TPMJS tools to help users.
Available tools:
${Object.entries(tools)
.map(([name, tool]) => `- ${name}: ${tool.description}`)
.join('\n')}
Call tools as needed to answer user questions. Execute tools directly.`,
};
// Stream the AI response with tools
const result = streamText({
model: openai('gpt-4o-mini'),
messages: [systemMessage, ...messages],
tools,
maxSteps: 5,
});
return result.toTextStreamResponse();
} catch (error) {
console.error('Chat API error:', error);
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : 'Unknown error occurred',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
}
```
## Package Configuration
Located at: `packages/tools/hello/package.json`
```json
{
"name": "@tpmjs/hello",
"version": "0.0.1",
"private": true,
"description": "Example TPMJS tools - Hello World and Hello Name",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"type-check": "tsc --noEmit"
},
"keywords": [
"tpmjs-tool",
"ai-sdk",
"hello",
"example"
],
"tpmjs": {
"category": "text-analysis",
"description": "Simple greeting tools - Hello World and personalized Hello Name greetings"
},
"dependencies": {
"ai": "6.0.0-beta.124",
"zod": "^4.0.0"
},
"devDependencies": {
"@tpmjs/tsconfig": "workspace:*",
"typescript": "^5.9.3"
},
"files": [
"dist",
"README.md"
]
}
```
## TypeScript Configuration
Located at: `packages/tools/hello/tsconfig.json`
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
## Compiled Output
Located at: `packages/tools/hello/dist/index.js`
```javascript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.helloNameTool = exports.helloWorldTool = void 0;
const zod_1 = require("zod");
/**
* Hello World Tool
* Returns a simple "Hello, World!" greeting
*
* This is a proper AI SDK v6 tool that can be used with streamText()
*/
exports.helloWorldTool = {
description: 'Returns a simple "Hello, World!" greeting message',
parameters: zod_1.z.object({
// OpenAI requires at least one optional parameter, can't be completely empty
includeTimestamp: zod_1.z.boolean().optional().describe('Whether to include a timestamp in the response'),
}),
execute: async ({ includeTimestamp = true }) => {
const response = {
message: 'Hello, World!',
};
if (includeTimestamp) {
response.timestamp = new Date().toISOString();
}
return response;
},
};
/**
* Hello Name Tool
* Returns a personalized greeting with the provided name
*
* This is a proper AI SDK v6 tool that can be used with streamTime()
*/
exports.helloNameTool = {
description: 'Returns a personalized greeting with the provided name',
parameters: zod_1.z.object({
name: zod_1.z.string().describe('The name of the person to greet'),
}),
execute: async ({ name }) => {
return {
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
};
},
};
```
## Full Error Response from OpenAI
```json
{
"error": {
"message": "Invalid schema for function 'helloWorld': schema must be a JSON Schema of 'type: \"object\"', got 'type: \"None\"'.",
"type": "invalid_request_error",
"param": "tools[0].parameters",
"code": "invalid_function_parameters"
}
}
```
API endpoint: `https://api.openai.com/v1/responses`
Status code: 400
## Problem Analysis
1. **OpenAI expects JSON Schema format** - The `tools[0].parameters` field must be a valid JSON Schema object with `type: "object"`
2. **AI SDK v6 should convert Zod to JSON Schema** - The AI SDK is supposed to automatically convert Zod schemas to JSON Schema when sending to OpenAI, but it's producing `type: "None"` instead
3. **Potential causes**:
- Zod 4.0.0 compatibility issue with AI SDK v6 beta
- AI SDK not properly converting the Zod schema
- Issue with how the tool object is structured
- Problem with how tools are passed to `streamText()`
4. **Already tried**:
- Added at least one parameter (even optional) to helloWorldTool
- Used proper Zod schema with `.describe()` for descriptions
- Followed AI SDK v6 tool definition format exactly
- Built the package successfully (dist folder exists)
## AI SDK v6 Tool Format Reference
According to AI SDK v6 documentation, a tool should be defined as:
```typescript
{
description: string,
parameters: ZodSchema,
execute: async (args) => Promise<any>
}
```
This matches our implementation exactly.
## Questions for ChatGPT
1. Is there a known compatibility issue between AI SDK v6 Beta (6.0.0-beta.124) and Zod 4.0.0?
2. Does the AI SDK v6 require a specific tool registration format when passing to `streamText()`?
3. Should tools be wrapped in a different structure (e.g., using `tool()` helper function)?
4. Is there a way to manually convert Zod schema to JSON Schema that OpenAI accepts?
5. Are there any known issues with using workspace packages (`@tpmjs/hello`) in Next.js API routes with dynamic imports?
6. Should we downgrade to Zod 3.x instead of Zod 4.0.0?
7. Is there a debug mode to see what JSON Schema is being sent to OpenAI?
## Additional Context
- The `firecrawl-aisdk` package works correctly with the same setup
- Build process completes successfully with no TypeScript errors
- The tool is being loaded and passed to `streamText()` correctly
- Error only occurs when OpenAI validates the tool schema
- This is a monorepo using pnpm workspaces and Turborepo
## Related Files
- Tool definition: `packages/tools/hello/src/index.ts`
- Tool loader: `apps/playground/src/lib/tool-loader.ts`
- API route: `apps/playground/src/app/api/chat/route.ts`
- Package config: `packages/tools/hello/package.json`
- Compiled output: `packages/tools/hello/dist/index.js`
## Expected Behavior
Tools should be automatically converted from Zod schema to JSON Schema by AI SDK v6 and accepted by OpenAI's API.
## Actual Behavior
OpenAI rejects the tool schema with error: `got 'type: "None"'` instead of a valid JSON Schema object.
---
## ✅ SOLUTION IMPLEMENTED
### What We Changed
Instead of using Zod schemas with `parameters`, we now use AI SDK's `tool()` helper with `jsonSchema()` for the input schema. This bypasses Zod's JSON Schema conversion entirely.
### Before (Broken with Zod 4)
```typescript
import { z } from 'zod';
export const helloWorldTool = {
description: 'Returns a simple "Hello, World!" greeting message',
parameters: z.object({
includeTimestamp: z.boolean().optional().describe('Whether to include a timestamp'),
}),
execute: async ({ includeTimestamp = true }) => {
// ...
},
};
```
### After (Working with jsonSchema)
```typescript
import { jsonSchema, tool } from 'ai';
type HelloWorldInput = {
includeTimestamp?: boolean;
};
export const helloWorldTool = tool({
description: 'Returns a simple "Hello, World!" greeting message',
inputSchema: jsonSchema<HelloWorldInput>({
type: 'object',
properties: {
includeTimestamp: {
type: 'boolean',
description: 'Whether to include a timestamp in the response',
},
},
additionalProperties: false,
}),
async execute({ includeTimestamp = true }) {
const response: any = {
message: 'Hello, World!',
};
if (includeTimestamp) {
response.timestamp = new Date().toISOString();
}
return response;
},
});
```
### Key Changes
1. **Import from `ai`**: Added `jsonSchema` and `tool` imports
2. **Define TypeScript types**: Created `HelloWorldInput` type for type safety
3. **Use `tool()` wrapper**: Wraps the entire tool definition
4. **Use `jsonSchema()` for schema**: Provides explicit JSON Schema with `type: "object"` at root
5. **Removed Zod dependency**: No longer need `zod` in package.json
### Benefits
- ✅ Works with OpenAI's strict schema validation
- ✅ Explicit control over JSON Schema structure
- ✅ Full TypeScript type safety with generic types
- ✅ No dependency on Zod (one less package to maintain)
- ✅ Follows AI SDK v6 best practices
- ✅ Guaranteed `type: "object"` at root level
### Updated Package Dependencies
```json
{
"dependencies": {
"ai": "6.0.0-beta.124"
}
}
```
Zod is no longer needed in tool packages that use `jsonSchema()`.
### References
- [AI SDK Core: tool](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool)
- [AI SDK Core: jsonSchema](https://ai-sdk.dev/docs/reference/ai-sdk-core/json-schema)
- [GitHub Issue: Zod 4 JSON Schema compatibility](https://github.com/vercel/ai/issues/10240)

363
STREAMING_EMPTY_RESPONSE.md Normal file
View file

@ -0,0 +1,363 @@
# AI SDK v6 Streaming Empty Response Issue
## Problem
Using AI SDK v6 Beta with OpenAI and `streamText()`, the API route returns a 200 OK response, but the streamed response body is **completely empty** when tools are involved.
- **Normal chat** (without tool calls): Works fine, streams text back
- **Tool calls** (when user asks "say hello world"): Returns empty response body, no error messages
## Environment
- **AI SDK Version**: `ai@6.0.0-beta.124`
- **OpenAI Provider**: `@ai-sdk/openai@3.0.0-beta.74`
- **OpenAI Library**: `openai@^6.9.1`
- **Next.js Version**: `next@^16.0.4` (App Router)
- **Runtime**: Node.js (`runtime = 'nodejs'`)
- **Model**: `gpt-4o-mini`
## API Route Implementation
Located at: `apps/playground/src/app/api/chat/route.ts`
```typescript
import { createOpenAI } from '@ai-sdk/openai';
import { streamText, type CoreMessage, tool, jsonSchema } from 'ai';
import { type NextRequest } from 'next/server';
import { z } from 'zod';
import { env } from '~/env';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
// Initialize OpenAI provider
const openai = createOpenAI({
apiKey: env.OPENAI_API_KEY,
});
// Request schema
const RequestSchema = z.object({
messages: z.array(
z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z.string(),
})
),
});
// Simple inline test tool to verify streaming works
const testHelloTool = tool({
description: 'Returns a simple hello world greeting',
inputSchema: jsonSchema<{ includeTimestamp?: boolean }>({
type: 'object',
properties: {
includeTimestamp: {
type: 'boolean',
description: 'Whether to include a timestamp',
},
},
additionalProperties: false,
}),
async execute({ includeTimestamp = true }) {
const response: any = { message: 'Hello, World!' };
if (includeTimestamp) {
response.timestamp = new Date().toISOString();
}
return response;
},
});
/**
* POST /api/chat
* Chat with AI agent that can execute TPMJS tools
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { messages } = RequestSchema.parse(body);
// Use simple inline tool for testing
const tools = {
testHello: testHelloTool,
};
// Create system prompt listing available tools
const toolsList = Object.keys(tools)
.map((name) => `- ${name}: ${tools[name]?.description}`)
.join('\n');
const systemMessage: CoreMessage = {
role: 'system',
content: `You are a helpful AI assistant that can use TPMJS tools to help users.
Available tools:
${toolsList}
Call tools as needed to answer user questions. When a user asks to say hello world or for a greeting, use the testHello tool.`,
};
// Stream the response
const result = streamText({
model: openai('gpt-4o-mini'),
messages: [systemMessage, ...messages],
tools,
});
// Return the stream as SSE
return result.toTextStreamResponse();
} catch (error) {
console.error('Chat API error:', error);
if (error instanceof z.ZodError) {
return new Response(
JSON.stringify({
success: false,
error: 'Invalid request format',
details: error.issues,
}),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
);
}
return new Response(
JSON.stringify({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
}
```
## Client-Side Hook
Located at: `apps/playground/src/hooks/useChat.ts`
```typescript
'use client';
import { useCallback, useState } from 'react';
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
}
export function useChat() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const sendMessage = useCallback(async (content: string) => {
if (!content.trim()) return;
// Add user message immediately
const userMessage: ChatMessage = {
id: crypto.randomUUID(),
role: 'user',
content,
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setIsLoading(true);
setError(null);
try {
// Create assistant message placeholder
const assistantMessageId = crypto.randomUUID();
const assistantMessage: ChatMessage = {
id: assistantMessageId,
role: 'assistant',
content: '',
timestamp: new Date(),
};
setMessages((prev) => [...prev, assistantMessage]);
// Send request to API
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: [...messages, userMessage].map((m) => ({
role: m.role,
content: m.content,
})),
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Read the streaming response
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
throw new Error('Response body is null');
}
let accumulatedContent = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
// Decode the chunk
const chunk = decoder.decode(value, { stream: true });
accumulatedContent += chunk;
// Update the assistant message with accumulated content
setMessages((prev) =>
prev.map((m) =>
m.id === assistantMessageId
? { ...m, content: accumulatedContent }
: m
)
);
}
} catch (err) {
console.error('Error sending message:', err);
setError(err instanceof Error ? err.message : 'Failed to send message');
} finally {
setIsLoading(false);
}
}, [messages]);
const clearChat = useCallback(() => {
setMessages([]);
setError(null);
}, []);
return {
messages,
isLoading,
error,
sendMessage,
clearChat,
};
}
```
## Observed Behavior
### Working Case (Normal Chat)
- User types: "hi"
- API response: 200 OK
- Response body: Streams text chunks successfully
- UI shows: "Hi! How can I help you today?"
### Broken Case (Tool Call)
- User types: "say hello world"
- API response: 200 OK ✅
- Response body: **EMPTY** ❌ (no chunks, no data, nothing)
- UI shows: Empty message bubble
- Console: No errors logged
## HTTP Response Details
```
Request Method: POST
Status Code: 200 OK
URL: http://localhost:3001/api/chat
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
```
The response headers look correct for a streaming response, but the body is completely empty.
## What We've Tried
1. ✅ Fixed OpenAI schema validation error (was `type: "None"`, now uses proper JSON Schema)
2. ✅ Using `tool()` and `jsonSchema()` from AI SDK
3. ✅ Simplified to a single inline test tool
4. ✅ Tool executes without errors (no schema validation issues)
5. ✅ Normal chat works fine (proves streaming infrastructure is correct)
## Questions
1. **Is `toTextStreamResponse()` the correct method for streaming with tools in AI SDK v6?**
- Should we use a different method like `toDataStreamResponse()` for tool calls?
2. **Are we constructing the messages array correctly?**
- We're sending `{ role: 'user' | 'assistant' | 'system', content: string }[]`
- Do we need to include tool call messages or tool result messages?
3. **Does AI SDK v6 require a specific message format for tool calls?**
- Should we be including `toolInvocations` or `tool_calls` in the message history?
- Are we missing required fields in the `CoreMessage` type?
4. **Is the client-side streaming reader correct?**
- We're reading chunks with `response.body.getReader()`
- Should we be parsing SSE events differently for tool calls?
5. **Does `streamText()` with tools require `maxSteps` parameter?**
- Do we need to set `maxSteps: 5` to allow multi-step reasoning?
6. **Are we handling the conversation history correctly?**
- We're sending all previous messages on each request
- Should we be including assistant messages with tool call results?
## AI SDK v6 Documentation References
We're following these patterns from the official docs:
- [streamText() API](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text)
- [tool() API](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool)
- [Tool Calling Guide](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling)
But we might be missing something specific about:
- How to handle streaming when tools are executed
- What response format tool calls produce
- How to parse the stream when tools are involved
## Suspected Issue
**The message format might be wrong.** We're sending:
```typescript
const systemMessage: CoreMessage = {
role: 'system',
content: `You are a helpful AI assistant...`,
};
const result = streamText({
model: openai('gpt-4o-mini'),
messages: [systemMessage, ...messages],
tools,
});
```
But `CoreMessage` might need additional fields when tools are involved, or we might need to handle tool call results differently in the conversation history.
## What We Need
1. Correct message format for `streamText()` with tools
2. How to properly stream responses that include tool calls
3. Whether we need different client-side parsing for tool call streams
4. Example of a working Next.js API route using AI SDK v6 with `streamText()` and tools
## Repo Context
- Monorepo using Turborepo + pnpm workspaces
- Next.js 16 App Router with Turbopack
- TypeScript strict mode
- All UI components from internal `@tpmjs/ui` package
- Tools are imported from workspace package `@tpmjs/hello`

400
USECHAT_INPUT_UNDEFINED.md Normal file
View file

@ -0,0 +1,400 @@
# useChat Hook Returns Undefined Input Property
## Problem
Using `@ai-sdk/react`'s `useChat` hook, the `input` property is returning `undefined`, causing the application to crash when trying to call `.trim()` on it.
## Error
```
TypeError: Cannot read properties of undefined (reading 'trim')
at ChatInput (src/components/chat/ChatInput.tsx:41:48)
```
**Code that fails:**
```typescript
<Button type="submit" disabled={!input.trim() || isLoading} loading={isLoading} size="lg">
```
## Environment
- **AI SDK Version**: `ai@6.0.0-beta.124`
- **AI SDK React**: `@ai-sdk/react` (latest version installed via pnpm)
- **OpenAI Provider**: `@ai-sdk/openai@3.0.0-beta.74`
- **Next.js**: `16.0.4` (App Router with Turbopack)
- **React**: `19.0.0`
- **Zod**: `4.0.0` (required, not downgrading)
- **TypeScript**: `5.9.3`
## Current Implementation
### Custom useChat Hook Wrapper
Located at: `apps/playground/src/hooks/useChat.ts`
```typescript
'use client';
import { useChat as useAISDKChat } from '@ai-sdk/react';
/**
* Custom chat hook that wraps the official @ai-sdk/react useChat
* Handles SSE streaming with tool calls and UI message protocol
*/
export function useChat() {
const chat = useAISDKChat({
api: '/api/chat',
});
return {
messages: chat.messages,
input: chat.input,
isLoading: chat.isLoading,
error: chat.error,
handleInputChange: chat.handleInputChange,
handleSubmit: chat.handleSubmit,
setInput: chat.setInput,
reload: chat.reload,
stop: chat.stop,
};
}
```
### API Route
Located at: `apps/playground/src/app/api/chat/route.ts`
```typescript
import { createOpenAI } from '@ai-sdk/openai';
import { streamText, convertToModelMessages, stepCountIs, type UIMessage } from 'ai';
import { type NextRequest } from 'next/server';
import { env } from '~/env';
import { loadAllTools } from '~/lib/tool-loader';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
// Initialize OpenAI provider
const openai = createOpenAI({
apiKey: env.OPENAI_API_KEY,
});
/**
* POST /api/chat
* Chat with AI agent that can execute TPMJS tools
*/
export async function POST(request: NextRequest) {
try {
const { messages }: { messages: UIMessage[] } = await request.json();
// Load all available TPMJS tools
const tools = await loadAllTools();
// Create system prompt listing available tools
const toolsList = Object.keys(tools)
.map((name) => `- ${name}: ${tools[name]?.description}`)
.join('\n');
const system = `You are a helpful AI assistant that can use TPMJS tools to help users.
Available tools:
${toolsList}
When you use a tool, you MUST always follow up with a natural language answer to the user summarizing the result.`;
// Stream the response with multi-step tool usage enabled
const result = streamText({
model: openai('gpt-4o-mini'),
system,
messages: convertToModelMessages(messages),
tools,
stopWhen: stepCountIs(5), // Allow model to call tools AND generate text response
});
// Return UI message stream with tool calls and text
return result.toUIMessageStreamResponse();
} catch (error) {
console.error('Chat API error:', error);
return new Response(
JSON.stringify({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
}
```
### Component Using the Hook
Located at: `apps/playground/src/components/chat/ChatInterface.tsx`
```typescript
'use client';
import { useChat } from '~/hooks/useChat';
import { ChatInput } from './ChatInput';
import { ChatMessages } from './ChatMessages';
export function ChatInterface(): React.ReactElement {
const { messages, input, isLoading, handleInputChange, handleSubmit, setInput } = useChat();
return (
<div className="flex flex-1 flex-col">
<ChatMessages messages={messages} />
<ChatInput
input={input}
isLoading={isLoading}
onInputChange={handleInputChange}
onSubmit={handleSubmit}
setInput={setInput}
/>
</div>
);
}
```
### ChatInput Component
Located at: `apps/playground/src/components/chat/ChatInput.tsx`
```typescript
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import type { FormEvent } from 'react';
interface ChatInputProps {
input: string;
isLoading: boolean;
onInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onSubmit: (e: FormEvent<HTMLFormElement>) => void;
setInput: (value: string) => void;
}
export function ChatInput({ input, isLoading, onInputChange, onSubmit }: ChatInputProps): React.ReactElement {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (input.trim() && !isLoading) {
// Trigger form submission
const form = e.currentTarget.form;
if (form) {
form.requestSubmit();
}
}
}
};
return (
<form onSubmit={onSubmit} className="border-t border-border bg-background p-4">
<div className="mx-auto flex max-w-4xl gap-2">
<Textarea
value={input}
onChange={onInputChange}
onKeyDown={handleKeyDown}
placeholder="Ask me to tell you a fish joke..."
className="min-h-[60px] flex-1 resize-none"
disabled={isLoading}
rows={3}
/>
<Button type="submit" disabled={!input.trim() || isLoading} loading={isLoading} size="lg">
Send
</Button>
</div>
</form>
);
}
```
## Tool Definition (Working)
The tools are defined using `tool()` and `jsonSchema()` from AI SDK to avoid Zod 4 compatibility issues:
Located at: `packages/tools/hello/src/index.ts`
```typescript
import { jsonSchema, tool } from 'ai';
type HelloWorldInput = {
includeTimestamp?: boolean;
};
export const helloWorldTool = tool({
description: 'Returns a simple "Hello, World!" greeting message',
inputSchema: jsonSchema<HelloWorldInput>({
type: 'object',
properties: {
includeTimestamp: {
type: 'boolean',
description: 'Whether to include a timestamp in the response',
},
},
additionalProperties: false,
}),
async execute({ includeTimestamp = true }) {
const response: any = {
message: 'Hello, World!',
};
if (includeTimestamp) {
response.timestamp = new Date().toISOString();
}
return response;
},
});
type HelloNameInput = {
name: string;
};
export const helloNameTool = tool({
description: 'Returns a personalized greeting with the provided name',
inputSchema: jsonSchema<HelloNameInput>({
type: 'object',
properties: {
name: {
type: 'string',
description: 'The name of the person to greet',
},
},
required: ['name'],
additionalProperties: false,
}),
async execute({ name }) {
return {
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
};
},
});
```
## What's Working
1. ✅ API route receives requests correctly
2. ✅ Tools are loaded and registered successfully
3. ✅ `streamText()` with `stopWhen: stepCountIs(5)` configured
4. ✅ `toUIMessageStreamResponse()` returns proper SSE stream
5. ✅ curl test shows tools are called correctly with proper JSON Schema
6. ✅ Stream format includes `tool-input-start`, `tool-output-available`, `text-delta` events
## What's NOT Working
1. ❌ `input` property from `useChat` is `undefined`
2. ❌ Application crashes when trying to access `input.trim()`
3. ❌ Can't type in the chat input field
## curl Test (Successful)
```bash
curl -N http://localhost:3001/api/chat \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"say hello thomas"}]}'
```
**Response:**
```
data: {"type":"start"}
data: {"type":"start-step"}
data: {"type":"tool-input-start","toolCallId":"call_...","toolName":"helloName"}
data: {"type":"tool-input-delta","toolCallId":"call_...","inputTextDelta":"{\""}
data: {"type":"tool-input-delta","toolCallId":"call_...","inputTextDelta":"name"}
data: {"type":"tool-input-delta","toolCallId":"call_...","inputTextDelta":"\":\""}
data: {"type":"tool-input-delta","toolCallId":"call_...","inputTextDelta":"Thomas"}
data: {"type":"tool-input-delta","toolCallId":"call_...","inputTextDelta":"\"}"}
data: {"type":"tool-input-available","toolCallId":"call_...","toolName":"helloName","input":{"name":"Thomas"},"providerMetadata":{...}}
data: {"type":"tool-output-available","toolCallId":"call_...","output":{"message":"Hello, Thomas!","timestamp":"2025-12-03T16:07:31.366Z"}}
data: {"type":"finish-step"}
data: {"type":"start-step"}
data: {"type":"text-start","id":"msg_...","providerMetadata":{...}}
data: {"type":"text-delta","id":"msg_...","delta":"Hello"}
data: {"type":"text-delta","id":"msg_...","delta":","}
data: {"type":"text-delta","id":"msg_...","delta":" Thomas"}
data: {"type":"text-delta","id":"msg_...","delta":"!"}
data: {"type":"text-end","id":"msg_...","providerMetadata":{...}}
data: {"type":"finish-step"}
data: {"type":"finish","finishReason":"stop"}
data: [DONE]
```
The API works perfectly - tools are called, results are returned, text is generated. The issue is purely on the React client side.
## Questions
1. **Is `@ai-sdk/react`'s `useChat` compatible with AI SDK v6 Beta (6.0.0-beta.124)?**
- Should we be using a different version of `@ai-sdk/react`?
- Are there known compatibility issues with AI SDK v6 Beta?
2. **Why is `input` undefined?**
- Does `useChat` require specific initialization options?
- Do we need to provide `initialMessages` or `initialInput`?
- Is there a required prop we're missing?
3. **Is the API route format correct for `@ai-sdk/react`'s `useChat`?**
- Should the API accept a different request format?
- Is `UIMessage[]` the correct type for messages?
- Should we use `toDataStreamResponse()` instead of `toUIMessageStreamResponse()`?
4. **Do we need to handle client-side state differently?**
- Should we initialize `input` with a default value?
- Is there a provider or context missing?
- Do we need to wrap the component tree with any providers?
5. **Is there a version mismatch between packages?**
- `ai@6.0.0-beta.124`
- `@ai-sdk/openai@3.0.0-beta.74`
- `@ai-sdk/react@?` (unknown version)
6. **Does Zod 4 affect the client-side hook?**
- We fixed the server-side tool schemas using `jsonSchema()`
- Could there be client-side Zod 4 issues affecting `useChat`?
## Expected Behavior
The `useChat` hook should return:
- `input: string` - Current input value (should be empty string initially)
- `handleInputChange: (e) => void` - Update input value
- `handleSubmit: (e) => void` - Submit form and send message
- `messages: Message[]` - Array of messages
- `isLoading: boolean` - Loading state
## Actual Behavior
- `input: undefined`
- Everything else appears to be defined
- Crash on first render when trying to access `input.trim()`
## Monorepo Context
- Turborepo monorepo with pnpm workspaces
- Next.js 16 App Router with Turbopack
- TypeScript strict mode
- `@tpmjs/ui` package for UI components
- `@tpmjs/hello` package for tools
- Using workspace protocol (`workspace:*`) for internal dependencies
## Related Documentation
- [AI SDK Core: streamText](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text)
- [AI SDK React: useChat](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat)
- [AI SDK UI: Stream Protocol](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol)
## What We Need
1. Correct version compatibility information for AI SDK v6 Beta + @ai-sdk/react
2. Why `input` is undefined and how to fix it
3. Whether our API route format is correct for the React hook
4. Any missing initialization or configuration for `useChat`
5. Whether there are alternative approaches (custom SSE parsing, different hook, etc.)
We must keep Zod 4 and cannot downgrade. The server-side tools are working correctly with `jsonSchema()` workaround.

View file

@ -0,0 +1,2 @@
# Required: OpenAI API key
OPENAI_API_KEY=sk-...

6
apps/playground/next-env.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -0,0 +1,8 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: ['@tpmjs/ui', '@tpmjs/utils', '@tpmjs/types', '@tpmjs/env'],
reactStrictMode: true,
};
export default nextConfig;

View file

@ -0,0 +1,46 @@
{
"name": "@tpmjs/playground",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev --port 3001",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"type-check": "tsc --noEmit",
"clean": "rm -rf .next .turbo"
},
"dependencies": {
"@ai-sdk/openai": "3.0.0-beta.74",
"@ai-sdk/react": "^2.0.106",
"@tpmjs/env": "workspace:*",
"@tpmjs/hello": "workspace:*",
"@tpmjs/types": "workspace:*",
"@tpmjs/ui": "workspace:*",
"@tpmjs/utils": "workspace:*",
"ai": "6.0.0-beta.124",
"firecrawl-aisdk": "^0.7.2",
"fish-joke-generator": "^1.1.0",
"next": "^16.0.4",
"next-themes": "^0.4.6",
"openai": "^6.9.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@tpmjs/eslint-config": "workspace:*",
"@tpmjs/tailwind-config": "workspace:*",
"@tpmjs/tsconfig": "workspace:*",
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.4",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View file

@ -0,0 +1,70 @@
import { createOpenAI } from '@ai-sdk/openai';
import { type UIMessage, convertToModelMessages, stepCountIs, streamText } from 'ai';
import type { NextRequest } from 'next/server';
import { env } from '~/env';
import { loadAllTools } from '~/lib/tool-loader';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
// Initialize OpenAI provider
const openai = createOpenAI({
apiKey: env.OPENAI_API_KEY,
});
/**
* POST /api/chat
* Chat with AI agent that can execute TPMJS tools
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
console.log('Request body:', JSON.stringify(body, null, 2));
const messages: UIMessage[] = body.messages || [];
// Load all available TPMJS tools
const tools = await loadAllTools();
// Create system prompt listing available tools
const toolsList = Object.keys(tools)
.map((name) => {
const tool = tools[name] as { description?: string } | undefined;
return `- ${name}: ${tool?.description || 'No description'}`;
})
.join('\n');
const system = `You are a helpful AI assistant that can use TPMJS tools to help users.
Available tools:
${toolsList}
When you use a tool, you MUST always follow up with a natural language answer to the user summarizing the result.`;
// Stream the response with multi-step tool usage enabled
const result = streamText({
model: openai('gpt-4o-mini'),
system,
messages: convertToModelMessages(messages),
tools,
stopWhen: stepCountIs(5), // Allow model to call tools AND generate text response
});
// Return UI message stream with tool calls and text
return result.toUIMessageStreamResponse();
} catch (error) {
console.error('Chat API error:', error);
return new Response(
JSON.stringify({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
}

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,42 @@
import type { Metadata } from 'next';
import { ThemeProvider } from 'next-themes';
import { Space_Grotesk, Space_Mono } from 'next/font/google';
import './globals.css';
const spaceGrotesk = Space_Grotesk({
subsets: ['latin'],
variable: '--font-sans',
display: 'swap',
});
const spaceMono = Space_Mono({
subsets: ['latin'],
weight: ['400', '700'],
variable: '--font-mono',
display: 'swap',
});
export const metadata: Metadata = {
title: 'TPMJS Playground - Test AI Tools',
description: 'Interactive playground for testing TPMJS tools with AI agents',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}): React.ReactElement {
return (
<html
lang="en"
suppressHydrationWarning
className={`${spaceGrotesk.variable} ${spaceMono.variable}`}
>
<body>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
{children}
</ThemeProvider>
</body>
</html>
);
}

View file

@ -0,0 +1,22 @@
'use client';
import { ChatHeader } from '~/components/chat/ChatHeader';
import { ChatInterface } from '~/components/chat/ChatInterface';
import { ToolsSidebar } from '~/components/sidebar/ToolsSidebar';
export default function PlaygroundPage(): React.ReactElement {
const handleClearChat = () => {
// Refresh the page to clear chat
window.location.reload();
};
return (
<div className="flex h-screen flex-col bg-background">
<ChatHeader onClear={handleClearChat} />
<div className="flex flex-1 overflow-hidden">
<ToolsSidebar />
<ChatInterface />
</div>
</div>
);
}

View file

@ -0,0 +1,52 @@
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import { useTheme } from 'next-themes';
import Link from 'next/link';
import { useEffect, useState } from 'react';
interface ChatHeaderProps {
onClear: () => void;
}
export function ChatHeader({ onClear }: ChatHeaderProps): React.ReactElement {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
// Avoid hydration mismatch
useEffect(() => {
setMounted(true);
}, []);
const toggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
};
return (
<header className="border-b border-border bg-background px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<h1 className="text-xl font-bold">TPMJS Playground</h1>
<Link
href="https://tpmjs.com"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-foreground-secondary hover:text-foreground"
>
View Registry
</Link>
</div>
<div className="flex items-center gap-2">
{mounted && (
<Button variant="ghost" onClick={toggleTheme} size="md">
{theme === 'dark' ? '☀️' : '🌙'}
</Button>
)}
<Button variant="ghost" onClick={onClear} size="md">
Clear Chat
</Button>
</div>
</div>
</header>
);
}

View file

@ -0,0 +1,52 @@
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import type { FormEvent } from 'react';
interface ChatInputProps {
input: string;
isLoading: boolean;
onInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onSubmit: (e: FormEvent<HTMLFormElement>) => void;
setInput: (value: string) => void;
}
export function ChatInput({
input,
isLoading,
onInputChange,
onSubmit,
}: ChatInputProps): React.ReactElement {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (input.trim() && !isLoading) {
// Trigger form submission
const form = e.currentTarget.form;
if (form) {
form.requestSubmit();
}
}
}
};
return (
<form onSubmit={onSubmit} className="border-t border-border bg-background p-4">
<div className="mx-auto flex max-w-4xl gap-2">
<Textarea
value={input}
onChange={onInputChange}
onKeyDown={handleKeyDown}
placeholder="Ask me to tell you a fish joke..."
className="min-h-[60px] flex-1 resize-none"
disabled={isLoading}
rows={3}
/>
<Button type="submit" disabled={!input.trim() || isLoading} loading={isLoading} size="lg">
Send
</Button>
</div>
</form>
);
}

View file

@ -0,0 +1,38 @@
'use client';
import { useState } from 'react';
import { useChat } from '~/hooks/useChat';
import { ChatInput } from './ChatInput';
import { ChatMessages } from './ChatMessages';
export function ChatInterface(): React.ReactElement {
const { messages, sendMessage, status } = useChat();
const [input, setInput] = useState('');
const isLoading = status !== 'ready';
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (input.trim() && status === 'ready') {
sendMessage({ text: input });
setInput('');
}
};
return (
<div className="flex flex-1 flex-col">
<ChatMessages messages={messages} />
<ChatInput
input={input}
isLoading={isLoading}
onInputChange={handleInputChange}
onSubmit={handleSubmit}
setInput={setInput}
/>
</div>
);
}

View file

@ -0,0 +1,50 @@
'use client';
import { useEffect, useRef } from 'react';
import { MessageBubble } from './MessageBubble';
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
createdAt?: Date;
toolInvocations?: Array<{
toolName: string;
args: unknown;
result?: unknown;
}>;
}
interface ChatMessagesProps {
messages: Message[];
}
export function ChatMessages({ messages }: ChatMessagesProps): React.ReactElement {
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
});
if (messages.length === 0) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center text-foreground-secondary">
<div className="mb-2 text-4xl">🐟</div>
<p className="text-lg">Start chatting to test TPMJS tools</p>
<p className="mt-2 text-sm">Try asking: "Tell me a fish joke"</p>
</div>
</div>
);
}
return (
<div className="flex-1 space-y-4 overflow-y-auto p-4">
{messages.map((message) => (
<MessageBubble key={message.id} message={message} />
))}
<div ref={messagesEndRef} />
</div>
);
}

View file

@ -0,0 +1,125 @@
'use client';
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Card, CardContent } from '@tpmjs/ui/Card/Card';
interface MessagePart {
type: string; // Can be 'text', 'tool-{toolName}', 'step-start', etc.
text?: string;
toolCallId?: string;
input?: unknown;
output?: unknown;
state?: string;
providerMetadata?: unknown;
}
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
parts?: MessagePart[];
createdAt?: Date;
}
interface MessageBubbleProps {
message: Message;
}
export function MessageBubble({ message }: MessageBubbleProps): React.ReactElement {
const isUser = message.role === 'user';
// Debug: Log message structure
console.log('Message:', {
id: message.id,
role: message.role,
content: message.content,
parts: message.parts,
fullMessage: message,
});
return (
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
<Card variant={isUser ? 'elevated' : 'outline'} className="w-full max-w-2xl">
<CardContent className="p-4">
<div className="mb-2 flex items-center gap-2">
<Badge variant={isUser ? 'default' : 'secondary'} size="sm">
{isUser ? 'You' : 'AI'}
</Badge>
<span className="text-xs text-foreground-tertiary">
{new Date(message.createdAt || Date.now()).toLocaleTimeString()}
</span>
</div>
{/* Render message parts */}
{message.parts && message.parts.length > 0 ? (
<div className="space-y-3">
{message.parts.map((part, idx) => {
// Skip step-start markers
if (part.type === 'step-start') {
return null;
}
// Render text parts
if (part.type === 'text') {
return (
<div key={`text-${message.id}-${idx}`} className="whitespace-pre-wrap text-sm">
{part.text}
</div>
);
}
// Render tool calls (type starts with 'tool-')
if (part.type.startsWith('tool-')) {
const toolName = part.type.replace('tool-', '');
return (
<div
key={part.toolCallId || idx}
className="rounded border border-amber-500/20 bg-amber-500/5 p-3"
>
<div className="mb-3 flex items-center gap-2 border-b border-amber-500/20 pb-2">
<span className="text-base">🔧</span>
<strong className="font-mono text-sm text-foreground">{toolName}</strong>
{part.state && (
<Badge variant="secondary" size="sm">
{part.state}
</Badge>
)}
</div>
{/* Tool Input */}
<div className="mb-3">
<div className="mb-1 text-xs font-semibold text-foreground-secondary">
Input:
</div>
<pre className="overflow-x-auto rounded bg-surface p-2 text-xs text-foreground-secondary">
{JSON.stringify(part.input, null, 2)}
</pre>
</div>
{/* Tool Output */}
{part.output && (
<div>
<div className="mb-1 text-xs font-semibold text-foreground-secondary">
Output:
</div>
<pre className="overflow-x-auto rounded bg-surface p-2 text-xs text-foreground-secondary">
{JSON.stringify(part.output, null, 2)}
</pre>
</div>
)}
</div>
);
}
return null;
})}
</div>
) : (
// Fallback to content if no parts
<div className="whitespace-pre-wrap text-sm">{message.content || '...'}</div>
)}
</CardContent>
</Card>
</div>
);
}

View file

@ -0,0 +1,63 @@
'use client';
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
// Hardcoded list of installed tools
const INSTALLED_TOOLS = [
{
name: 'hello-world',
description: 'Returns a simple "Hello, World!" greeting',
category: 'text-analysis',
},
{
name: 'hello-name',
description: 'Returns a personalized greeting with a name',
category: 'text-analysis',
},
{
name: 'firecrawl (scrape)',
description: 'Scrape content from any URL',
category: 'web-scraping',
},
{
name: 'firecrawl (crawl)',
description: 'Crawl entire websites recursively',
category: 'web-scraping',
},
{
name: 'firecrawl (search)',
description: 'Search the web for content',
category: 'web-scraping',
},
];
export function ToolsSidebar(): React.ReactElement {
return (
<aside className="hidden w-64 border-r border-border bg-surface md:block">
<div className="p-4">
<h2 className="mb-4 text-lg font-bold">
Available Tools <Badge variant="secondary">{INSTALLED_TOOLS.length}</Badge>
</h2>
<div className="space-y-2">
{INSTALLED_TOOLS.map((tool) => (
<Card key={tool.name} variant="outline" className="cursor-pointer hover:bg-background">
<CardHeader>
<CardTitle className="text-sm">{tool.name}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-xs text-foreground-secondary">{tool.description}</p>
<div className="mt-2">
<Badge variant="secondary" size="sm">
{tool.category}
</Badge>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</aside>
);
}

View file

@ -0,0 +1,7 @@
import { createEnv } from '@tpmjs/env';
import { z } from 'zod';
export const env = createEnv({
// Server-only
OPENAI_API_KEY: z.string().min(1),
});

View file

@ -0,0 +1,16 @@
'use client';
import { useChat as useAISDKChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
/**
* Custom chat hook that wraps the official @ai-sdk/react useChat
* Handles SSE streaming with tool calls and UI message protocol
*/
export function useChat() {
return useAISDKChat({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
});
}

View file

@ -0,0 +1,46 @@
'use client';
import { useCallback, useState } from 'react';
import type { ToolUsageStats } from '~/lib/types';
export function useToolUsage() {
const [toolUsage, setToolUsage] = useState<Map<string, ToolUsageStats>>(new Map());
const trackTool = useCallback((packageName: string) => {
setToolUsage((prev) => {
const newMap = new Map(prev);
const existing = newMap.get(packageName);
if (existing) {
newMap.set(packageName, {
...existing,
callCount: existing.callCount + 1,
lastCalledAt: new Date(),
});
} else {
newMap.set(packageName, {
packageName,
callCount: 1,
lastCalledAt: new Date(),
});
}
return newMap;
});
}, []);
const clearUsage = useCallback(() => {
setToolUsage(new Map());
}, []);
// Convert Map to array sorted by most recent first
const toolUsageArray = Array.from(toolUsage.values()).sort(
(a, b) => b.lastCalledAt.getTime() - a.lastCalledAt.getTime()
);
return {
toolUsage: toolUsageArray,
trackTool,
clearUsage,
};
}

View file

@ -0,0 +1,92 @@
// Static imports for tools (required for Next.js/webpack)
import { helloNameTool, helloWorldTool } from '@tpmjs/hello';
import { crawlTool, scrapeTool, searchTool } from 'firecrawl-aisdk';
/**
* Load a specific TPMJS tool by package name
*/
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
export async function loadTpmjsTool(packageName: string): Promise<Record<string, any>> {
try {
// Map package names to their tool functions
switch (packageName) {
case '@tpmjs/hello':
// Hello has multiple tools, return all of them
return {
helloWorld: helloWorldTool,
helloName: helloNameTool,
};
case 'firecrawl-aisdk':
// Firecrawl has multiple tools, return all of them
return {
scrapeTool,
crawlTool,
searchTool,
};
default:
throw new Error(`Unknown tool package: ${packageName}`);
}
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to load tool from package ${packageName}: ${error.message}`);
}
throw new Error(`Failed to load tool from package ${packageName}: Unknown error`);
}
}
/**
* Type guard to check if an object is a valid AI SDK tool
*/
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
function isCoreTool(obj: unknown): obj is Record<string, any> {
if (typeof obj !== 'object' || obj === null) {
return false;
}
const tool = obj as Record<string, unknown>;
// Check for required AI SDK tool properties
return (
typeof tool.description === 'string' &&
typeof tool.parameters === 'object' &&
typeof tool.execute === 'function'
);
}
/**
* Load all installed TPMJS tools
*
* For now, this is a manual list. In the future, we can scan node_modules
* for packages with the "tpmjs-tool" keyword.
*/
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
export async function loadAllTools(): Promise<Record<string, any>> {
const installedTools = ['@tpmjs/hello', 'firecrawl-aisdk'];
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
const tools: Record<string, any> = {};
for (const packageName of installedTools) {
try {
const tool = await loadTpmjsTool(packageName);
// If the tool returns an object with multiple tools (like firecrawl), spread them
if (tool && typeof tool === 'object' && !tool.description) {
Object.assign(tools, tool);
} else {
// Single tool - use a cleaned name (remove hyphens, camelCase)
const toolName = packageName
.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())
.replace(/-/g, '');
tools[toolName] = tool;
}
} catch (error) {
console.error(`Failed to load tool ${packageName}:`, error);
// Continue loading other tools even if one fails
}
}
return tools;
}

View file

@ -0,0 +1,37 @@
export type MessageRole = 'user' | 'assistant' | 'system';
export interface ChatMessage {
id: string;
role: MessageRole;
content: string;
timestamp: Date;
toolCalls?: ToolCallInfo[];
}
export interface ToolCallInfo {
id: string;
toolName: string;
parameters: Record<string, unknown>;
result?: unknown;
status?: 'pending' | 'success' | 'error';
error?: string;
}
export interface TokenUsage {
inputTokens: number;
outputTokens: number;
totalTokens: number;
estimatedCost: number;
}
export interface ToolUsageStats {
packageName: string;
callCount: number;
lastCalledAt: Date;
}
export type SSEEvent =
| { type: 'chunk'; data: { text: string } }
| { type: 'tool-call'; data: ToolCallInfo }
| { type: 'complete'; data: { tokenUsage?: TokenUsage } }
| { type: 'error'; data: { message: string } };

View file

@ -0,0 +1,12 @@
import baseConfig from '@tpmjs/tailwind-config/base';
import type { Config } from 'tailwindcss';
export default {
...baseConfig,
content: [
'./src/app/**/*.{ts,tsx}',
'./src/components/**/*.{ts,tsx}',
'../../packages/ui/src/**/*.ts',
],
plugins: [...(baseConfig.plugins || []), require('@tailwindcss/typography')],
} satisfies Config;

View file

@ -0,0 +1,12 @@
{
"extends": "@tpmjs/tsconfig/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"~/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,80 @@
# @tpmjs/hello
Simple example TPMJS tools for AI SDK v6 - demonstrates how to create tools that work with AI agents.
## Tools
### helloWorldTool
Returns a simple "Hello, World!" greeting with a timestamp.
**Parameters:** None
**Returns:**
```json
{
"message": "Hello, World!",
"timestamp": "2024-12-04T..."
}
```
### helloNameTool
Returns a personalized greeting with the provided name.
**Parameters:**
- `name` (string, required): The name of the person to greet
**Returns:**
```json
{
"message": "Hello, John!",
"timestamp": "2024-12-04T..."
}
```
## Usage
### With AI SDK v6
```typescript
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { helloWorldTool, helloNameTool } from '@tpmjs/hello';
const result = streamText({
model: openai('gpt-4o-mini'),
messages: [{ role: 'user', content: 'Say hello to Alice' }],
tools: {
helloWorld: helloWorldTool,
helloName: helloNameTool,
},
});
```
## Template for Creating TPMJS Tools
This package serves as a template for creating your own TPMJS tools. Key requirements:
1. **Use AI SDK v6 Beta** (`ai@6.0.0-beta.124`)
2. **Use Zod 4** for parameter validation
3. **Export tool objects** with `description`, `parameters`, and `execute`
4. **Add TPMJS metadata** to package.json:
```json
"tpmjs": {
"category": "text-analysis",
"description": "Your tool description"
}
```
5. **Include the `tpmjs-tool` keyword** in package.json
## Development
```bash
# Build
pnpm build
# Type check
pnpm type-check
# Watch mode
pnpm dev
```

View file

@ -0,0 +1,27 @@
{
"name": "@tpmjs/hello",
"version": "0.0.1",
"private": true,
"description": "Example TPMJS tools - Hello World and Hello Name",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"type-check": "tsc --noEmit"
},
"keywords": ["tpmjs-tool", "ai-sdk", "hello", "example"],
"tpmjs": {
"category": "text-analysis",
"description": "Simple greeting tools - Hello World and personalized Hello Name greetings"
},
"dependencies": {
"ai": "6.0.0-beta.124"
},
"devDependencies": {
"@tpmjs/tsconfig": "workspace:*",
"typescript": "^5.9.3"
},
"files": ["dist", "README.md"]
}

View file

@ -0,0 +1,75 @@
import { jsonSchema, tool } from 'ai';
/**
* Input type for Hello World Tool
*/
type HelloWorldInput = {
includeTimestamp?: boolean;
};
/**
* Hello World Tool
* Returns a simple "Hello, World!" greeting
*
* This is a proper AI SDK v6 tool that can be used with streamText()
* Uses jsonSchema() to avoid Zod 4 JSON Schema conversion issues with OpenAI
*/
export const helloWorldTool = tool({
description: 'Returns a simple "Hello, World!" greeting message',
inputSchema: jsonSchema<HelloWorldInput>({
type: 'object',
properties: {
includeTimestamp: {
type: 'boolean',
description: 'Whether to include a timestamp in the response',
},
},
additionalProperties: false,
}),
async execute({ includeTimestamp = true }) {
const response: Record<string, unknown> = {
message: 'Hello, World!',
};
if (includeTimestamp) {
response.timestamp = new Date().toISOString();
}
return response;
},
});
/**
* Input type for Hello Name Tool
*/
type HelloNameInput = {
name: string;
};
/**
* Hello Name Tool
* Returns a personalized greeting with the provided name
*
* This is a proper AI SDK v6 tool that can be used with streamText()
* Uses jsonSchema() to avoid Zod 4 JSON Schema conversion issues with OpenAI
*/
export const helloNameTool = tool({
description: 'Returns a personalized greeting with the provided name',
inputSchema: jsonSchema<HelloNameInput>({
type: 'object',
properties: {
name: {
type: 'string',
description: 'The name of the person to greet',
},
},
required: ['name'],
additionalProperties: false,
}),
async execute({ name }) {
return {
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
};
},
});

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,23 @@
{
"name": "@tpmjs/tools",
"version": "0.0.0",
"private": true,
"description": "Example TPMJS tools for AI SDK v6",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"type-check": "tsc --noEmit"
},
"keywords": ["tpmjs-tool", "ai-sdk", "example"],
"dependencies": {
"ai": "6.0.0-beta.124",
"zod": "^4.0.0"
},
"devDependencies": {
"@tpmjs/tsconfig": "workspace:*",
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,37 @@
import { z } from 'zod';
/**
* Hello World Tool
* Returns a simple "Hello, World!" greeting
*
* This is a proper AI SDK v6 tool that can be used with streamText()
*/
export const helloWorldTool = {
description: 'Returns a simple "Hello, World!" greeting',
parameters: z.object({}),
execute: async () => {
return {
message: 'Hello, World!',
timestamp: new Date().toISOString(),
};
},
};
/**
* Hello Name Tool
* Returns a personalized greeting with the provided name
*
* This is a proper AI SDK v6 tool that can be used with streamText()
*/
export const helloNameTool = {
description: 'Returns a personalized greeting with the provided name',
parameters: z.object({
name: z.string().describe('The name of the person to greet'),
}),
execute: async ({ name }: { name: string }) => {
return {
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
};
},
};

View file

@ -0,0 +1,11 @@
{
"extends": "@tpmjs/tsconfig/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

457
pnpm-lock.yaml generated
View file

@ -39,6 +39,97 @@ importers:
specifier: ^5.9.3
version: 5.9.3
apps/playground:
dependencies:
'@ai-sdk/openai':
specifier: 3.0.0-beta.74
version: 3.0.0-beta.74(effect@3.18.4)(zod@4.1.13)
'@ai-sdk/react':
specifier: ^2.0.106
version: 2.0.106(react@19.2.0)(zod@4.1.13)
'@tpmjs/env':
specifier: workspace:*
version: link:../../packages/env
'@tpmjs/hello':
specifier: workspace:*
version: link:../../packages/tools/hello
'@tpmjs/types':
specifier: workspace:*
version: link:../../packages/types
'@tpmjs/ui':
specifier: workspace:*
version: link:../../packages/ui
'@tpmjs/utils':
specifier: workspace:*
version: link:../../packages/utils
ai:
specifier: 6.0.0-beta.124
version: 6.0.0-beta.124(effect@3.18.4)(zod@4.1.13)
firecrawl-aisdk:
specifier: ^0.7.2
version: 0.7.2
fish-joke-generator:
specifier: ^1.1.0
version: 1.1.0(openai@6.9.1(ws@8.18.3)(zod@4.1.13))(zod@4.1.13)
next:
specifier: ^16.0.4
version: 16.0.4(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
openai:
specifier: ^6.9.1
version: 6.9.1(ws@8.18.3)(zod@4.1.13)
react:
specifier: ^19.0.0
version: 19.2.0
react-dom:
specifier: ^19.0.0
version: 19.2.0(react@19.2.0)
zod:
specifier: ^4.0.0
version: 4.1.13
devDependencies:
'@tailwindcss/typography':
specifier: ^0.5.19
version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6))
'@tpmjs/eslint-config':
specifier: workspace:*
version: link:../../packages/config/eslint
'@tpmjs/tailwind-config':
specifier: workspace:*
version: link:../../packages/config/tailwind
'@tpmjs/tsconfig':
specifier: workspace:*
version: link:../../packages/config/tsconfig
'@types/node':
specifier: ^22.10.2
version: 22.19.1
'@types/react':
specifier: ^19.0.2
version: 19.2.7
'@types/react-dom':
specifier: ^19.0.2
version: 19.2.3(@types/react@19.2.7)
autoprefixer:
specifier: ^10.4.20
version: 10.4.22(postcss@8.5.6)
eslint:
specifier: ^9.39.1
version: 9.39.1(jiti@1.21.7)
eslint-config-next:
specifier: ^16.0.4
version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
postcss:
specifier: ^8.5.1
version: 8.5.6
tailwindcss:
specifier: ^3.4.17
version: 3.4.18(tsx@4.20.6)
typescript:
specifier: ^5.9.3
version: 5.9.3
apps/web:
dependencies:
'@ai-sdk/openai':
@ -131,10 +222,10 @@ importers:
version: 10.4.22(postcss@8.5.6)
eslint:
specifier: ^9.39.1
version: 9.39.1(jiti@1.21.7)
version: 9.39.1(jiti@2.6.1)
eslint-config-next:
specifier: ^16.0.4
version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
postcss:
specifier: ^8.5.1
version: 8.5.6
@ -368,6 +459,22 @@ importers:
specifier: ^4.1.13
version: 4.1.13
packages/tools:
dependencies:
ai:
specifier: 6.0.0-beta.124
version: 6.0.0-beta.124(effect@3.18.4)(zod@4.1.13)
zod:
specifier: ^4.0.0
version: 4.1.13
devDependencies:
'@tpmjs/tsconfig':
specifier: workspace:*
version: link:../config/tsconfig
typescript:
specifier: ^5.9.3
version: 5.9.3
packages/tools/createBlogPost:
dependencies:
zod:
@ -384,6 +491,19 @@ importers:
specifier: ^5.9.3
version: 5.9.3
packages/tools/hello:
dependencies:
ai:
specifier: 6.0.0-beta.124
version: 6.0.0-beta.124(effect@3.18.4)(zod@4.1.13)
devDependencies:
'@tpmjs/tsconfig':
specifier: workspace:*
version: link:../../config/tsconfig
typescript:
specifier: ^5.9.3
version: 5.9.3
packages/types:
dependencies:
zod:
@ -497,12 +617,24 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/gateway@2.0.18':
resolution: {integrity: sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/openai@3.0.0-beta.74':
resolution: {integrity: sha512-0AofFL0odf7dUjmpiKVBxemXWK7L5YTmqD+9sY3V/0yzxtWuP8effQVOTz3rXO7kSF8OabDxW4WUHGwAOBiIJA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider-utils@3.0.18':
resolution: {integrity: sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider-utils@4.0.0-beta.40':
resolution: {integrity: sha512-5345iQxWV1coKbs85vkrThsEmiFcIkgqMZUKFrVDM7E4FRqgsnWtn4394/RnShOKRun5K7TWIYCLz3GfRGy/Ig==}
engines: {node: '>=18'}
@ -519,10 +651,24 @@ packages:
effect:
optional: true
'@ai-sdk/provider@2.0.0':
resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
engines: {node: '>=18'}
'@ai-sdk/provider@3.0.0-beta.22':
resolution: {integrity: sha512-Ss0tgCZwzccS6MREhjAI28lkAV5PfJnoBFbv8mas74Cs5OseFKqxg3dEd1lRL8mf4Qapu1xpBLkV4/ldSShSdA==}
engines: {node: '>=18'}
'@ai-sdk/react@2.0.106':
resolution: {integrity: sha512-TU8ONNhm64GI7O60UDCcOz9CdyCp3emQwSYrSnq+QWBNgS8vDlRQ3ZwXyPNAJQdXyBTafVS2iyS0kvV+KXaPAQ==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
zod: ^3.25.76 || ^4.1.8
peerDependenciesMeta:
zod:
optional: true
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@ -1449,6 +1595,10 @@ packages:
'@types/react': '>=16'
react: '>=16'
'@mendable/firecrawl-js@4.8.1':
resolution: {integrity: sha512-40fW83AuGziIrhONyexdc/EvfSUFvlqgcwmpAoUxY7yAl84kfSbqTgxJ/5ycd4SoIDW2plQOXzC/rxwtwFUnYg==}
engines: {node: '>=22.0.0'}
'@mswjs/interceptors@0.40.0':
resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==}
engines: {node: '>=18'}
@ -2325,6 +2475,12 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
ai@5.0.106:
resolution: {integrity: sha512-M5obwavxSJJ3tGlAFqI6eltYNJB0D20X6gIBCFx/KVorb/X1fxVVfiZZpZb+Gslu4340droSOjT0aKQFCarNVg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
ai@6.0.0-beta.124:
resolution: {integrity: sha512-Apl4uNZLzc4sAUtu4W77DpM9o+bJJB46A/ayVLDck3MBv+rw4/qGBY9H6/gY9Xpc/CeVogyyEr1DxBMv47olCw==}
engines: {node: '>=18'}
@ -2435,6 +2591,9 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
autoprefixer@10.4.22:
resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==}
engines: {node: ^10 || ^12 || >=14}
@ -2450,6 +2609,9 @@ packages:
resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==}
engines: {node: '>=4'}
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
@ -2606,6 +2768,10 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@ -2713,6 +2879,10 @@ packages:
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dependency-cruiser@17.3.1:
resolution: {integrity: sha512-yWwszB4GKIBKK/xiHSQ6TVIV6k8byd+gMGT2RMQ+03wb1jGH48cSsLH29iUUZgGtKyLSH51NurbnjXV0+niUjA==}
engines: {node: ^20.12||^22||>=24}
@ -3081,6 +3251,17 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
firecrawl-aisdk@0.7.2:
resolution: {integrity: sha512-JyqKs12ScYcKmHnvkc/h/ItvcJxLjxn1WkFTe6w6RVb/LXVw7gqjOuRg2bvfsZeeqlTOyiLAzy5tbj4eMOgjRA==}
engines: {node: '>=18.0.0'}
fish-joke-generator@1.1.0:
resolution: {integrity: sha512-w5NE7hIVEOBRcIYIc8AhOJyQSFhDfJF7/0www2p5g3t3XcQuYpRjQ8sclw601eXqrygBPbYchSM0+KfdlpESNg==}
engines: {node: '>=18.0.0'}
peerDependencies:
openai: ^6.0.0
zod: ^4.0.0
fix-dts-default-cjs-exports@1.0.1:
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
@ -3091,6 +3272,15 @@ packages:
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
@ -3099,6 +3289,10 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
format@0.2.2:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
@ -3868,6 +4062,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mimic-function@5.0.1:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
@ -4271,6 +4473,9 @@ packages:
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@ -4658,6 +4863,11 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
swr@2.3.7:
resolution: {integrity: sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
@ -4685,6 +4895,10 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
throttleit@2.1.0:
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
engines: {node: '>=18'}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@ -4873,6 +5087,9 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
typescript-event-target@1.1.1:
resolution: {integrity: sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@ -4929,6 +5146,11 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -5149,12 +5371,20 @@ packages:
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
engines: {node: '>=18'}
zod-to-json-schema@3.25.0:
resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==}
peerDependencies:
zod: ^3.25 || ^4
zod-validation-error@4.0.2:
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
zod: ^3.25.0 || ^4.0.0
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.1.13:
resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
@ -5176,6 +5406,20 @@ snapshots:
- arktype
- effect
'@ai-sdk/gateway@2.0.18(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 2.0.0
'@ai-sdk/provider-utils': 3.0.18(zod@3.25.76)
'@vercel/oidc': 3.0.5
zod: 3.25.76
'@ai-sdk/gateway@2.0.18(zod@4.1.13)':
dependencies:
'@ai-sdk/provider': 2.0.0
'@ai-sdk/provider-utils': 3.0.18(zod@4.1.13)
'@vercel/oidc': 3.0.5
zod: 4.1.13
'@ai-sdk/openai@3.0.0-beta.74(effect@3.18.4)(zod@4.1.13)':
dependencies:
'@ai-sdk/provider': 3.0.0-beta.22
@ -5186,6 +5430,20 @@ snapshots:
- arktype
- effect
'@ai-sdk/provider-utils@3.0.18(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 2.0.0
'@standard-schema/spec': 1.0.0
eventsource-parser: 3.0.6
zod: 3.25.76
'@ai-sdk/provider-utils@3.0.18(zod@4.1.13)':
dependencies:
'@ai-sdk/provider': 2.0.0
'@standard-schema/spec': 1.0.0
eventsource-parser: 3.0.6
zod: 4.1.13
'@ai-sdk/provider-utils@4.0.0-beta.40(effect@3.18.4)(zod@4.1.13)':
dependencies:
'@ai-sdk/provider': 3.0.0-beta.22
@ -5195,10 +5453,24 @@ snapshots:
optionalDependencies:
effect: 3.18.4
'@ai-sdk/provider@2.0.0':
dependencies:
json-schema: 0.4.0
'@ai-sdk/provider@3.0.0-beta.22':
dependencies:
json-schema: 0.4.0
'@ai-sdk/react@2.0.106(react@19.2.0)(zod@4.1.13)':
dependencies:
'@ai-sdk/provider-utils': 3.0.18(zod@4.1.13)
ai: 5.0.106(zod@4.1.13)
react: 19.2.0
swr: 2.3.7(react@19.2.0)
throttleit: 2.1.0
optionalDependencies:
zod: 4.1.13
'@alloc/quick-lru@5.2.0': {}
'@babel/code-frame@7.27.1':
@ -5982,6 +6254,15 @@ snapshots:
'@types/react': 19.2.7
react: 19.2.0
'@mendable/firecrawl-js@4.8.1':
dependencies:
axios: 1.13.2
typescript-event-target: 1.1.1
zod: 3.25.76
zod-to-json-schema: 3.25.0(zod@3.25.76)
transitivePeerDependencies:
- debug
'@mswjs/interceptors@0.40.0':
dependencies:
'@open-draft/deferred-promise': 2.2.0
@ -6889,6 +7170,22 @@ snapshots:
acorn@8.15.0: {}
ai@5.0.106(zod@3.25.76):
dependencies:
'@ai-sdk/gateway': 2.0.18(zod@3.25.76)
'@ai-sdk/provider': 2.0.0
'@ai-sdk/provider-utils': 3.0.18(zod@3.25.76)
'@opentelemetry/api': 1.9.0
zod: 3.25.76
ai@5.0.106(zod@4.1.13):
dependencies:
'@ai-sdk/gateway': 2.0.18(zod@4.1.13)
'@ai-sdk/provider': 2.0.0
'@ai-sdk/provider-utils': 3.0.18(zod@4.1.13)
'@opentelemetry/api': 1.9.0
zod: 4.1.13
ai@6.0.0-beta.124(effect@3.18.4)(zod@4.1.13):
dependencies:
'@ai-sdk/gateway': 2.0.0-beta.68(effect@3.18.4)(zod@4.1.13)
@ -7029,6 +7326,8 @@ snapshots:
async-function@1.0.0: {}
asynckit@0.4.0: {}
autoprefixer@10.4.22(postcss@8.5.6):
dependencies:
browserslist: 4.28.0
@ -7045,6 +7344,14 @@ snapshots:
axe-core@4.11.0: {}
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@4.1.0: {}
bail@2.0.2: {}
@ -7203,6 +7510,10 @@ snapshots:
color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
comma-separated-tokens@2.0.3: {}
commander@14.0.2: {}
@ -7287,6 +7598,8 @@ snapshots:
defu@6.1.4: {}
delayed-stream@1.0.0: {}
dependency-cruiser@17.3.1:
dependencies:
acorn: 8.15.0
@ -7586,7 +7899,7 @@ snapshots:
eslint: 9.39.1(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7))
eslint-plugin-import: 2.32.0(eslint@9.39.1(jiti@1.21.7))
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7))
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@1.21.7))
@ -7600,6 +7913,26 @@ snapshots:
- eslint-plugin-import-x
- supports-color
eslint-config-next@16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@next/eslint-plugin-next': 16.0.4
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@2.6.1))
globals: 16.4.0
typescript-eslint: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-webpack
- eslint-plugin-import-x
- supports-color
eslint-import-resolver-node@0.3.9:
dependencies:
debug: 3.2.7
@ -7619,7 +7952,22 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(eslint@9.39.1(jiti@1.21.7))
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7))
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
eslint: 9.39.1(jiti@2.6.1)
get-tsconfig: 4.13.0
is-bun-module: 2.0.0
stable-hash: 0.0.5
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@ -7643,6 +7991,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
@ -7672,7 +8030,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)):
eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@ -7699,6 +8057,33 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
array.prototype.findlastindex: 1.2.6
array.prototype.flat: 1.3.3
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
minimatch: 3.1.2
object.fromentries: 2.0.8
object.groupby: 1.0.3
object.values: 1.2.1
semver: 6.3.1
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.1(jiti@1.21.7)):
dependencies:
aria-query: 5.3.2
@ -7752,6 +8137,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@babel/core': 7.28.5
'@babel/parser': 7.28.5
eslint: 9.39.1(jiti@2.6.1)
hermes-parser: 0.25.1
zod: 4.1.13
zod-validation-error: 4.0.2(zod@4.1.13)
transitivePeerDependencies:
- supports-color
eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)):
dependencies:
array-includes: 3.1.9
@ -7987,6 +8383,19 @@ snapshots:
locate-path: 6.0.0
path-exists: 4.0.0
firecrawl-aisdk@0.7.2:
dependencies:
'@mendable/firecrawl-js': 4.8.1
ai: 5.0.106(zod@3.25.76)
zod: 3.25.76
transitivePeerDependencies:
- debug
fish-joke-generator@1.1.0(openai@6.9.1(ws@8.18.3)(zod@4.1.13))(zod@4.1.13):
dependencies:
openai: 6.9.1(ws@8.18.3)(zod@4.1.13)
zod: 4.1.13
fix-dts-default-cjs-exports@1.0.1:
dependencies:
magic-string: 0.30.21
@ -8000,6 +8409,8 @@ snapshots:
flatted@3.3.3: {}
follow-redirects@1.15.11: {}
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
@ -8009,6 +8420,14 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
format@0.2.2: {}
formatly@0.3.0:
@ -9014,6 +9433,12 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mimic-function@5.0.1: {}
min-indent@1.0.1: {}
@ -9428,6 +9853,8 @@ snapshots:
property-information@7.1.0: {}
proxy-from-env@1.1.0: {}
punycode@2.3.1: {}
pure-rand@6.1.0: {}
@ -9952,6 +10379,12 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
swr@2.3.7(react@19.2.0):
dependencies:
dequal: 2.0.3
react: 19.2.0
use-sync-external-store: 1.6.0(react@19.2.0)
tagged-tag@1.0.0: {}
tailwind-merge@2.6.0: {}
@ -9996,6 +10429,8 @@ snapshots:
dependencies:
any-promise: 1.3.0
throttleit@2.1.0: {}
tiny-invariant@1.3.3: {}
tinybench@2.9.0: {}
@ -10214,6 +10649,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
typescript-event-target@1.1.1: {}
typescript@5.9.3: {}
ufo@1.6.1: {}
@ -10303,6 +10740,10 @@ snapshots:
dependencies:
punycode: 2.3.1
use-sync-external-store@1.6.0(react@19.2.0):
dependencies:
react: 19.2.0
util-deprecate@1.0.2: {}
util@0.12.5:
@ -10511,10 +10952,16 @@ snapshots:
yoctocolors-cjs@2.1.3: {}
zod-to-json-schema@3.25.0(zod@3.25.76):
dependencies:
zod: 3.25.76
zod-validation-error@4.0.2(zod@4.1.13):
dependencies:
zod: 4.1.13
zod@3.25.76: {}
zod@4.1.13: {}
zwitch@2.0.4: {}