diff --git a/.env.local.example b/.env.local.example index 8c25739a..78d9b072 100644 --- a/.env.local.example +++ b/.env.local.example @@ -15,5 +15,8 @@ NEXT_PUBLIC_GISCUS_REPO_ID= NEXT_PUBLIC_GISCUS_CATEGORY= NEXT_PUBLIC_GISCUS_CATEGORY_ID= +# AI (Anthropic) +ANTHROPIC_API_KEY= + # Google Tag Manager NEXT_PUBLIC_GTM_ID= diff --git a/app/api/ai/chat/route.ts b/app/api/ai/chat/route.ts new file mode 100644 index 00000000..cd43c270 --- /dev/null +++ b/app/api/ai/chat/route.ts @@ -0,0 +1,28 @@ +import { anthropic } from '@ai-sdk/anthropic'; +import { streamText } from 'ai'; +import { type AIContext, buildSystemPrompt } from '@/lib/ai/system-prompt'; + +export const maxDuration = 30; + +export async function POST(req: Request) { + const body = await req.json(); + const { prompt, context } = body as { prompt: string; context?: AIContext }; + + if (!prompt || typeof prompt !== 'string') { + return new Response('Missing prompt', { status: 400 }); + } + + if (prompt.length > 500) { + return new Response('Prompt too long (max 500 chars)', { status: 400 }); + } + + const result = streamText({ + model: anthropic('claude-sonnet-4-20250514'), + system: buildSystemPrompt(context), + prompt, + maxOutputTokens: 300, + temperature: 0.7, + }); + + return result.toTextStreamResponse(); +} diff --git a/app/manifest.json b/app/manifest.json index ca75abbf..dbef89fb 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -33,4 +33,4 @@ "purpose": "any maskable" } ] -} +} \ No newline at end of file diff --git a/components/mate/terminal/command-context.tsx b/components/mate/terminal/command-context.tsx index 6f74db69..4d71b710 100644 --- a/components/mate/terminal/command-context.tsx +++ b/components/mate/terminal/command-context.tsx @@ -30,6 +30,8 @@ export type DataSources = { export interface TerminalTools { clearLines: () => void; + appendStreamingText: (chunk: string) => void; + finalizeStream: () => void; } export interface CommandContextType { @@ -42,7 +44,7 @@ const CommandContext = createContext({ spotify: { data: null }, github: { data: null }, } as DataSources, - tools: { clearLines: () => {} }, + tools: { clearLines: () => {}, appendStreamingText: () => {}, finalizeStream: () => {} }, }); export const useCommandContext = () => useContext(CommandContext); diff --git a/components/mate/terminal/commands/ai.ts b/components/mate/terminal/commands/ai.ts new file mode 100644 index 00000000..ec0a2c7c --- /dev/null +++ b/components/mate/terminal/commands/ai.ts @@ -0,0 +1,140 @@ +import type { AIContext } from '@/lib/ai/system-prompt'; +import type { DataSources } from '../command-context'; +import type { Command, CommandContext } from '../types/commands'; + +function buildContextFromDataSources(dataSources: DataSources): AIContext { + const context: AIContext = {}; + + const spotifyData = dataSources.spotify.data; + if (spotifyData) { + context.spotify = { + currentlyPlaying: spotifyData.currentlyPlaying + ? { + title: spotifyData.currentlyPlaying.title, + artist: spotifyData.currentlyPlaying.artist, + album: spotifyData.currentlyPlaying.album, + } + : null, + recentTracks: spotifyData.recentlyPlayed?.slice(0, 5).map((t) => ({ title: t.title, artist: t.artist })), + topTracks: spotifyData.topTracks?.slice(0, 5).map((t) => ({ title: t.title, artist: t.artist })), + topArtists: spotifyData.topArtists?.slice(0, 5).map((a) => ({ name: a.name, genres: a.genres })), + }; + } + + const githubData = dataSources.github.data; + if (githubData) { + context.github = { + login: githubData.profile?.login, + followers: githubData.profile?.followers?.length, + activities: githubData.activities?.activities?.slice(0, 5).map((a) => ({ + type: a.type, + title: a.title, + repo: a.repo.fullName, + date: a.date, + })), + }; + } + + return context; +} + +async function streamAIResponse( + prompt: string, + { dataSources, tools }: Pick, +): Promise { + const context = buildContextFromDataSources(dataSources); + + const response = await fetch('/api/ai/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt, context }), + }); + + if (!response.ok) { + const errorText = await response.text(); + tools.appendStreamingText(`Error: ${errorText || response.statusText}`); + tools.finalizeStream(); + return; + } + + const reader = response.body?.getReader(); + if (!reader) { + tools.appendStreamingText('Error: No response stream.'); + tools.finalizeStream(); + return; + } + + const decoder = new TextDecoder(); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + const chunk = decoder.decode(value, { stream: true }); + tools.appendStreamingText(chunk); + } + } finally { + tools.finalizeStream(); + } +} + +export const askCommand: Command = { + name: 'ask', + description: 'Ask AIt anything', + usage: 'ask ', + aliases: ['ai', 'chat', 'q'], + handler: async ({ args, dataSources, tools }) => { + if (!args) return 'Usage: ask \nExample: ask what tech stack do you use?'; + + tools.appendStreamingText(''); + await streamAIResponse(args, { dataSources, tools }); + return { type: 'streamed' }; + }, +}; + +export const explainCommand: Command = { + name: 'explain', + description: "Explain a topic from Mateo's perspective", + usage: 'explain ', + aliases: ['ex'], + handler: async ({ args, dataSources, tools }) => { + if (!args) return 'Usage: explain \nExample: explain your approach to testing'; + + const prompt = `Explain the following from your perspective and experience as a senior engineer: ${args}`; + tools.appendStreamingText(''); + await streamAIResponse(prompt, { dataSources, tools }); + return { type: 'streamed' }; + }, +}; + +export const summarizeCommand: Command = { + name: 'summarize', + description: 'Summarize what Mateo is about', + usage: 'summarize [topic]', + aliases: ['sum'], + handler: async ({ args, dataSources, tools }) => { + const prompt = args + ? `Give a focused summary about: ${args}. Draw from your experience and projects.` + : "Give a concise summary of who you are, what you do, and what you're working on right now."; + + tools.appendStreamingText(''); + await streamAIResponse(prompt, { dataSources, tools }); + return { type: 'streamed' }; + }, +}; + +export const tldrCommand: Command = { + name: 'tldr', + description: 'Quick take on any topic', + usage: 'tldr ', + handler: async ({ args, dataSources, tools }) => { + if (!args) return 'Usage: tldr \nExample: tldr your open source work'; + + const prompt = `Give me a very brief, 2-3 line tldr about: ${args}`; + tools.appendStreamingText(''); + await streamAIResponse(prompt, { dataSources, tools }); + return { type: 'streamed' }; + }, +}; + +export const aiCommands: Command[] = [askCommand, explainCommand, summarizeCommand, tldrCommand]; diff --git a/components/mate/terminal/commands/index.ts b/components/mate/terminal/commands/index.ts index 3a117d9f..ed410410 100644 --- a/components/mate/terminal/commands/index.ts +++ b/components/mate/terminal/commands/index.ts @@ -1,4 +1,5 @@ import { registry } from '../command-registry'; +import { aiCommands } from './ai'; import { aitCommand } from './ait'; import { linksCommand, siteCommand } from './navigation'; import { personalCommands } from './personal'; @@ -9,6 +10,7 @@ registry.registerGroup('System', systemCommands); registry.registerGroup('Personal', personalCommands); registry.registerGroup('Social', socialCommands); registry.registerGroup('Navigation', [linksCommand, siteCommand]); +registry.registerGroup('AI', aiCommands); registry.registerGroup('Fun', [aitCommand]); export { registry as commandRegistry }; diff --git a/components/mate/terminal/hooks/use-command-executor.ts b/components/mate/terminal/hooks/use-command-executor.ts index 5f9ff35c..9634f4fc 100644 --- a/components/mate/terminal/hooks/use-command-executor.ts +++ b/components/mate/terminal/hooks/use-command-executor.ts @@ -20,11 +20,15 @@ export function useCommandExecutor({ dataSources, tools, actions }: CommandExecu const commandName = input.trim().split(' ')[0]; trackTerminal.commandExecuted(commandName, result.success !== false); - const newLines = [ - { text: input, showPrompt: true }, - ...result.output.split('\n').map((text) => ({ text, showPrompt: false })), - ]; - actions.addCompletedLines(newLines); + if (result.streamed) { + actions.addCompletedLines([{ text: input, showPrompt: true }]); + } else { + const newLines = [ + { text: input, showPrompt: true }, + ...result.output.split('\n').map((text) => ({ text, showPrompt: false })), + ]; + actions.addCompletedLines(newLines); + } actions.addToHistory(input); }, [runCommand, actions], diff --git a/components/mate/terminal/hooks/use-command-runner.tsx b/components/mate/terminal/hooks/use-command-runner.tsx index 7beefdfe..0b1db476 100644 --- a/components/mate/terminal/hooks/use-command-runner.tsx +++ b/components/mate/terminal/hooks/use-command-runner.tsx @@ -2,9 +2,10 @@ import { useCallback, useMemo } from 'react'; import type { DataSources, TerminalTools } from '../command-context'; import { commandRegistry } from '../commands/index'; -type CommandResult = { +export type CommandResult = { success: boolean; output: string; + streamed?: boolean; }; interface CommandRunnerOptions { @@ -17,8 +18,13 @@ export function useCommandRunner({ dataSources, tools }: CommandRunnerOptions) { const runCommand = useCallback( async (input: string): Promise => { - const trimmedInput = input.trim().toLowerCase(); - const command = commandMap.get(trimmedInput); + const trimmedInput = input.trim(); + const spaceIndex = trimmedInput.indexOf(' '); + const commandName = + spaceIndex === -1 ? trimmedInput.toLowerCase() : trimmedInput.slice(0, spaceIndex).toLowerCase(); + const args = spaceIndex === -1 ? undefined : trimmedInput.slice(spaceIndex + 1).trim() || undefined; + + const command = commandMap.get(commandName); if (!command) { return { @@ -28,8 +34,13 @@ export function useCommandRunner({ dataSources, tools }: CommandRunnerOptions) { } try { - const output = await command.handler({ dataSources, tools }); - return { success: true, output }; + const result = await command.handler({ dataSources, tools, args }); + + if (typeof result === 'object' && result.type === 'streamed') { + return { success: true, output: '', streamed: true }; + } + + return { success: true, output: result as string }; } catch (error) { console.error('Error executing command:', error); return { diff --git a/components/mate/terminal/hooks/use-terminal-input.ts b/components/mate/terminal/hooks/use-terminal-input.ts index 46a42a0f..000ec949 100644 --- a/components/mate/terminal/hooks/use-terminal-input.ts +++ b/components/mate/terminal/hooks/use-terminal-input.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import type { TerminalState, TerminalStateActions } from './use-terminal-state'; interface TerminalInputOptions { - state: Pick; + state: Pick; actions: Pick; executeCommand: (input: string) => Promise; getMatchingCommands: (input: string) => string[]; @@ -12,7 +12,9 @@ interface TerminalInputOptions { export function useTerminalInput({ state, actions, executeCommand, getMatchingCommands }: TerminalInputOptions) { const handleUserInput = useCallback( (e: KeyboardEvent) => { - const { userInput, commandHistory, historyIndex } = state; + const { userInput, commandHistory, historyIndex, streamingText } = state; + + if (streamingText !== null) return; switch (e.key) { case 'Enter': diff --git a/components/mate/terminal/hooks/use-terminal-state.ts b/components/mate/terminal/hooks/use-terminal-state.ts index 332a42d2..99ce22f8 100644 --- a/components/mate/terminal/hooks/use-terminal-state.ts +++ b/components/mate/terminal/hooks/use-terminal-state.ts @@ -10,6 +10,7 @@ export interface TerminalState { userInput: string; commandHistory: string[]; historyIndex: number; + streamingText: string | null; } export interface TerminalStateActions { @@ -22,6 +23,8 @@ export interface TerminalStateActions { setUserInput: (input: string) => void; addToHistory: (command: string) => void; updateHistoryIndex: (index: number) => void; + setStreamingText: (text: string | null) => void; + appendStreamingText: (chunk: string) => void; } export function useTerminalState(_initialMessages: string[]): [TerminalState, TerminalStateActions] { @@ -32,6 +35,7 @@ export function useTerminalState(_initialMessages: string[]): [TerminalState, Te const [userInput, setUserInput] = useState(''); const [commandHistory, setCommandHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); + const [streamingText, setStreamingText] = useState(null); const addCompletedLine = useCallback((line: TerminalLine) => { setCompletedLines((prev) => [...prev, line].slice(-100)); @@ -50,6 +54,10 @@ export function useTerminalState(_initialMessages: string[]): [TerminalState, Te setHistoryIndex(-1); }, []); + const appendStreamingText = useCallback((chunk: string) => { + setStreamingText((prev) => (prev === null ? chunk : prev + chunk)); + }, []); + return [ { currentLine, @@ -59,6 +67,7 @@ export function useTerminalState(_initialMessages: string[]): [TerminalState, Te userInput, commandHistory, historyIndex, + streamingText, }, { setCurrentLine, @@ -70,6 +79,8 @@ export function useTerminalState(_initialMessages: string[]): [TerminalState, Te setUserInput, addToHistory, updateHistoryIndex: setHistoryIndex, + setStreamingText, + appendStreamingText, }, ]; } diff --git a/components/mate/terminal/terminal.tsx b/components/mate/terminal/terminal.tsx index cd1bef88..b67c3e54 100644 --- a/components/mate/terminal/terminal.tsx +++ b/components/mate/terminal/terminal.tsx @@ -33,7 +33,7 @@ export function Terminal({ const commandCount = useRef(0); const [state, actions] = useTerminalState(initialMessages); - const { currentLine, typingLine, completedLines, isComplete, userInput } = state; + const { currentLine, typingLine, completedLines, isComplete, userInput, streamingText } = state; const terminalRef = useRef(null); const inputRef = useRef(null); @@ -75,7 +75,27 @@ export function Terminal({ ], ); - const tools = useMemo(() => ({ clearLines: actions.clearCompletedLines }), [actions]); + const isStreaming = streamingText !== null; + const streamingTextRef = useRef(null); + streamingTextRef.current = streamingText; + + const finalizeStream = useCallback(() => { + const text = streamingTextRef.current; + if (text !== null) { + const lines = text.split('\n').map((t) => ({ text: t, showPrompt: false })); + actions.addCompletedLines(lines); + } + actions.setStreamingText(null); + }, [actions]); + + const tools = useMemo( + () => ({ + clearLines: actions.clearCompletedLines, + appendStreamingText: actions.appendStreamingText, + finalizeStream, + }), + [actions, finalizeStream], + ); const { executeCommand: baseExecuteCommand, getMatchingCommands } = useCommandExecutor({ dataSources, @@ -184,7 +204,7 @@ export function Terminal({ }; debouncedScroll(); return () => clearTimeout(timeoutId); - }, [completedLines, typingLine, scrollToBottom]); + }, [completedLines, typingLine, streamingText, scrollToBottom]); const handleTerminalClick = useCallback(() => { if (isComplete && inputRef.current) { @@ -195,6 +215,7 @@ export function Terminal({ const handleTerminalKeyDown = useCallback( (e: React.KeyboardEvent) => { + if (e.target instanceof HTMLInputElement) return; if (isComplete && inputRef.current && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); inputRef.current.focus(); @@ -224,7 +245,8 @@ export function Terminal({ {!isComplete && !skipAnimations && typingLine.currentIndex > 0 && ( )} - {isComplete && ( + {isStreaming && } + {isComplete && !isStreaming && ( +

+ {text} + +

+ + ); +}); + const MemoizedLine = memo(Line); const TerminalHeader = memo(function TerminalHeader() { return ( diff --git a/components/mate/terminal/types/commands.ts b/components/mate/terminal/types/commands.ts index b73cb302..d65fe61d 100644 --- a/components/mate/terminal/types/commands.ts +++ b/components/mate/terminal/types/commands.ts @@ -3,13 +3,17 @@ import type { DataSources, TerminalTools } from '../command-context'; export interface CommandContext { dataSources: DataSources; tools: TerminalTools; + args?: string; } +export type CommandHandlerResult = string | { type: 'streamed' }; + export interface Command { name: string; description: string; - handler: (context: CommandContext) => Promise | string; + handler: (context: CommandContext) => Promise | CommandHandlerResult; aliases?: string[]; + usage?: string; } export interface CommandGroup { diff --git a/components/mate/terminal/utils/formatting.ts b/components/mate/terminal/utils/formatting.ts index 7a3aac3a..0a44ef13 100644 --- a/components/mate/terminal/utils/formatting.ts +++ b/components/mate/terminal/utils/formatting.ts @@ -1,8 +1,8 @@ import type { Command, CommandGroup } from '../types/commands'; export const formatCommand = (cmd: Command): string => { - const nameAndAliases = cmd.aliases?.length ? `${cmd.name} (${cmd.aliases.join(', ')})` : cmd.name; - const paddedName = nameAndAliases.padEnd(20); + const label = cmd.usage || (cmd.aliases?.length ? `${cmd.name} (${cmd.aliases.join(', ')})` : cmd.name); + const paddedName = label.padEnd(28); return ` ${paddedName} - ${cmd.description}`; }; diff --git a/lib/ai/system-prompt.ts b/lib/ai/system-prompt.ts new file mode 100644 index 00000000..8abb0f15 --- /dev/null +++ b/lib/ai/system-prompt.ts @@ -0,0 +1,111 @@ +import personal from '@/lib/config/personal'; + +export interface AIContext { + spotify?: { + currentlyPlaying?: { title: string; artist: string; album: string } | null; + recentTracks?: { title: string; artist: string }[]; + topTracks?: { title: string; artist: string }[]; + topArtists?: { name: string; genres?: string[] }[]; + }; + github?: { + login?: string; + followers?: number; + activities?: { type: string; title: string; repo: string; date: string }[]; + }; +} + +function buildSpotifyContext(spotify: AIContext['spotify']): string { + if (!spotify) return ''; + + const parts: string[] = []; + + if (spotify.currentlyPlaying) { + parts.push( + `Currently listening to: "${spotify.currentlyPlaying.title}" by ${spotify.currentlyPlaying.artist} (${spotify.currentlyPlaying.album})`, + ); + } + + if (spotify.recentTracks?.length) { + const tracks = spotify.recentTracks + .slice(0, 5) + .map((t) => `"${t.title}" by ${t.artist}`) + .join(', '); + parts.push(`Recent tracks: ${tracks}`); + } + + if (spotify.topArtists?.length) { + const artists = spotify.topArtists + .slice(0, 5) + .map((a) => a.name) + .join(', '); + parts.push(`Top artists: ${artists}`); + } + + return parts.length ? `\n\nLive Spotify data:\n${parts.join('\n')}` : ''; +} + +function buildGitHubContext(github: AIContext['github']): string { + if (!github) return ''; + + const parts: string[] = []; + + if (github.login) { + parts.push(`GitHub: @${github.login}`); + } + if (github.followers) { + parts.push(`Followers: ${github.followers}`); + } + + if (github.activities?.length) { + const activities = github.activities + .slice(0, 5) + .map((a) => `${a.type}: ${a.title} (${a.repo})`) + .join(', '); + parts.push(`Recent activity: ${activities}`); + } + + return parts.length ? `\n\nLive GitHub data:\n${parts.join('\n')}` : ''; +} + +export function buildSystemPrompt(context?: AIContext): string { + const spotifySection = buildSpotifyContext(context?.spotify); + const githubSection = buildGitHubContext(context?.github); + + return `You are AIt — Mateo Nunez's digital alter ego, embedded in the terminal of his personal website (mateonunez.co). + +## Who you are +You speak as Mateo's AI counterpart. You're sharp, direct, warm but never cheesy. You match Mateo's personality: technically rigorous, a bit irreverent, Colombian humor, zero fluff. You use concise, terminal-friendly language — no markdown headers, no bullet lists, no verbose paragraphs. Keep responses short (2-6 lines for simple questions, up to 12 for complex ones). You can use emojis sparingly like Mateo does. + +## About Mateo +- ${personal.bio.full} +- Location: ${personal.location.display} +- Title: ${personal.title} / ${personal.alternativeTitle} +- Languages spoken: ${personal.languagesSpoken.join(', ')} +- Currently: ${personal.currentWork.join('; ')} + +## Technical skills +- Languages: ${personal.skills.languages.join(', ')} +- Frameworks: ${personal.skills.frameworks.join(', ')} +- Cloud & DevOps: ${personal.skills.cloud.join(', ')} +- Databases: ${personal.skills.databases.join(', ')} +- AI: ${personal.skills.ai.join(', ')} + +## Site sections +- / — Homepage with terminal, about, latest articles, Spotify integration +- /blog — Technical articles on Node.js, Next.js, testing, and more +- /open-source — Open source projects and GitHub activity +- /spotify — Full Spotify profile, playlists, top tracks and artists + +## Bookshelf +${personal.bookshelf.map((b) => `- "${b.label}" by ${b.author}: ${b.description}`).join('\n')} +${spotifySection}${githubSection} + +## Rules +- You respond as AIt, Mateo's alter ego — not as a generic assistant. +- Never break character. You ARE AIt. +- Format for terminal output: plain text, no markdown. Use line breaks for structure. +- If asked about something you genuinely don't know about Mateo, say so honestly. +- You can reference live Spotify/GitHub data when relevant. +- If someone asks to contact Mateo, share: ${personal.email} +- Keep it real. No corporate speak. No filler. Direct and human.`; +} diff --git a/package.json b/package.json index a56829ba..ef6bb9ea 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "prepare": "husky && tsx scripts/sync-skills.ts" }, "dependencies": { + "@ai-sdk/anthropic": "^3.0.58", "@giscus/react": "^3.1.0", "@octokit/graphql": "^9.0.3", "@radix-ui/react-avatar": "^1.1.11", @@ -28,6 +29,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@vercel/analytics": "^2.0.1", + "ai": "^6.0.116", "babel-plugin-react-compiler": "1.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b48d676..bb00ca8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ importers: .: dependencies: + '@ai-sdk/anthropic': + specifier: ^3.0.58 + version: 3.0.58(zod@4.3.6) '@giscus/react': specifier: ^3.1.0 version: 3.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -53,7 +56,10 @@ importers: version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@vercel/analytics': specifier: ^2.0.1 - version: 2.0.1(next@16.1.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 2.0.1(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + ai: + specifier: ^6.0.116 + version: 6.0.116(zod@4.3.6) babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 @@ -80,7 +86,7 @@ importers: version: 0.577.0(react@19.2.4) next: specifier: 16.1.7 - version: 16.1.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-mdx-remote: specifier: ^6.0.0 version: 6.0.0(@types/react@19.2.14)(react@19.2.4) @@ -172,6 +178,28 @@ importers: packages: + '@ai-sdk/anthropic@3.0.58': + resolution: {integrity: sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/gateway@3.0.66': + resolution: {integrity: sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.19': + resolution: {integrity: sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -697,6 +725,10 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1148,6 +1180,9 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1324,6 +1359,10 @@ packages: vue-router: optional: true + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1338,6 +1377,12 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ai@6.0.116: + resolution: {integrity: sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} @@ -1495,6 +1540,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -1597,6 +1646,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + lightningcss-android-arm64@1.31.1: resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} engines: {node: '>= 12.0.0'} @@ -2210,6 +2262,30 @@ packages: snapshots: + '@ai-sdk/anthropic@3.0.58(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) + zod: 4.3.6 + + '@ai-sdk/gateway@3.0.66(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) + '@vercel/oidc': 3.1.0 + zod: 4.3.6 + + '@ai-sdk/provider-utils@4.0.19(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.6 + + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + '@alloc/quick-lru@5.2.0': {} '@babel/code-frame@7.29.0': @@ -2589,6 +2665,8 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 + '@opentelemetry/api@1.9.0': {} + '@polka/url@1.0.0-next.29': {} '@radix-ui/number@1.1.1': {} @@ -3029,6 +3107,8 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@standard-schema/spec@1.1.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -3151,11 +3231,13 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vercel/analytics@2.0.1(next@16.1.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + '@vercel/analytics@2.0.1(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': optionalDependencies: - next: 16.1.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 + '@vercel/oidc@3.1.0': {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -3166,6 +3248,14 @@ snapshots: acorn@8.15.0: {} + ai@6.0.116(zod@4.3.6): + dependencies: + '@ai-sdk/gateway': 3.0.66(zod@4.3.6) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) + '@opentelemetry/api': 1.9.0 + zod: 4.3.6 + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -3331,6 +3421,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + eventsource-parser@3.0.6: {} + extend@3.0.2: {} fast-content-type-parse@3.0.0: {} @@ -3445,6 +3537,8 @@ snapshots: js-tokens@4.0.0: {} + json-schema@0.4.0: {} + lightningcss-android-arm64@1.31.1: optional: true @@ -3982,7 +4076,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next@16.1.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.1.7 '@swc/helpers': 0.5.15 @@ -4001,6 +4095,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.1.7 '@next/swc-win32-arm64-msvc': 16.1.7 '@next/swc-win32-x64-msvc': 16.1.7 + '@opentelemetry/api': 1.9.0 babel-plugin-react-compiler: 1.0.0 sharp: 0.34.5 transitivePeerDependencies: