From e0880e80c3da9ac6acb2f8eb089ae77980fa6257 Mon Sep 17 00:00:00 2001 From: malewis5 Date: Wed, 11 Mar 2026 14:51:18 -0400 Subject: [PATCH] save --- examples/nextjs-chat/package.json | 7 +- .../src/app/.well-known/vercel/flags/route.ts | 7 + examples/nextjs-chat/src/flags.ts | 14 + examples/nextjs-chat/src/lib/agent.ts | 9 + examples/nextjs-chat/src/lib/bot.tsx | 847 +----------------- examples/nextjs-chat/src/proxy.ts | 13 + pnpm-lock.yaml | 171 +++- turbo.json | 9 +- 8 files changed, 225 insertions(+), 852 deletions(-) create mode 100644 examples/nextjs-chat/src/app/.well-known/vercel/flags/route.ts create mode 100644 examples/nextjs-chat/src/flags.ts create mode 100644 examples/nextjs-chat/src/lib/agent.ts diff --git a/examples/nextjs-chat/package.json b/examples/nextjs-chat/package.json index 7a83fb72..4d567961 100644 --- a/examples/nextjs-chat/package.json +++ b/examples/nextjs-chat/package.json @@ -11,6 +11,7 @@ "recording:export": "tsx src/lib/recorder.ts" }, "dependencies": { + "@ai-sdk/anthropic": "^3.0.58", "@chat-adapter/discord": "workspace:*", "@chat-adapter/gchat": "workspace:*", "@chat-adapter/github": "workspace:*", @@ -18,11 +19,13 @@ "@chat-adapter/slack": "workspace:*", "@chat-adapter/state-memory": "workspace:*", "@chat-adapter/state-redis": "workspace:*", - "@chat-adapter/telegram": "workspace:*", "@chat-adapter/teams": "workspace:*", + "@chat-adapter/telegram": "workspace:*", "@chat-adapter/whatsapp": "workspace:*", - "ai": "^6.0.5", + "@flags-sdk/vercel": "^1.0.2", + "ai": "^6.0.116", "chat": "workspace:*", + "flags": "^4.0.4", "next": "^16.1.5", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/nextjs-chat/src/app/.well-known/vercel/flags/route.ts b/examples/nextjs-chat/src/app/.well-known/vercel/flags/route.ts new file mode 100644 index 00000000..bb31706f --- /dev/null +++ b/examples/nextjs-chat/src/app/.well-known/vercel/flags/route.ts @@ -0,0 +1,7 @@ +import { getProviderData } from "@flags-sdk/vercel"; +import { createFlagsDiscoveryEndpoint } from "flags/next"; +import { ngrokTunnelFlag } from "../../../../flags"; + +export const GET = createFlagsDiscoveryEndpoint(async () => { + return await getProviderData({ ngrokTunnelFlag }); +}); diff --git a/examples/nextjs-chat/src/flags.ts b/examples/nextjs-chat/src/flags.ts new file mode 100644 index 00000000..49045d93 --- /dev/null +++ b/examples/nextjs-chat/src/flags.ts @@ -0,0 +1,14 @@ +import { vercelAdapter } from "@flags-sdk/vercel"; +import { flag } from "flags/next"; + +export const ngrokTunnelFlag = flag({ + key: "use-ngrok-tunnel", + description: + "Flag that will redirect traffic to ngrok tunnel for demo purposes", + defaultValue: false, + options: [ + { label: "Off", value: false }, + { label: "On", value: true }, + ], + adapter: vercelAdapter(), +}); diff --git a/examples/nextjs-chat/src/lib/agent.ts b/examples/nextjs-chat/src/lib/agent.ts new file mode 100644 index 00000000..aec9b5dc --- /dev/null +++ b/examples/nextjs-chat/src/lib/agent.ts @@ -0,0 +1,9 @@ +import { anthropic } from "@ai-sdk/anthropic"; +import { ToolLoopAgent } from "ai"; + +export const agent = new ToolLoopAgent({ + model: "anthropic/claude-opus-4.5", + instructions: + "You are a helpful assistant. Answer questions concisely and to the point.", + tools: { webSearch: anthropic.tools.webSearch_20260209({ maxUses: 3 }) }, +}); diff --git a/examples/nextjs-chat/src/lib/bot.tsx b/examples/nextjs-chat/src/lib/bot.tsx index b6fc9495..0c227fc9 100644 --- a/examples/nextjs-chat/src/lib/bot.tsx +++ b/examples/nextjs-chat/src/lib/bot.tsx @@ -1,835 +1,32 @@ /** @jsxImportSource chat */ -import { createMemoryState } from "@chat-adapter/state-memory"; -import { createRedisState } from "@chat-adapter/state-redis"; -import { ToolLoopAgent } from "ai"; -import { - Actions, - Button, - Card, - CardLink, - Chat, - Divider, - emoji, - Field, - Fields, - LinkButton, - Modal, - RadioSelect, - Section, - Select, - SelectOption, - Table, - CardText as Text, - TextInput, - toAiMessages, -} from "chat"; -import { buildAdapters } from "./adapters"; - -const AI_MENTION_REGEX = /\bAI\b/i; -const DISABLE_AI_REGEX = /disable\s*AI/i; -const ENABLE_AI_REGEX = /enable\s*AI/i; -const DM_ME_REGEX = /^dm\s*me$/i; - -const state = process.env.REDIS_URL - ? createRedisState({ - url: process.env.REDIS_URL, - keyPrefix: "chat-sdk-webhooks", - }) - : createMemoryState(); -const adapters = buildAdapters(); - -// Define thread state type -interface ThreadState { - aiMode?: boolean; - history?: { role: "user" | "assistant"; content: string }[]; -} - -// Create the bot instance with typed thread state -// @ts-expect-error Adapters type lacks string index signature -export const bot = new Chat({ - userName: process.env.BOT_USERNAME || "mybot", - adapters, - state, - logger: "debug", -}); -// AI agent for AI mode -const agent = new ToolLoopAgent({ - model: "anthropic/claude-4.5-sonnet", - instructions: - "You are a helpful assistant in a chat thread. Answer the user's queries in a concise manner.", +import { createSlackAdapter } from "@chat-adapter/slack"; +import { createRedisState } from "@chat-adapter/state-redis"; +import { Actions, Button, Card, CardText, Chat } from "chat"; +import { createDiscordAdapter } from "@chat-adapter/discord"; +import { agent } from "./agent"; +import { createWhatsAppAdapter } from "@chat-adapter/whatsapp"; +import { createTelegramAdapter } from "@chat-adapter/telegram"; + +export const bot = new Chat({ + userName: "Chat SDK Bot", + adapters: { + slack: createSlackAdapter(), + discord: createDiscordAdapter(), + telegram: createTelegramAdapter(), + whatsapp: createWhatsAppAdapter(), + }, + state: createRedisState(), }); -// Handle new @mentions of the bot bot.onNewMention(async (thread, message) => { - await thread.subscribe(); - - // Check if user wants to enable AI mode (mention contains "AI") - if (AI_MENTION_REGEX.test(message.text)) { - await thread.setState({ aiMode: true }); - // Also respond to the initial message with AI - await thread.startTyping("Thinking..."); - try { - const result = await agent.stream({ prompt: message.text }); - await thread.post(result.fullStream); - } catch (err) { - console.error("Error in AI response:", err); - await thread.post( - `${emoji.warning} Error in AI response: ${ - err instanceof Error ? err.message : "Unknown error" - }` - ); - } - return; - } - - // Default welcome card await thread.startTyping(); - await thread.post( - - I'm now listening to this thread. Try these actions: - - {`${emoji.sparkles} **Mention me with "AI"** to enable AI assistant mode`} - - - View documentation - - - - - - - - - - - - - - - - - - - Open Link - - - - ); -}); - -// Post a welcome message when the bot is added to a channel -bot.onMemberJoinedChannel(async (event) => { - // Only post when the bot itself joins - if (event.userId !== event.adapter.botUserId) { - return; - } - - await event.adapter.postMessage( - event.channelId, - "*Chat SDK Bot is available in this channel.* Tag @Chat SDK Bot to begin." - ); -}); - -// Handle direct messages — AI conversation by default -// This fires on every DM, regardless of subscription status -bot.onDirectMessage(async (_thread, message, channel) => { - await channel.startTyping("Thinking..."); - let history: { role: "user" | "assistant"; content: string }[]; - try { - const messages: (typeof message)[] = []; - for await (const msg of channel.messages) { - messages.push(msg); - if (messages.length >= 20) { - break; - } - } - history = - messages.length > 0 ? toAiMessages(messages) : toAiMessages([message]); - } catch { - history = toAiMessages([message]); - } - try { - const result = await agent.stream({ prompt: history }); - await channel.post(result.fullStream); - } catch (err) { - console.error("Error in DM AI response:", err); - await channel.post( - `${emoji.warning} Error: ${ - err instanceof Error ? err.message : "Unknown error" - }` - ); - } -}); - -bot.onAction("show_channel_help", async (event) => { - if (!event.thread) { - return; - } - const platforms = Object.keys(adapters).join(", ") || "none configured"; - await event.thread.post( - - {`Here's how I can help:`} - -
- {`${emoji.star} **Mention me** to start a conversation`} - {`${emoji.sparkles} **Mention me with "AI"** to enable AI assistant mode`} - {`${emoji.eyes} I'll respond to messages in threads where I'm mentioned`} - {`${emoji.fire} React to my messages and I'll react back!`} - {`${emoji.rocket} Active platforms: ${platforms}`} -
-
- ); -}); - -bot.onAction("ephemeral", async (event) => { - if (!event.thread) { - return; - } - await event.thread.postEphemeral( - event.user, - - - Only you can see this message. It will disappear when you reload. - - Try opening a modal from this ephemeral: - - - - , - { fallbackToDM: true } - ); -}); - -bot.onAction("ephemeral_modal", async (event) => { - await event.openModal( - - - - ); -}); - -// @ts-expect-error async void handler vs ModalSubmitHandler return type -bot.onModalSubmit("ephemeral_modal_form", async (event) => { - await event.relatedMessage?.edit( - - Your response: **{event.values.response}** - The original ephemeral message was updated. - - ); -}); - -bot.onAction("quick_action", async (event) => { - if (!event.thread) { - return; - } - const action = event.value; - if (action === "greet") { - await event.thread.post(`${emoji.wave} Hello, ${event.user.fullName}!`); - } else if (action === "info") { - await event.thread.post( - `${emoji.memo} You're on **${event.adapter.name}** in thread \`${event.threadId}\`` - ); - } else if (action === "help") { - await event.thread.post( - `${emoji.question} Try mentioning me with "AI" to enable AI assistant mode!` - ); - } -}); - -bot.onAction("choose_plan", (event) => { - if (!event.thread) { - return; - } - event.thread.post( - - - - - - - - - - - ); -}); -bot.onAction("plan_selected", (event) => { - if (!event.thread) { - return; - } - event.thread.post( - - You chose plan *{event.value}* - - ); -}); - -// Handle card button actions -bot.onAction("hello", async (event) => { - if (!event.thread) { - return; - } - await event.thread.post(`${emoji.wave} Hello, ${event.user.fullName}!`); -}); - -bot.onAction("info", async (event) => { - if (!event.thread) { - return; - } - const threadState = await event.thread.state; - await event.thread.post( - - - - - - - - - - ); -}); - -bot.onAction("goodbye", async (event) => { - if (!event.thread) { - return; - } - await event.thread.post( - `${emoji.wave} Goodbye, ${event.user.fullName}! See you later.` - ); -}); - -bot.onAction("show-table", async (event) => { - if (!event.thread) { - return; - } - await event.thread.post( - - Here's the current team roster: - - - ); -}); - -// Feedback modal component -const FeedbackModal = ( - - - - - -); - -// Open feedback modal -bot.onAction("feedback", async (event) => { - await event.openModal(FeedbackModal); -}); - -bot.onSlashCommand("/ping", async (event) => { - await event.channel.post( - `Pong! Command invoked by ${event.user.fullName}${event.text ? `: ${event.text}` : ""}` - ); -}); - -// Opens feedback modal via /feedback -bot.onSlashCommand("/test-feedback", async (event) => { - const result = await event.openModal(FeedbackModal); - if (!result) { - await event.channel.post( - `${emoji.warning} Couldn't open the feedback modal. Please try again.` - ); - } + const result = await agent.stream({ prompt: message.text }); + await thread.post(result.fullStream); }); -// Open bug report modal with privateMetadata carrying context from button value -bot.onAction("report", async (event) => { - await event.openModal( - - - - - - ); -}); - -// Handle bug report modal — reads context from privateMetadata -bot.onModalSubmit("report_form", async (event) => { - console.log("report_form privateMetadata:", event.privateMetadata); - const metadata = event.privateMetadata - ? JSON.parse(event.privateMetadata) - : {}; - const { title, steps, severity } = event.values; - - if (!title || title.length < 3) { - return { - action: "errors" as const, - errors: { title: "Title must be at least 3 characters" }, - }; - } - - await event.relatedThread?.post( - - - - - - - - - - {`**Steps to Reproduce:**\n${steps}`} - - ); -}); - -// Handle modal submission -bot.onModalSubmit("feedback_form", async (event) => { - const { message, category, email } = event.values; - - // Validate message - if (!message || message.length < 5) { - return { - action: "errors" as const, - errors: { message: "Feedback must be at least 5 characters" }, - }; - } - - // Log the feedback - console.log("Received feedback:", { - message, - category, - email, - user: event.user.userName, - }); - await event.relatedMessage?.edit(`${emoji.check} **Feedback received!**`); - const target = event.relatedChannel || event.relatedThread; - await target?.postEphemeral( - event.user, - - Thank you for your feedback! - - - - - - - , - { fallbackToDM: false } - ); -}); - -// Handle modal close (cancel) -bot.onModalClose("feedback_form", (event) => { - console.log(`${event.user.userName} cancelled the feedback form`); -}); - -// Demonstrate fetchMessages and allMessages -bot.onAction("messages", async (event) => { - if (!event.thread) { - return; - } - const { thread } = event; - - // Helper to get display text for a message (handles empty text from cards) - const getDisplayText = (text: string, hasAttachments?: boolean) => { - if (text?.trim()) { - const truncated = text.slice(0, 30); - return text.length > 30 ? `${truncated}...` : truncated; - } - // Empty text - likely a card or attachment-only message - return hasAttachments ? "[Attachment]" : "[Card]"; - }; - - try { - // 1. fetchMessages with backward direction (default) - gets most recent messages - const recentResult = await thread.adapter.fetchMessages(thread.id, { - limit: 5, - direction: "backward", - }); - - // 2. fetchMessages with forward direction - gets oldest messages first - const oldestResult = await thread.adapter.fetchMessages(thread.id, { - limit: 5, - direction: "forward", - }); - - // 3. allMessages iterator - iterate through all messages (uses forward direction) - const allMessages: string[] = []; - let count = 0; - for await (const msg of thread.allMessages) { - const displayText = getDisplayText( - msg.text, - msg.attachments && msg.attachments.length > 0 - ); - allMessages.push( - `Msg ${count + 1}: ${msg.author.userName} - ${displayText}` - ); - count++; - } - - // Format results - const formatMessages = (msgs: typeof recentResult.messages) => - msgs.length > 0 - ? msgs - .map((m, i) => { - const displayText = getDisplayText( - m.text, - m.attachments && m.attachments.length > 0 - ); - return `Msg ${i + 1}: ${m.author.userName} - ${displayText}`; - }) - .join("\n\n") - : "(no messages)"; - - await thread.post( - -
- **fetchMessages (backward, limit: 5)** - Gets most recent messages, cursor points to older - {formatMessages(recentResult.messages)} - {`Next cursor: ${ - recentResult.nextCursor ? "yes" : "none" - }`} -
- -
- **fetchMessages (forward, limit: 5)** - Gets oldest messages first, cursor points to newer - {formatMessages(oldestResult.messages)} - {`Next cursor: ${ - oldestResult.nextCursor ? "yes" : "none" - }`} -
- -
- **allMessages iterator** - Iterates from oldest to newest using forward direction - - {allMessages.length > 0 - ? allMessages.join("\n\n") - : "(no messages)"} - -
-
- ); - } catch (err) { - await thread.post( - `${emoji.warning} Error fetching messages: ${ - err instanceof Error ? err.message : "Unknown error" - }` - ); - } -}); - -// Demonstrate channel abstraction: read channel messages and post summary -bot.onAction("channel-post", async (event) => { - if (!event.thread) { - return; - } - const { thread } = event; - const channel = thread.channel; - - try { - // Fetch channel info for the name - const info = await channel.fetchMetadata(); - const channelName = info.name || channel.id; - - // Get the last 3 top-level channel messages using the backward iterator - const recent: string[] = []; - for await (const msg of channel.messages) { - const preview = msg.text?.trim() - ? msg.text.slice(0, 50) - : "[Card/Attachment]"; - recent.push(`- ${msg.author.userName}: ${preview}`); - if (recent.length >= 3) { - break; - } - } - - const summary = - recent.length > 0 ? recent.join("\n\n") : "(no top-level messages found)"; - - await channel.post( - -
- {`Channel: ${channelName}`} - **Last 3 top-level messages:** - {summary} -
-
- ); - } catch (err) { - await thread.post( - `${emoji.warning} Error reading channel: ${ - err instanceof Error ? err.message : "Unknown error" - }` - ); - } -}); - -// Helper to delay execution -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -// Handle messages matching a pattern -bot.onNewMessage(/help/i, async (thread, message) => { - const platforms = Object.keys(adapters).join(", ") || "none configured"; - await thread.post( - - {`Hi ${message.author.userName}! Here's how I can help:`} - -
- {`${emoji.star} **Mention me** to start a conversation`} - {`${emoji.sparkles} **Mention me with "AI"** to enable AI assistant mode`} - {`${emoji.eyes} I'll respond to messages in threads where I'm mentioned`} - {`${emoji.fire} React to my messages and I'll react back!`} - {`${emoji.rocket} Active platforms: ${platforms}`} -
-
- ); -}); - -// Handle messages in subscribed threads -bot.onSubscribedMessage(async (thread, message) => { - if (!(thread.adapter.name === "telegram" || message.isMention)) { - return; - } - // Get thread state to check AI mode - const threadState = await thread.state; - - // Check if user wants to disable AI mode - if (DISABLE_AI_REGEX.test(message.text)) { - await thread.setState({ aiMode: false }); - await thread.post(`${emoji.check} AI mode disabled for this thread.`); - return; - } - - // Check if user wants to enable AI mode - if (ENABLE_AI_REGEX.test(message.text)) { - await thread.setState({ aiMode: true }); - await thread.post(`${emoji.sparkles} AI mode enabled for this thread!`); - return; - } - - // If AI mode is enabled (or this is a DM), use the AI agent - if (threadState?.aiMode) { - // Build conversation history: try fetchMessages first, then fall back to - // stored history in thread state (for platforms without message history API) - let history: { role: "user" | "assistant"; content: string }[]; - try { - const result = await thread.adapter.fetchMessages(thread.id, { - limit: 20, - }); - if (result.messages.length > 0) { - history = toAiMessages(result.messages); - } else { - // No messages from API — use stored history + current message - history = [ - ...(threadState?.history ?? []), - { role: "user" as const, content: message.text }, - ]; - } - } catch { - // fetchMessages not supported — use stored history + current message - history = [ - ...(threadState?.history ?? []), - { role: "user" as const, content: message.text }, - ]; - } - - await thread.startTyping("Thinking..."); - try { - const result = await agent.stream({ prompt: history }); - await thread.post(result.fullStream); - const responseText = await result.text; - // Persist updated history for platforms without message history API - history.push({ role: "assistant", content: responseText }); - await thread.setState({ history }); - } catch (err) { - console.error("Error in AI response:", err); - await thread.post( - `${emoji.warning} Error: ${ - err instanceof Error ? err.message : "Unknown error" - }` - ); - } - return; - } - - // Check if user wants a DM - if (DM_ME_REGEX.test(message.text.trim())) { - try { - const dmThread = await bot.openDM(message.author); - await dmThread.post( - - {`Hi ${message.author.fullName}! You requested a DM from the thread.`} - - This is a private conversation between us. - - ); - await thread.post(`${emoji.check} I've sent you a DM!`); - } catch (err) { - await thread.post( - `${emoji.warning} Sorry, I couldn't send you a DM. Error: ${ - err instanceof Error ? err.message : "Unknown error" - }` - ); - } - return; - } - - // Check if message has attachments - if (message.attachments && message.attachments.length > 0) { - const attachmentInfo = message.attachments - .map( - (a) => - `- ${a.name || "unnamed"} (${a.type}, ${a.mimeType || "unknown"})` - ) - .join("\n"); - - await thread.post( - - {`You sent ${message.attachments.length} file(s):`} - {attachmentInfo} - - ); - return; - } - - // Default response for other messages +bot.onDirectMessage(async (thread, message) => { await thread.startTyping(); - await delay(1000); - const response = await thread.post(`${emoji.thinking} Processing...`); - try { - await delay(2000); - await response.edit(`${emoji.eyes} Just a little bit...`); - await delay(1000); - await response.edit(`${emoji.check} Thanks for your message!`); - } catch { - // Some platforms (WhatsApp) don't support editing — send a follow-up instead - await thread.post(`${emoji.check} Thanks for your message!`); - } -}); - -// Handle emoji reactions - respond with a matching emoji or message -bot.onReaction(["thumbs_up", "heart", "fire", "rocket"], async (event) => { - // Only respond to added reactions, not removed ones - if (!event.added) { - return; - } - - // GChat and Teams bots cannot add reactions via their APIs - // Respond with a message instead - if (event.adapter.name === "gchat" || event.adapter.name === "teams") { - await event.adapter.postMessage( - event.threadId, - `Thanks for the ${event.rawEmoji}!` - ); - return; - } - - // React to the same message with the same emoji - // Adapters auto-convert normalized emoji to platform-specific format - await event.adapter.addReaction( - event.threadId, - event.messageId, - emoji.raised_hands - ); + const result = await agent.stream({ prompt: message.text }); + await thread.post(result.fullStream); }); diff --git a/examples/nextjs-chat/src/proxy.ts b/examples/nextjs-chat/src/proxy.ts index 3034b353..2281aaa4 100644 --- a/examples/nextjs-chat/src/proxy.ts +++ b/examples/nextjs-chat/src/proxy.ts @@ -1,6 +1,9 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { createClient } from "redis"; +import { ngrokTunnelFlag } from "./flags"; + +const NGROK_TUNNEL_URL = "https://chat-sdk.ngrok.app"; // Redis URL from environment const REDIS_URL = process.env.REDIS_URL || ""; @@ -53,6 +56,15 @@ async function getPreviewBranchUrl(): Promise { } export async function proxy(request: NextRequest) { + const ngrokEnabled = await ngrokTunnelFlag(); + + if (ngrokEnabled) { + const { pathname, search } = request.nextUrl; + const targetUrl = new URL(`${pathname}${search}`, NGROK_TUNNEL_URL); + console.warn(`[proxy] Rewriting ${pathname} to ngrok tunnel`); + return NextResponse.rewrite(targetUrl, { request }); + } + if (process.env.NODE_ENV !== "production") { return NextResponse.next(); } @@ -87,5 +99,6 @@ export const config = { matcher: [ // Match webhook API routes "/api/webhooks/:path*", + "/api/discord/gateway", ], }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3a1f680..ed8d55d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: examples/nextjs-chat: dependencies: + '@ai-sdk/anthropic': + specifier: ^3.0.58 + version: 3.0.58(zod@4.3.3) '@chat-adapter/discord': specifier: workspace:* version: link:../../packages/adapter-discord @@ -207,12 +210,18 @@ importers: '@chat-adapter/whatsapp': specifier: workspace:* version: link:../../packages/adapter-whatsapp + '@flags-sdk/vercel': + specifier: ^1.0.2 + version: 1.0.2(flags@4.0.4(@opentelemetry/api@1.9.0)(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) ai: - specifier: ^6.0.5 - version: 6.0.6(zod@4.3.3) + specifier: ^6.0.116 + version: 6.0.116(zod@4.3.3) chat: specifier: workspace:* version: link:../../packages/chat + flags: + specifier: ^4.0.4 + version: 4.0.4(@opentelemetry/api@1.9.0)(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next: specifier: ^16.1.5 version: 16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -652,14 +661,20 @@ 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@2.0.39': resolution: {integrity: sha512-ULnefGmRHG0/tRrf+dtDwgQYAttGi/TR0FmASAzTs1dtpeZp4Xoh1VyWrX3Z1bM3WDs9RM3ZeSE77kQT/jbfjw==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.5': - resolution: {integrity: sha512-AtxA1wcoKTHr9uFoC5KZEXqJP4SMW4j3VbcliUECUYssbWbePJ9+b3AaCny1lxf1xhDK9EIyAgBOKhXoQSr9nA==} + '@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 @@ -670,8 +685,8 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.2': - resolution: {integrity: sha512-KaykkuRBdF/ffpI5bwpL4aSCmO/99p8/ci+VeHwJO8tmvXtiVAb99QeyvvvXmL61e9Zrvv4GBGoajW19xdjkVQ==} + '@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 @@ -680,8 +695,8 @@ packages: resolution: {integrity: sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==} engines: {node: '>=18'} - '@ai-sdk/provider@3.0.1': - resolution: {integrity: sha512-2lR4w7mr9XrydzxBSjir4N6YMGdXD+Np1Sh0RXABh7tWdNFFwIeRI1Q+SaYZMbfL8Pg8RRLcrxQm51yxTLhokg==} + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} engines: {node: '>=18'} '@ai-sdk/react@2.0.135': @@ -946,6 +961,10 @@ packages: resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} engines: {node: '>=16.11.0'} + '@edge-runtime/cookies@5.0.2': + resolution: {integrity: sha512-Sd8LcWpZk/SWEeKGE8LT6gMm5MGfX/wm+GPnh1eBEtCpya3vYqn37wYknwAHw92ONoyyREl1hJwxV/Qx2DWNOg==} + engines: {node: '>=16'} + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -1114,6 +1133,11 @@ packages: cpu: [x64] os: [win32] + '@flags-sdk/vercel@1.0.2': + resolution: {integrity: sha512-QVCJBlYWXYNFph5jhxV3v5rMYnKXxWQZQwtGgProVRqVF1k0Dpum/ZttlKHyays4I3zJi5Z6oYVxb2XEhIZFuA==} + peerDependencies: + flags: '*' + '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} @@ -2968,14 +2992,35 @@ packages: vue-router: optional: true - '@vercel/oidc@3.0.5': - resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==} + '@vercel/flags-core@1.1.0': + resolution: {integrity: sha512-QA0Vj90aflwbsOoJc5hMaumRXD4hItK3CgBQXDJtx/Ob/ty85/rFSQbvzSoENq+d5EB/JBc4dhq96CZ70NWhSg==} + peerDependencies: + '@openfeature/server-sdk': 1.18.0 + flags: '*' + next: '*' + peerDependenciesMeta: + '@openfeature/server-sdk': + optional: true + next: + optional: true + + '@vercel/functions@3.4.3': + resolution: {integrity: sha512-kA14KIUVgAY6VXbhZ5jjY+s0883cV3cZqIU3WhrSRxuJ9KvxatMjtmzl0K23HK59oOUjYl7HaE/eYMmhmqpZzw==} engines: {node: '>= 20'} + peerDependencies: + '@aws-sdk/credential-provider-web-identity': '*' + peerDependenciesMeta: + '@aws-sdk/credential-provider-web-identity': + optional: true '@vercel/oidc@3.1.0': resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} engines: {node: '>= 20'} + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + '@vercel/speed-insights@1.3.1': resolution: {integrity: sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==} peerDependencies: @@ -3070,8 +3115,8 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - ai@6.0.6: - resolution: {integrity: sha512-LM0eAMWVn3RTj+0X5O1m/8g+7QiTeWG5aN5FsDbdmCkAQHVg93XxLbljFOLzi0NMjuJgf7fKLKmWoPsrdMyqfw==} + ai@6.0.116: + resolution: {integrity: sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -3828,6 +3873,26 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flags@4.0.4: + resolution: {integrity: sha512-JZtN48U/rxUm+ysvVq/7Iqc37U512XBPbkrbhWt7Jw887x5zd/Plr6wqax2cd3VcPnpBgud2QLlCYkKOU9Yoiw==} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + '@sveltejs/kit': '*' + next: '*' + react: '*' + react-dom: '*' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@sveltejs/kit': + optional: true + next: + optional: true + react: + optional: true + react-dom: + optional: true + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -4254,6 +4319,12 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + + jose@5.2.1: + resolution: {integrity: sha512-qiaQhtQRw6YrOaOj0v59h3R6hUY9NvxBmmnMfKemkqYmBB0tEc97NbLP7ix44VP5p9/0YHG8Vyhzuo5YBNwviA==} + jotai@2.17.1: resolution: {integrity: sha512-TFNZZDa/0ewCLQyRC/Sq9crtixNj/Xdf/wmj9631xxMuKToVJZDbqcHIYN0OboH+7kh6P6tpIK7uKWClj86PKw==} engines: {node: '>=12.20.0'} @@ -4282,6 +4353,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-xxhash@4.0.0: + resolution: {integrity: sha512-3Q2eIqG6s1KEBBmkj9tGM9lef8LJbuRyTVBdI3GpTnrvtytunjLPO0wqABp5qhtMzfA32jYn1FlnIV7GH1RAHQ==} + engines: {node: '>=18.0.0'} + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -5999,6 +6074,12 @@ packages: snapshots: + '@ai-sdk/anthropic@3.0.58(zod@4.3.3)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.3) + zod: 4.3.3 + '@ai-sdk/gateway@2.0.39(zod@4.3.3)': dependencies: '@ai-sdk/provider': 2.0.1 @@ -6006,11 +6087,11 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 4.3.3 - '@ai-sdk/gateway@3.0.5(zod@4.3.3)': + '@ai-sdk/gateway@3.0.66(zod@4.3.3)': dependencies: - '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.3.3) - '@vercel/oidc': 3.0.5 + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.3) + '@vercel/oidc': 3.1.0 zod: 4.3.3 '@ai-sdk/provider-utils@3.0.21(zod@4.3.3)': @@ -6020,9 +6101,9 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.3.3 - '@ai-sdk/provider-utils@4.0.2(zod@4.3.3)': + '@ai-sdk/provider-utils@4.0.19(zod@4.3.3)': dependencies: - '@ai-sdk/provider': 3.0.1 + '@ai-sdk/provider': 3.0.8 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 zod: 4.3.3 @@ -6031,7 +6112,7 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/provider@3.0.1': + '@ai-sdk/provider@3.0.8': dependencies: json-schema: 0.4.0 @@ -6426,6 +6507,8 @@ snapshots: - bufferutil - utf-8-validate + '@edge-runtime/cookies@5.0.2': {} + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -6525,6 +6608,15 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true + '@flags-sdk/vercel@1.0.2(flags@4.0.4(@opentelemetry/api@1.9.0)(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + dependencies: + '@vercel/flags-core': 1.1.0(flags@4.0.4(@opentelemetry/api@1.9.0)(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + flags: 4.0.4(@opentelemetry/api@1.9.0)(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + transitivePeerDependencies: + - '@aws-sdk/credential-provider-web-identity' + - '@openfeature/server-sdk' + - next + '@floating-ui/core@1.7.4': dependencies: '@floating-ui/utils': 0.2.10 @@ -8319,10 +8411,25 @@ snapshots: next: 16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 - '@vercel/oidc@3.0.5': {} + '@vercel/flags-core@1.1.0(flags@4.0.4(@opentelemetry/api@1.9.0)(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + dependencies: + '@vercel/functions': 3.4.3 + flags: 4.0.4(@opentelemetry/api@1.9.0)(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + jose: 5.2.1 + js-xxhash: 4.0.0 + optionalDependencies: + next: 16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + transitivePeerDependencies: + - '@aws-sdk/credential-provider-web-identity' + + '@vercel/functions@3.4.3': + dependencies: + '@vercel/oidc': 3.2.0 '@vercel/oidc@3.1.0': {} + '@vercel/oidc@3.2.0': {} + '@vercel/speed-insights@1.3.1(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': optionalDependencies: next: 16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8408,11 +8515,11 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.3 - ai@6.0.6(zod@4.3.3): + ai@6.0.116(zod@4.3.3): dependencies: - '@ai-sdk/gateway': 3.0.5(zod@4.3.3) - '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.3.3) + '@ai-sdk/gateway': 3.0.66(zod@4.3.3) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.3) '@opentelemetry/api': 1.9.0 zod: 4.3.3 @@ -9282,6 +9389,16 @@ snapshots: mlly: 1.8.0 rollup: 4.54.0 + flags@4.0.4(@opentelemetry/api@1.9.0)(next@16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@edge-runtime/cookies': 5.0.2 + jose: 5.10.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + next: 16.1.5(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + follow-redirects@1.15.11: {} foreground-child@3.3.1: @@ -9843,6 +9960,10 @@ snapshots: jiti@2.6.1: {} + jose@5.10.0: {} + + jose@5.2.1: {} + jotai@2.17.1(@types/react@19.2.7)(react@19.2.3): optionalDependencies: '@types/react': 19.2.7 @@ -9854,6 +9975,8 @@ snapshots: js-tokens@4.0.0: {} + js-xxhash@4.0.0: {} + js-yaml@3.14.2: dependencies: argparse: 1.0.10 diff --git a/turbo.json b/turbo.json index b9c56a82..016e56e6 100644 --- a/turbo.json +++ b/turbo.json @@ -4,6 +4,9 @@ "NODE_ENV", "SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET", + "DISCORD_BOT_TOKEN", + "DISCORD_PUBLIC_KEY", + "DISCORD_APPLICATION_ID", "TEAMS_APP_ID", "TEAMS_APP_PASSWORD", "TEAMS_APP_TENANT_ID", @@ -15,7 +18,11 @@ "WHATSAPP_PHONE_NUMBER_ID", "WHATSAPP_VERIFY_TOKEN", "BOT_USERNAME", - "REDIS_URL" + "REDIS_URL", + "FLAGS", + "FLAGS_SECRET", + "TELEGRAM_BOT_TOKEN", + "TELEGRAM_WEBHOOK_SECRET_TOKEN" ], "tasks": { "build": {