diff --git a/apps/mail/app/(routes)/settings/connections/page.tsx b/apps/mail/app/(routes)/settings/connections/page.tsx index 018a3c271a..d400986c53 100644 --- a/apps/mail/app/(routes)/settings/connections/page.tsx +++ b/apps/mail/app/(routes)/settings/connections/page.tsx @@ -7,36 +7,78 @@ import { DialogTrigger, DialogClose, } from '@/components/ui/dialog'; +import { AddArcadeConnectionDialog } from '@/components/connection/add-arcade-connection'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { useArcadeConnections } from '@/hooks/use-arcade-connections'; import { SettingsCard } from '@/components/settings/settings-card'; import { AddConnectionDialog } from '@/components/connection/add'; - +import { Trash, Plus, Unplug, Sparkles } from 'lucide-react'; import { useSession, authClient } from '@/lib/auth-client'; import { useConnections } from '@/hooks/use-connections'; import { useTRPC } from '@/providers/query-provider'; import { Skeleton } from '@/components/ui/skeleton'; import { useMutation } from '@tanstack/react-query'; -import { Trash, Plus, Unplug } from 'lucide-react'; import { useThreads } from '@/hooks/use-threads'; import { useBilling } from '@/hooks/use-billing'; import { emailProviders } from '@/lib/constants'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { useState, useEffect } from 'react'; import { m } from '@/paraglide/messages'; import { useQueryState } from 'nuqs'; -import { useState } from 'react'; import { toast } from 'sonner'; export default function ConnectionsPage() { const { data, isLoading, refetch: refetchConnections } = useConnections(); + const { + connections: arcadeConnections, + isLoading: arcadeLoading, + refetch: refetchArcadeConnections, + revokeAuthorization, + } = useArcadeConnections(); const { refetch } = useSession(); const [openTooltip, setOpenTooltip] = useState(null); const trpc = useTRPC(); const { mutateAsync: deleteConnection } = useMutation(trpc.connections.delete.mutationOptions()); + const { mutateAsync: createArcadeConnection } = useMutation( + trpc.arcadeConnections.createConnection.mutationOptions(), + ); const [{ refetch: refetchThreads }] = useThreads(); const { isPro } = useBilling(); const [, setPricingDialog] = useQueryState('pricingDialog'); + const [arcadeAuthSuccess] = useQueryState('arcade_auth_success'); + const [toolkit] = useQueryState('toolkit'); + const [authId] = useQueryState('auth_id'); + const [error] = useQueryState('error'); + + useEffect(() => { + if (arcadeAuthSuccess === 'true' && toolkit && authId) { + createArcadeConnection({ toolkit, authId }) + .then(() => { + toast.success(`Successfully connected ${toolkit}`); + void refetchArcadeConnections(); + window.history.replaceState({}, document.title, window.location.pathname); + }) + .catch((err) => { + console.error('Failed to create Arcade connection:', err); + toast.error(`Failed to connect ${toolkit}`); + }); + } else if (error) { + let errorMessage = 'Authentication failed'; + if (error === 'arcade_auth_failed') { + errorMessage = 'Arcade authorization failed'; + } else if (error === 'arcade_auth_incomplete') { + errorMessage = 'Authorization was not completed'; + } else if (error === 'arcade_verification_failed') { + errorMessage = 'User verification failed'; + } else if (error === 'arcade_auth_error') { + errorMessage = 'An error occurred during authentication'; + } + toast.error(errorMessage); + window.history.replaceState({}, document.title, window.location.pathname); + } + }, [arcadeAuthSuccess, toolkit, authId, error, createArcadeConnection, refetchArcadeConnections]); const disconnectAccount = async (connectionId: string) => { await deleteConnection( { connectionId }, @@ -55,10 +97,7 @@ export default function ConnectionsPage() { return (
- +
{isLoading ? (
@@ -228,6 +267,123 @@ export default function ConnectionsPage() {
+ + +
+ {arcadeLoading ? ( +
+ {[...Array(3)].map((n) => ( +
+
+ +
+ + +
+
+ +
+ ))} +
+ ) : arcadeConnections.length > 0 ? ( +
+ {arcadeConnections.map((connection) => ( +
+
+
+ +
+
+ + {connection.toolkit} + +
+ + Connected + + + {connection.authorizedAt && + new Date(connection.authorizedAt).toLocaleDateString()} + +
+
+
+ + + + + + + Disconnect {connection.toolkit} + + Are you sure you want to disconnect this integration? + + +
+ + + + + + +
+
+
+
+ ))} +
+ ) : ( +
+ +

No integrations connected

+

+ Connect to external services to access powerful AI tools +

+
+ )} + +
+ void refetchArcadeConnections()}> + + +
+
+
); } diff --git a/apps/mail/components/ai/arcade-auth-dialog.tsx b/apps/mail/components/ai/arcade-auth-dialog.tsx new file mode 100644 index 0000000000..08e51a4149 --- /dev/null +++ b/apps/mail/components/ai/arcade-auth-dialog.tsx @@ -0,0 +1,124 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '../ui/dialog'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { useTRPC } from '@/providers/query-provider'; +import { ExternalLink, Loader2 } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import { Button } from '../ui/button'; + +interface ArcadeAuthDialogProps { + toolName: string | null; + onAuthorized?: () => void; + onCancel?: () => void; +} + +export function ArcadeAuthDialog({ toolName, onAuthorized, onCancel }: ArcadeAuthDialogProps) { + const [isWaiting, setIsWaiting] = useState(false); + const [authId, setAuthId] = useState(null); + const trpc = useTRPC(); + + // Check if tool needs authorization + const { data: authStatus } = useQuery( + trpc.arcadeConnections.checkAuthorization.queryOptions( + { toolName: toolName || '' }, + { + enabled: !!toolName, + }, + ), + ); + + // Wait for authorization mutation + const { mutateAsync: waitForAuth } = useMutation( + trpc.arcadeConnections.waitForAuthorization.mutationOptions(), + ); + + useEffect(() => { + if (authStatus?.authId && authStatus.authId !== authId) { + setAuthId(authStatus.authId); + } + }, [authStatus?.authId, authId]); + + const handleAuthorize = () => { + if (authStatus?.authUrl) { + window.open(authStatus.authUrl, '_blank'); + setIsWaiting(true); + + // Start polling for authorization completion + if (authId) { + waitForAuth({ authId }) + .then((result) => { + if (result.success) { + onAuthorized?.(); + } + }) + .catch(() => { + setIsWaiting(false); + }); + } + } + }; + + const handleCancel = () => { + setIsWaiting(false); + onCancel?.(); + }; + + if (!toolName || !authStatus?.needsAuth) { + return null; + } + + return ( + !open && handleCancel()} + > + + + Authorization Required + + The AI assistant needs your permission to use {toolName}. This will + allow the assistant to perform actions on your behalf. + + + +
+ {isWaiting ? ( +
+ +

+ Waiting for authorization... Please complete the authorization in the new window. +

+
+ ) : ( +
+

+ Click the button below to authorize this tool. A new window will open where you can + grant the necessary permissions. +

+ + {authStatus.error &&

{authStatus.error}

} +
+ )} +
+ + + + {!isWaiting && ( + + )} + +
+
+ ); +} diff --git a/apps/mail/components/connection/add-arcade-connection.tsx b/apps/mail/components/connection/add-arcade-connection.tsx new file mode 100644 index 0000000000..cd160d8ea7 --- /dev/null +++ b/apps/mail/components/connection/add-arcade-connection.tsx @@ -0,0 +1,171 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../ui/dialog'; +import { useArcadeConnections } from '@/hooks/use-arcade-connections'; +import { Gmail, GitHub, Slack, Linear, Stripe } from '../icons/icons'; +import { Loader2, CheckCircle2, Sparkles } from 'lucide-react'; +import { useTRPC } from '@/providers/query-provider'; +import { useMutation } from '@tanstack/react-query'; +import { Button } from '../ui/button'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +// Arcade toolkit icons mapping +const toolkitIcons: Record> = { + gmail: Gmail, + github: GitHub, + slack: Slack, + notion: Sparkles, // Notion icon not available, using Sparkles as fallback + linear: Linear, + stripe: Stripe, +}; + +export const AddArcadeConnectionDialog = ({ + children, + // onSuccess, +}: { + children?: React.ReactNode; + onSuccess?: () => void; +}) => { + const [isOpen, setIsOpen] = useState(false); + const [connectingToolkit, setConnectingToolkit] = useState(null); + const { toolkits, connections, isLoading, authorizeToolkit } = useArcadeConnections(); + const trpc = useTRPC(); + const { mutateAsync: createConnection } = useMutation( + trpc.arcadeConnections.createConnection.mutationOptions(), + ); + + const handleConnect = async (toolkit: string) => { + setConnectingToolkit(toolkit); + try { + const authResult = await authorizeToolkit(toolkit); + if (authResult?.authUrl && authResult?.authId) { + // Open the authorization URL in a new window + // window.location.href = authResult.authUrl; + const authWindow = window.open(authResult.authUrl, '_blank', 'width=600,height=600'); + + // Poll to check if the window is closed and authorization is complete + const checkInterval = setInterval(async () => { + if (authWindow?.closed) { + clearInterval(checkInterval); + + // Try to create the connection + try { + await createConnection({ + toolkit, + authId: authResult.authId, + }); + + toast.success(`Successfully connected ${toolkit}`); + setConnectingToolkit(null); + // onSuccess?.(); + } catch { + // Authorization might not be complete yet + console.log('Authorization not complete or failed'); + setConnectingToolkit(null); + } + } + }, 1000); + + // Also set a timeout to stop checking after 5 minutes + setTimeout( + () => { + clearInterval(checkInterval); + setConnectingToolkit(null); + }, + 5 * 60 * 1000, + ); + } + } catch (error) { + console.error('Failed to connect toolkit:', error); + toast.error(`Failed to connect ${toolkit}`); + setConnectingToolkit(null); + } + }; + + const isConnected = (toolkit: string) => { + return connections.some((c) => c.toolkit === toolkit); + }; + + return ( + + {children} + + + Add Arcade Integration + + Connect to external services through Arcade to enhance Zero Mail with AI-powered tools + + + + {isLoading ? ( +
+ +
+ ) : toolkits.length === 0 ? ( +
+ +

No integrations available

+

Please check your Arcade API key configuration

+
+ ) : ( +
+ {toolkits.map((toolkit) => { + const Icon = toolkitIcons[toolkit.name] || Sparkles; + const connected = isConnected(toolkit.name); + + return ( +
+
+
+ +
+
+

{toolkit.name}

+

{toolkit.description}

+

+ {toolkit.toolCount} tools available +

+
+ {connected ? ( + + ) : ( + + )} +
+
+ ); + })} +
+ )} +
+
+ ); +}; diff --git a/apps/mail/components/icons/icons.tsx b/apps/mail/components/icons/icons.tsx index 0a13e7c352..c0c0be3ca5 100644 --- a/apps/mail/components/icons/icons.tsx +++ b/apps/mail/components/icons/icons.tsx @@ -165,14 +165,42 @@ export const Google = ({ className }: { className?: string }) => ( + /> ); export const GitHub = ({ className }: { className?: string }) => ( GitHub - + + +); + +export const Slack = ({ className }: { className?: string }) => ( + + + +); + +export const Linear = ({ className }: { className?: string }) => ( + + ); @@ -193,6 +221,15 @@ export const YouTube = ({ className }: { className?: string }) => ( ); +export const Stripe = ({ className }: { className?: string }) => ( + + + +); + export const CurvedArrow = ({ className }: { className?: string }) => ( ( (null); + const [isAuthorizing, setIsAuthorizing] = useState(false); + + const checkAuth = useCallback(async (toolName: string) => { + return await trpcClient.arcadeConnections.checkAuthorization.query({ toolName }); + }, []); + + const requestAuthorization = useCallback( + async (toolName: string, message?: string) => { + const authStatus = await checkAuth(toolName); + + if (authStatus.needsAuth) { + setPendingAuth({ toolName, message }); + return { needsAuth: true, authUrl: authStatus.authUrl, authId: authStatus.authId }; + } + + return { needsAuth: false }; + }, + [checkAuth], + ); + + const handleAuthorization = useCallback(async () => { + if (!pendingAuth) return; + + setIsAuthorizing(true); + try { + setPendingAuth(null); + return true; + } finally { + setIsAuthorizing(false); + } + }, [pendingAuth]); + + const handleCancel = useCallback(() => { + setPendingAuth(null); + setIsAuthorizing(false); + }, []); + + const clearPendingAuth = useCallback(() => { + setPendingAuth(null); + setIsAuthorizing(false); + }, []); + + return { + pendingAuth, + isAuthorizing, + requestAuthorization, + handleAuthorization, + handleCancel, + clearPendingAuth, + }; +} diff --git a/apps/mail/hooks/use-arcade-connections.ts b/apps/mail/hooks/use-arcade-connections.ts new file mode 100644 index 0000000000..3a57c2e86c --- /dev/null +++ b/apps/mail/hooks/use-arcade-connections.ts @@ -0,0 +1,80 @@ +import { useQuery, useMutation } from '@tanstack/react-query'; +import { useTRPC } from '../providers/query-provider'; +import { useMemo } from 'react'; + +export interface ArcadeConnection { + id: string; + userId: string; + toolkit: string; + status: 'connected' | 'error'; + authorizedAt: string; + createdAt: string; + updatedAt: string; +} + +export interface ArcadeToolkit { + name: string; + description: string; + toolCount: number; + icon?: string; +} + +export function useArcadeConnections() { + const trpc = useTRPC(); + + const { data: toolkitsData, isLoading: toolkitsLoading } = useQuery( + trpc.arcadeConnections.toolkits.queryOptions(), + ); + + const toolkits = useMemo(() => { + return (toolkitsData?.toolkits || []).map((toolkit) => ({ + ...toolkit, + icon: getToolkitIcon(toolkit.name), + })); + }, [toolkitsData]); + + const { + data: connections, + isLoading: connectionsLoading, + refetch, + } = useQuery(trpc.arcadeConnections.list.queryOptions()); + + const { mutateAsync: getAuthUrl } = useMutation( + trpc.arcadeConnections.getAuthUrl.mutationOptions(), + ); + + const { mutateAsync: authorizeToolkit } = useMutation({ + mutationFn: async (toolkit: string) => { + const result = await getAuthUrl({ toolkit }); + return result; + }, + }); + + const { mutateAsync: revokeAuthorization } = useMutation( + trpc.arcadeConnections.revoke.mutationOptions(), + ); + + return { + toolkits, + connections: connections?.connections || [], + isLoading: connectionsLoading || toolkitsLoading, + refetch, + authorizeToolkit, + revokeAuthorization, + }; +} + +function getToolkitIcon(toolkit: string): string { + const icons: Record = { + gmail: 'gmail', + github: 'github', + slack: 'slack', + notion: 'notion', + linear: 'linear', + stripe: 'stripe', + hubspot: 'hubspot', + salesforce: 'salesforce', + }; + + return icons[toolkit.toLowerCase()] || 'default'; +} diff --git a/apps/mail/package.json b/apps/mail/package.json index 39c256e1b1..43ccdd4e91 100644 --- a/apps/mail/package.json +++ b/apps/mail/package.json @@ -15,6 +15,7 @@ "dependencies": { "@ai-sdk/perplexity": "1.1.9", "@ai-sdk/react": "1.2.12", + "@arcadeai/arcadejs": "1.9.0", "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "10.0.0", @@ -25,6 +26,7 @@ "@fontsource-variable/geist-mono": "5.2.6", "@hookform/resolvers": "4.1.2", "@intercom/messenger-js-sdk": "0.0.14", + "@langchain/core": "0.3.72", "@react-email/components": "^0.0.36", "@react-email/html": "^0.0.11", "@react-email/render": "1.1.0", diff --git a/apps/server/package.json b/apps/server/package.json index c05f5a4eb7..aa3eaa092d 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -28,7 +28,7 @@ "@ai-sdk/openai": "^1.3.21", "@ai-sdk/perplexity": "1.1.9", "@ai-sdk/ui-utils": "1.2.11", - "@arcadeai/arcadejs": "1.8.1", + "@arcadeai/arcadejs": "1.9.0", "@barkleapp/css-sanitizer": "1.0.0", "@coinbase/cookie-manager": "1.1.8", "@datadog/datadog-api-client": "1.40.0", @@ -37,6 +37,8 @@ "@googleapis/gmail": "12.0.0", "@googleapis/people": "3.0.9", "@hono/trpc-server": "^0.3.4", + "@langchain/core": "0.3.72", + "@langchain/openai": "0.6.9", "@microlabs/otel-cf-workers": "1.0.0-rc.52", "@microsoft/microsoft-graph-client": "^3.0.7", "@microsoft/microsoft-graph-types": "^2.40.0", diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts index 3e71b48acc..ba182cc1c1 100644 --- a/apps/server/src/db/schema.ts +++ b/apps/server/src/db/schema.ts @@ -322,3 +322,44 @@ export const emailTemplate = createTable( unique('mail0_email_template_user_id_name_unique').on(t.userId, t.name), ], ); + +export const arcadeConnection = createTable( + 'arcade_connection', + { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + toolkit: text('toolkit').notNull(), // e.g. 'gmail', 'github', 'slack' + status: text('status').$type<'connected' | 'error'>().notNull().default('connected'), + accessToken: text('access_token'), + refreshToken: text('refresh_token'), + expiresAt: timestamp('expires_at'), + authorizedAt: timestamp('authorized_at').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (t) => [ + index('arcade_connection_user_id_idx').on(t.userId), + index('arcade_connection_toolkit_idx').on(t.toolkit), + index('arcade_connection_status_idx').on(t.status), + unique('arcade_connection_user_toolkit_unique').on(t.userId, t.toolkit), + ], +); + +export const arcadeAuthState = createTable( + 'arcade_auth_state', + { + state: text('state').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + toolkit: text('toolkit').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + expiresAt: timestamp('expires_at').notNull(), // Clean up old states + }, + (t) => [ + index('arcade_auth_state_user_id_idx').on(t.userId), + index('arcade_auth_state_expires_at_idx').on(t.expiresAt), + ], +); diff --git a/apps/server/src/env.ts b/apps/server/src/env.ts index 4a7b37125e..46f0a99ea9 100644 --- a/apps/server/src/env.ts +++ b/apps/server/src/env.ts @@ -82,6 +82,8 @@ export type ZeroEnv = { MICROSOFT_CLIENT_SECRET: string; VOICE_SECRET: string; ARCADE_API_KEY: string; + ARCADE_CLIENT_ID: string; + ARCADE_CLIENT_SECRET: string; OPENAI_MODEL: string; OPENAI_MINI_MODEL: string; ANTHROPIC_API_KEY: string; diff --git a/apps/server/src/lib/arcade-auth-handler.ts b/apps/server/src/lib/arcade-auth-handler.ts new file mode 100644 index 0000000000..2696242873 --- /dev/null +++ b/apps/server/src/lib/arcade-auth-handler.ts @@ -0,0 +1,130 @@ +import Arcade from '@arcadeai/arcadejs'; + +export interface AuthorizationState { + toolName: string; + userId: string; + authId?: string; + authUrl?: string; + status: 'pending' | 'completed' | 'failed'; +} + +export class ArcadeAuthHandler { + private arcade: Arcade; + private pendingAuthorizations: Map = new Map(); + + constructor(apiKey: string) { + this.arcade = new Arcade({ apiKey }); + } + + async requiresAuth(toolName: string, userId: string): Promise { + const cacheKey = `${userId}:${toolName}`; + + const cached = this.pendingAuthorizations.get(cacheKey); + if (cached && cached.status === 'completed') { + return cached; + } + + try { + const authResponse = await this.arcade.tools.authorize({ + tool_name: toolName, + user_id: userId, + }); + + const state: AuthorizationState = { + toolName, + userId, + authId: authResponse.id, + authUrl: authResponse.url, + status: authResponse.status === 'completed' ? 'completed' : 'pending', + }; + + this.pendingAuthorizations.set(cacheKey, state); + return state; + } catch (error) { + console.error(`[ArcadeAuthHandler] Error checking auth for ${toolName}:`, error); + return { + toolName, + userId, + status: 'failed', + }; + } + } + + async waitForAuthorization(authId: string, userId: string): Promise { + try { + const response = await this.arcade.auth.waitForCompletion(authId); + + if (response.status === 'completed') { + // Update cache for all tools that might have been authorized + for (const [key, state] of this.pendingAuthorizations.entries()) { + if (state.userId === userId && state.authId === authId) { + state.status = 'completed'; + this.pendingAuthorizations.set(key, state); + } + } + return true; + } + + return false; + } catch (error) { + console.error(`[ArcadeAuthHandler] Error waiting for authorization:`, error); + return false; + } + } + + getPendingAuthorizations(userId: string): AuthorizationState[] { + const pending: AuthorizationState[] = []; + + for (const state of this.pendingAuthorizations.values()) { + if (state.userId === userId && state.status === 'pending') { + pending.push(state); + } + } + + return pending; + } + + clearUserCache(userId: string) { + for (const [key, state] of this.pendingAuthorizations.entries()) { + if (state.userId === userId) { + this.pendingAuthorizations.delete(key); + } + } + } + + async authorizeTools( + toolNames: string[], + userId: string, + ): Promise<{ + authorized: string[]; + pending: AuthorizationState[]; + failed: string[]; + }> { + const authorized: string[] = []; + const pending: AuthorizationState[] = []; + const failed: string[] = []; + + for (const toolName of toolNames) { + const authState = await this.requiresAuth(toolName, userId); + + if (authState.status === 'completed') { + authorized.push(toolName); + } else if (authState.status === 'pending') { + pending.push(authState); + } else { + failed.push(toolName); + } + } + + return { authorized, pending, failed }; + } +} + +let authHandler: ArcadeAuthHandler | null = null; + +export function getArcadeAuthHandler(apiKey: string): ArcadeAuthHandler { + if (!authHandler) { + authHandler = new ArcadeAuthHandler(apiKey); + } + return authHandler; +} diff --git a/apps/server/src/lib/arcade-loader.ts b/apps/server/src/lib/arcade-loader.ts new file mode 100644 index 0000000000..af95402253 --- /dev/null +++ b/apps/server/src/lib/arcade-loader.ts @@ -0,0 +1,238 @@ +import { DynamicStructuredTool } from '@langchain/core/tools'; +import { getArcadeAuthHandler } from './arcade-auth-handler'; +import { arcadeConnection } from '../db/schema'; +import Arcade from '@arcadeai/arcadejs'; +import { eq } from 'drizzle-orm'; +import { createDb } from '../db'; +import { z } from 'zod'; + +type LangChainTool = DynamicStructuredTool; + +export interface ArcadeToolsResult { + tools: Record; + authHandler: ReturnType; +} + +/** + * Load Arcade tools for a specific user with authorization handling + * + * Based on the authorization pattern from: https://docs.arcade.dev/home/langchain/user-auth-interrupts + * + * When a tool requires authorization: + * 1. The tool execution returns a special response with needsAuth=true + * 2. The response includes an authUrl for the user to authorize + * 3. The frontend can display this URL to the user + * 4. Once authorized, the tool can be executed normally + */ +export async function loadUserArcadeTools( + userId: string, + connectionString: string, + arcadeApiKey?: string, +): Promise { + if (!arcadeApiKey) { + console.warn('[loadUserArcadeTools] No Arcade API key configured'); + return { + tools: {}, + authHandler: getArcadeAuthHandler(''), + }; + } + + const arcade = new Arcade({ + apiKey: arcadeApiKey, + }); + + const authHandler = getArcadeAuthHandler(arcadeApiKey); + const { db, conn } = createDb(connectionString); + + try { + const connections = await db.query.arcadeConnection.findMany({ + where: eq(arcadeConnection.userId, userId), + }); + + if (connections.length === 0) { + console.log(`[loadUserArcadeTools] No Arcade connections found for user ${userId}`); + return { tools: {}, authHandler }; + } + + const toolkits = connections.map((c) => c.toolkit); + console.log(`[loadUserArcadeTools] Loading tools for toolkits: ${toolkits.join(', ')}`); + + const allTools: Record = {}; + + for (const connection of connections) { + const toolkit = connection.toolkit; + + const toolDefinitions = getToolkitDefinitions(toolkit); + + for (const toolDef of toolDefinitions) { + const toolkitMap: Record = { + github: 'GitHub', + linear: 'Linear', + stripe: 'Stripe', + }; + const arcadeToolkit = toolkitMap[toolkit.toLowerCase()] || toolkit; + const toolName = `${arcadeToolkit}.${toolDef.name}`; + + const langchainTool = new DynamicStructuredTool({ + name: toolName, + description: toolDef.description, + schema: toolDef.schema, + func: async (input) => { + try { + console.log(`[loadUserArcadeTools] Executing ${toolName} with input:`, input); + + // First check if the tool needs authorization + const authState = await authHandler.requiresAuth(toolName, userId); + + if (authState.status === 'pending') { + // Return a special response indicating authorization is needed + return JSON.stringify({ + success: false, + needsAuth: true, + toolName, + authUrl: authState.authUrl, + authId: authState.authId, + message: `Tool ${toolName} requires authorization. Please visit: ${authState.authUrl}`, + }); + } + + // Tool is authorized, execute it + const response = await arcade.tools.execute({ + tool_name: toolName, + input: input, + user_id: userId, + }); + + return JSON.stringify(response.output?.value || response.output || response); + } catch (error) { + console.error(`[loadUserArcadeTools] Error executing tool ${toolName}:`, error); + + // Handle authorization errors + if (error instanceof Error && error.message.toLowerCase().includes('auth')) { + // Try to get auth URL + const authState = await authHandler.requiresAuth(toolName, userId); + return JSON.stringify({ + success: false, + needsAuth: true, + toolName, + authUrl: authState.authUrl, + authId: authState.authId, + message: `Authorization required for ${toolName}`, + }); + } + + throw error; + } + }, + }); + + allTools[toolName] = langchainTool; + } + } + + console.log(`[loadUserArcadeTools] Total tools loaded: ${Object.keys(allTools).length}`); + return { tools: allTools, authHandler }; + } catch (error) { + console.error('[loadUserArcadeTools] Failed to load Arcade tools:', error); + return { tools: {}, authHandler }; + } finally { + await conn.end(); + } +} + +function getToolkitDefinitions(toolkit: string) { + const definitions: Record< + string, + Array<{ name: string; description: string; schema: z.ZodTypeAny }> + > = { + Gmail: [ + { + name: 'SendEmail', + description: 'Send an email using Gmail', + schema: z.object({ + to: z.string().describe('Recipient email address'), + subject: z.string().describe('Email subject'), + body: z.string().describe('Email body'), + }), + }, + { + name: 'SearchEmails', + description: 'Search for emails in Gmail', + schema: z.object({ + query: z.string().describe('Search query'), + maxResults: z.number().optional().default(10).describe('Maximum number of results'), + }), + }, + ], + GitHub: [ + { + name: 'CreateIssue', + description: 'Create a new issue in a GitHub repository', + schema: z.object({ + owner: z.string().describe('Repository owner'), + repo: z.string().describe('Repository name'), + title: z.string().describe('Issue title'), + body: z.string().describe('Issue body'), + }), + }, + { + name: 'SetStarred', + description: 'Star or unstar a GitHub repository', + schema: z.object({ + owner: z.string().describe('Repository owner'), + name: z.string().describe('Repository name'), + starred: z.boolean().describe('Whether to star (true) or unstar (false)'), + }), + }, + ], + Slack: [ + { + name: 'PostMessage', + description: 'Send a message to a Slack channel', + schema: z.object({ + channel: z.string().describe('Channel name or ID'), + text: z.string().describe('Message text'), + }), + }, + ], + Notion: [ + { + name: 'CreatePage', + description: 'Create a new page in Notion', + schema: z.object({ + title: z.string().describe('Page title'), + content: z.string().describe('Page content'), + }), + }, + ], + Linear: [ + { + name: 'CreateIssue', + description: 'Create a new issue in Linear', + schema: z.object({ + title: z.string().describe('Issue title'), + description: z.string().describe('Issue description'), + priority: z.number().optional().describe('Priority (1-4)'), + }), + }, + ], + Stripe: [ + { + name: 'ListCustomers', + description: 'List Stripe customers', + schema: z.object({ + limit: z.number().optional().default(10).describe('Number of customers to return'), + }), + }, + ], + }; + + const toolkitMap: Record = { + github: 'GitHub', + linear: 'Linear', + stripe: 'Stripe', + }; + + const arcadeToolkit = toolkitMap[toolkit.toLowerCase()] || toolkit; + return definitions[arcadeToolkit] || []; +} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 54d7d2f0d5..0c4baa3f8a 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -1,9 +1,3 @@ -import { - createUpdatedMatrixFromNewEmail, - initializeStyleMatrixFromEmail, - type EmailMatrix, - type WritingStyleMatrix, -} from './services/writing-style-service'; import { account, connection, @@ -14,7 +8,15 @@ import { userSettings, writingStyleMatrix, emailTemplate, + arcadeConnection, + arcadeAuthState, } from './db/schema'; +import { + createUpdatedMatrixFromNewEmail, + initializeStyleMatrixFromEmail, + type EmailMatrix, + type WritingStyleMatrix, +} from './services/writing-style-service'; import { toAttachmentFiles, type SerializedAttachment, @@ -31,12 +33,12 @@ import { oAuthDiscoveryMetadata } from 'better-auth/plugins'; import { EProviders, type IEmailSendBatch } from './types'; import { eq, and, desc, asc, inArray } from 'drizzle-orm'; import { ThinkingMCP } from './lib/sequential-thinking'; - import { contextStorage } from 'hono/context-storage'; import { defaultUserSettings } from './lib/schemas'; import { createLocalJWKSet, jwtVerify } from 'jose'; import { enableBrainFunction } from './lib/brain'; import { trpcServer } from '@hono/trpc-server'; +import { arcadeRouter } from './routes/arcade'; import { agentsMiddleware } from 'hono-agents'; import { ZeroMCP } from './routes/agent/mcp'; import { publicRouter } from './routes/auth'; @@ -200,6 +202,39 @@ export class DbRpcDO extends RpcTarget { async updateEmailTemplate(templateId: string, data: Partial) { return await this.mainDo.updateEmailTemplate(this.userId, templateId, data); } + + // Arcade connection methods + async findManyArcadeConnections(): Promise<(typeof arcadeConnection.$inferSelect)[]> { + return await this.mainDo.findManyArcadeConnections(this.userId); + } + + async findArcadeConnection( + connectionId: string, + ): Promise { + return await this.mainDo.findArcadeConnection(this.userId, connectionId); + } + + async createArcadeConnection(data: Omit) { + return await this.mainDo.createArcadeConnection(this.userId, data); + } + + async deleteArcadeConnection(connectionId: string) { + return await this.mainDo.deleteArcadeConnection(this.userId, connectionId); + } + + async storeArcadeAuthState(state: string, toolkit: string) { + return await this.mainDo.storeArcadeAuthState(state, this.userId, toolkit); + } + + async verifyArcadeAuthState( + state: string, + ): Promise { + return await this.mainDo.verifyArcadeAuthState(state); + } + + async deleteArcadeAuthState(state: string) { + return await this.mainDo.deleteArcadeAuthState(state); + } } class ZeroDB extends DurableObject { @@ -562,6 +597,74 @@ class ZeroDB extends DurableObject { .where(and(eq(emailTemplate.id, templateId), eq(emailTemplate.userId, userId))) .returning(); } + + // Arcade connection methods + async findManyArcadeConnections( + userId: string, + ): Promise<(typeof arcadeConnection.$inferSelect)[]> { + return await this.db.query.arcadeConnection.findMany({ + where: eq(arcadeConnection.userId, userId), + orderBy: desc(arcadeConnection.createdAt), + }); + } + + async findArcadeConnection( + userId: string, + connectionId: string, + ): Promise { + return await this.db.query.arcadeConnection.findFirst({ + where: and(eq(arcadeConnection.userId, userId), eq(arcadeConnection.id, connectionId)), + }); + } + + async createArcadeConnection( + userId: string, + data: Omit, + ) { + return await this.db + .insert(arcadeConnection) + .values({ + ...data, + userId, + }) + .returning(); + } + + async deleteArcadeConnection(userId: string, connectionId: string) { + return await this.db + .delete(arcadeConnection) + .where(and(eq(arcadeConnection.userId, userId), eq(arcadeConnection.id, connectionId))); + } + + async storeArcadeAuthState(state: string, userId: string, toolkit: string) { + const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes + return await this.db.insert(arcadeAuthState).values({ + state, + userId, + toolkit, + expiresAt, + }); + } + + async verifyArcadeAuthState( + state: string, + ): Promise { + const authState = await this.db.query.arcadeAuthState.findFirst({ + where: eq(arcadeAuthState.state, state), + }); + + if (authState && authState.expiresAt < new Date()) { + // Expired, delete it + await this.deleteArcadeAuthState(state); + return undefined; + } + + return authState; + } + + async deleteArcadeAuthState(state: string) { + return await this.db.delete(arcadeAuthState).where(eq(arcadeAuthState.state, state)); + } } // Utility function to hash IP addresses for PII protection @@ -576,7 +679,7 @@ function hashIpAddress(ip: string | undefined): string | undefined { for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; + hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } @@ -610,13 +713,18 @@ const api = new Hono() }); // Start authentication span - const authSpan = TraceContext.startSpan(traceId, 'authentication', { - method: c.req.method, - url: c.req.url, - hasAuthHeader: !!c.req.header('Authorization'), - }, { - 'auth.method': c.req.header('Authorization') ? 'bearer_token' : 'session_cookie' - }); + const authSpan = TraceContext.startSpan( + traceId, + 'authentication', + { + method: c.req.method, + url: c.req.url, + hasAuthHeader: !!c.req.header('Authorization'), + }, + { + 'auth.method': c.req.header('Authorization') ? 'bearer_token' : 'session_cookie', + }, + ); const auth = createAuth(); c.set('auth', auth); @@ -625,11 +733,16 @@ const api = new Hono() if (c.req.header('Authorization') && !session?.user) { // Start token verification span - const tokenSpan = TraceContext.startSpan(traceId, 'token_verification', { - tokenPresent: true, - }, { - 'auth.token_type': 'jwt' - }); + const tokenSpan = TraceContext.startSpan( + traceId, + 'token_verification', + { + tokenPresent: true, + }, + { + 'auth.token_type': 'jwt', + }, + ); const token = c.req.header('Authorization')?.split(' ')[1]; @@ -657,10 +770,15 @@ const api = new Hono() }); } } catch (error) { - TraceContext.completeSpan(traceId, tokenSpan.id, { - success: false, - reason: 'token_verification_failed', - }, error instanceof Error ? error.message : 'Unknown token error'); + TraceContext.completeSpan( + traceId, + tokenSpan.id, + { + success: false, + reason: 'token_verification_failed', + }, + error instanceof Error ? error.message : 'Unknown token error', + ); } } else { TraceContext.completeSpan(traceId, tokenSpan.id, { @@ -674,7 +792,7 @@ const api = new Hono() TraceContext.completeSpan(traceId, authSpan.id, { authenticated: !!c.var.sessionUser, userId: c.var.sessionUser?.id, - authMethod: session?.user ? 'session' : (c.req.header('Authorization') ? 'token' : 'none'), + authMethod: session?.user ? 'session' : c.req.header('Authorization') ? 'token' : 'none', }); // Update trace metadata with user info @@ -691,11 +809,16 @@ const api = new Hono() await next(); // Don't complete the request span here - let TRPC middleware handle it } catch (error) { - TraceContext.completeSpan(traceId, requestSpan.id, { - success: false, + TraceContext.completeSpan( + traceId, + requestSpan.id, + { + success: false, - statusCode: c.res.status, - }, error instanceof Error ? error.message : 'Unknown request error'); + statusCode: c.res.status, + }, + error instanceof Error ? error.message : 'Unknown request error', + ); throw error; } // Note: Trace will be completed by TRPC middleware after logging @@ -706,6 +829,7 @@ const api = new Hono() .route('/ai', aiRouter) .route('/autumn', autumnApi) .route('/public', publicRouter) + .route('/arcade', arcadeRouter) .on(['GET', 'POST', 'OPTIONS'], '/auth/*', (c) => { return c.var.auth.handler(c.req.raw); }) diff --git a/apps/server/src/routes/agent/index.ts b/apps/server/src/routes/agent/index.ts index 3ad1d1b74c..85f10d4be8 100644 --- a/apps/server/src/routes/agent/index.ts +++ b/apps/server/src/routes/agent/index.ts @@ -45,8 +45,9 @@ import { import type { IGetThreadResponse, IGetThreadsResponse, MailManager } from '../../lib/driver/types'; import { connectionToDriver, getZeroSocketAgent, reSyncThread } from '../../lib/server-utils'; import { generateWhatUserCaresAbout, type UserTopic } from '../../lib/analyze/interests'; -import { DurableObjectOAuthClientProvider } from 'agents/mcp/do-oauth-client-provider'; + import { AiChatPrompt, GmailSearchAssistantSystemPrompt } from '../../lib/prompts'; +import { loadUserArcadeTools } from '../../lib/arcade-loader'; import { Migratable, Queryable, Transfer } from 'dormroom'; import type { CreateDraftData } from '../../lib/schemas'; import { drizzle } from 'drizzle-orm/durable-sqlite'; @@ -1700,32 +1701,10 @@ export class ZeroDriver extends DurableObject { export class ZeroAgent extends AIChatAgent { private chatMessageAbortControllers: Map = new Map(); - async registerZeroMCP() { - await this.mcp.connect(this.env.VITE_PUBLIC_BACKEND_URL + '/sse', { - transport: { - authProvider: new DurableObjectOAuthClientProvider( - this.ctx.storage, - 'zero-mcp', - this.env.VITE_PUBLIC_BACKEND_URL, - ), - }, - }); - } - - async registerThinkingMCP() { - await this.mcp.connect(this.env.VITE_PUBLIC_BACKEND_URL + '/mcp/thinking/sse', { - transport: { - authProvider: new DurableObjectOAuthClientProvider( - this.ctx.storage, - 'thinking-mcp', - this.env.VITE_PUBLIC_BACKEND_URL, - ), - }, - }); - } + // MCP registration methods removed - using Arcade instead onStart() { - this.registerThinkingMCP(); + // MCP disabled - using Arcade for integrations } async onConnect(connection: Connection): Promise { @@ -1753,11 +1732,33 @@ export class ZeroAgent extends AIChatAgent { const connectionId = this.name; const orchestrator = new ToolOrchestrator(dataStream, connectionId); - const mcpTools = this.mcp.unstable_getAITools(); + // Load user's Arcade tools + let userArcadeTools = {}; + + // Get the connection from the database to access userId + if (connectionId && connectionId !== 'general') { + const { db, conn } = createDb(this.env.HYPERDRIVE.connectionString); + try { + const connectionData = await db.query.connection.findFirst({ + where: eq(connection.id, connectionId), + }); + + if (connectionData?.userId) { + const arcadeResult = await loadUserArcadeTools( + connectionData.userId, + this.env.HYPERDRIVE.connectionString, + this.env.ARCADE_API_KEY, + ); + userArcadeTools = arcadeResult.tools; + } + } finally { + await conn.end(); + } + } const rawTools = { ...(await authTools(connectionId)), - ...mcpTools, + ...userArcadeTools, }; const tools = orchestrator.processTools(rawTools); diff --git a/apps/server/src/routes/arcade.ts b/apps/server/src/routes/arcade.ts new file mode 100644 index 0000000000..bbd4259a44 --- /dev/null +++ b/apps/server/src/routes/arcade.ts @@ -0,0 +1,155 @@ +import type { HonoContext } from '../ctx'; +import { createAuth } from '../lib/auth'; +import Arcade from '@arcadeai/arcadejs'; +import { env } from '../env'; +import { Hono } from 'hono'; + +export const arcadeRouter = new Hono() + .use('*', async (c, next) => { + // const { sessionUser } = c.var; + // c.set( + // 'customerData', + // !sessionUser + // ? null + // : { + // customerId: sessionUser.id, + // customerData: { + // name: sessionUser.name, + // email: sessionUser.email, + // }, + // }, + // ); + await next(); + }) + .get('/verify-user', async (c) => { + try { + // Extract flow_id from query parameters + const flowId = c.req.query('flow_id'); + + if (!flowId) { + console.error('[Arcade Verify User] Missing flow_id parameter'); + return c.json({ error: 'Missing required parameter: flow_id' }, 400); + } + + // Get the current user's session from Better Auth + const auth = createAuth(); + const session = await auth.api.getSession({ + headers: c.req.raw.headers, + }); + + if (!session || !session.user) { + console.error('[Arcade Verify User] No authenticated session found'); + return c.json({ error: 'Authentication required' }, 401); + } + + // Ensure Arcade API key is configured + if (!env.ARCADE_API_KEY) { + console.error('[Arcade Verify User] ARCADE_API_KEY not configured'); + return c.json({ error: 'Arcade integration not configured' }, 500); + } + + // Initialize Arcade client + const arcade = new Arcade({ apiKey: env.ARCADE_API_KEY }); + + try { + // Confirm the user's identity with Arcade + const result = await arcade.auth.confirmUser({ + flow_id: flowId, + user_id: session.user.id, + }); + + console.log('[Arcade Verify User] Successfully verified user', { + userId: session.user.id, + authId: result.auth_id, + user: result, + }); + + // Check the authorization status + + console.log('[Arcade Verify User] waiting for completion'); + + const authResponse = await arcade.auth.waitForCompletion(result.auth_id); + // const authResponse = await arcade.auth.status({ id: result.auth_id }); + + console.log('[Arcade Verify User] authResponse', authResponse); + + if (authResponse.status === 'completed') { + // const { mutateAsync: createConnection } = + // trpc.arcadeConnections.createConnection.mutationOptions(); + + // Authorization successful + // Extract toolkit/connection info if available + const toolkit = c.req.query('toolkit'); + + // Redirect to success page with appropriate parameters + const params = new URLSearchParams(); + params.set('arcade_auth_success', 'true'); + if (toolkit) { + params.set('toolkit', toolkit); + } + params.set('auth_id', result.auth_id); + + const redirectUrl = `${env.VITE_PUBLIC_APP_URL}/settings/connections?${params.toString()}`; + return c.redirect(redirectUrl); + } else { + console.error('[Arcade Verify User] Authorization not completed', { + status: authResponse.status, + }); + + return c.redirect( + `${env.VITE_PUBLIC_APP_URL}/settings/connections?error=arcade_auth_incomplete`, + ); + } + } catch (error) { + console.error('[Arcade Verify User] Error confirming user with Arcade:', error); + + // Check if it's a specific error from Arcade + if (error && typeof error === 'object' && 'status' in error) { + const statusCode = (error as { status: number }).status; + const errorData = (error as { status: number; data?: unknown }).data; + + console.error('[Arcade Verify User] Arcade API error details:', { + statusCode, + errorData, + }); + + // Redirect with error + return c.redirect( + `${env.VITE_PUBLIC_APP_URL}/settings/connections?error=arcade_verification_failed`, + ); + } + + // Generic error redirect + return c.redirect( + `${env.VITE_PUBLIC_APP_URL}/settings/connections?error=arcade_auth_error`, + ); + } + } catch (error) { + // Catch-all error handler + console.error('[Arcade Verify User] Unexpected error:', error); + return c.json({ error: 'Internal server error' }, 500); + } + }) + .get('/callback', async (c) => { + // Simple callback handler for Arcade SDK + // The actual authorization is handled by the Arcade SDK internally + // This is just a redirect endpoint after the user completes authorization + + const success = c.req.query('success'); + const error = c.req.query('error'); + const toolkit = c.req.query('toolkit'); + + if (error) { + console.error('Arcade authorization error:', error); + return c.redirect(`${env.VITE_PUBLIC_APP_URL}/settings/connections?error=arcade_auth_failed`); + } + + // Simply redirect back to the settings page + // The frontend will refresh the connections list when the auth window closes + const params = new URLSearchParams(); + if (success === 'true' && toolkit) { + params.set('arcade_connected', toolkit); + } + + return c.redirect(`${env.VITE_PUBLIC_APP_URL}/settings/connections?${params.toString()}`); + }); diff --git a/apps/server/src/trpc/index.ts b/apps/server/src/trpc/index.ts index 2df9cf237a..151033a798 100644 --- a/apps/server/src/trpc/index.ts +++ b/apps/server/src/trpc/index.ts @@ -1,4 +1,5 @@ import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; +import { arcadeConnectionsRouter } from './routes/arcade-connections'; import { cookiePreferencesRouter } from './routes/cookies'; import { connectionsRouter } from './routes/connections'; import { categoriesRouter } from './routes/categories'; @@ -6,6 +7,7 @@ import { templatesRouter } from './routes/templates'; import { shortcutRouter } from './routes/shortcut'; import { settingsRouter } from './routes/settings'; import { getContext } from 'hono/context-storage'; +import { loggingRouter } from './routes/logging'; import { draftsRouter } from './routes/drafts'; import { labelsRouter } from './routes/label'; import { notesRouter } from './routes/notes'; @@ -17,10 +19,10 @@ import { bimiRouter } from './routes/bimi'; import type { HonoContext } from '../ctx'; import { aiRouter } from './routes/ai'; import { router } from './trpc'; -import { loggingRouter } from './routes/logging'; export const appRouter = router({ ai: aiRouter, + arcadeConnections: arcadeConnectionsRouter, bimi: bimiRouter, brain: brainRouter, categories: categoriesRouter, diff --git a/apps/server/src/trpc/routes/arcade-connections.ts b/apps/server/src/trpc/routes/arcade-connections.ts new file mode 100644 index 0000000000..11a4732bd9 --- /dev/null +++ b/apps/server/src/trpc/routes/arcade-connections.ts @@ -0,0 +1,334 @@ +import { createRateLimiterMiddleware, privateProcedure, router } from '../trpc'; +import { getZeroDB } from '../../lib/server-utils'; +import { Ratelimit } from '@upstash/ratelimit'; +import { TRPCError } from '@trpc/server'; +import { z } from 'zod'; + +export const arcadeConnectionsRouter = router({ + checkAuthorization: privateProcedure + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(60, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:check-arcade-auth-${sessionUser?.id}`, + }), + ) + .input(z.object({ toolName: z.string() })) + .query(async ({ ctx, input }) => { + const { sessionUser } = ctx; + const { toolName } = input; + const env = ctx.c.env; + + if (!env.ARCADE_API_KEY) { + return { + needsAuth: false, + error: 'Arcade API key not configured', + }; + } + + const { getArcadeAuthHandler } = await import('../../lib/arcade-auth-handler'); + const authHandler = getArcadeAuthHandler(env.ARCADE_API_KEY); + + const authState = await authHandler.requiresAuth(toolName, sessionUser.id); + + return { + needsAuth: authState.status === 'pending', + authUrl: authState.authUrl, + authId: authState.authId, + status: authState.status, + }; + }), + + waitForAuthorization: privateProcedure + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(20, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:wait-arcade-auth-${sessionUser?.id}`, + }), + ) + .input(z.object({ authId: z.string() })) + .mutation(async ({ ctx, input }) => { + const { sessionUser } = ctx; + const { authId } = input; + const env = ctx.c.env; + + if (!env.ARCADE_API_KEY) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Arcade API key not configured', + }); + } + + const { getArcadeAuthHandler } = await import('../../lib/arcade-auth-handler'); + const authHandler = getArcadeAuthHandler(env.ARCADE_API_KEY); + + const success = await authHandler.waitForAuthorization(authId, sessionUser.id); + + return { success }; + }), + toolkits: privateProcedure + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(60, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:arcade-toolkits-${sessionUser?.id}`, + }), + ) + .query(async ({ ctx }) => { + const env = ctx.c.env; + + if (!env.ARCADE_API_KEY) { + return { toolkits: [] }; + } + + try { + const toolkits = [ + { + name: 'github', + description: 'Manage repositories, issues, and pull requests', + toolCount: 8, + }, + { name: 'linear', description: 'Track issues and manage projects', toolCount: 7 }, + { name: 'stripe', description: 'Access payment and customer data', toolCount: 10 }, + ]; + + return { toolkits }; + } catch (error) { + console.error('Error initializing Arcade client:', error); + return { toolkits: [] }; + } + }), + + list: privateProcedure + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(60, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:list-arcade-connections-${sessionUser?.id}`, + }), + ) + .query(async ({ ctx }) => { + const { sessionUser } = ctx; + const db = await getZeroDB(sessionUser.id); + + const connections = await db.findManyArcadeConnections(); + + return { + connections: connections.map((connection) => ({ + id: connection.id, + userId: connection.userId, + toolkit: connection.toolkit, + status: connection.status, + authorizedAt: connection.authorizedAt.toISOString(), + createdAt: connection.createdAt.toISOString(), + updatedAt: connection.updatedAt.toISOString(), + })), + }; + }), + + getAuthUrl: privateProcedure + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(20, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:arcade-auth-url-${sessionUser?.id}`, + }), + ) + .input(z.object({ toolkit: z.string() })) + .mutation(async ({ ctx, input }) => { + const { sessionUser } = ctx; + const { toolkit } = input; + const env = ctx.c.env; + + if (!env.ARCADE_API_KEY) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Arcade API key not configured', + }); + } + + // Use Arcade SDK to get the proper authorization URL + const { getArcadeAuthHandler } = await import('../../lib/arcade-auth-handler'); + const authHandler = getArcadeAuthHandler(env.ARCADE_API_KEY); + + // Map toolkit names to proper Arcade tool names + const toolkitMap: Record = { + gmail: 'Gmail.SendEmail', // Use a specific tool from the toolkit + github: 'GitHub.CreateIssue', + slack: 'Slack.SendMessage', + notion: 'Notion.CreatePage', + linear: 'Linear.CreateIssue', + stripe: 'Stripe.CreateCustomer', + }; + + const toolName = toolkitMap[toolkit.toLowerCase()] || `${toolkit}.Default`; + const authState = await authHandler.requiresAuth(toolName, sessionUser.id); + + if (!authState.authUrl || !authState.authId) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Failed to generate authorization URL', + }); + } + + return { + authUrl: authState.authUrl, + authId: authState.authId, + }; + }), + + // Create connection after successful authorization + createConnection: privateProcedure + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(20, '1m'), + generatePrefix: ({ sessionUser }) => + `ratelimit:arcade-create-connection-${sessionUser?.id}`, + }), + ) + .input( + z.object({ + toolkit: z.string(), + authId: z.string(), + }), + ) + .mutation(async ({ ctx, input }) => { + const { sessionUser } = ctx; + const { toolkit, authId } = input; + const env = ctx.c.env; + + if (!env.ARCADE_API_KEY) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Arcade API key not configured', + }); + } + + // Check if authorization is complete + const { getArcadeAuthHandler } = await import('../../lib/arcade-auth-handler'); + const authHandler = getArcadeAuthHandler(env.ARCADE_API_KEY); + + const success = await authHandler.waitForAuthorization(authId, sessionUser.id); + + if (!success) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Authorization not completed', + }); + } + + // Create the connection record + const db = await getZeroDB(sessionUser.id); + const connectionId = crypto.randomUUID(); + + await db.createArcadeConnection({ + id: connectionId, + toolkit, + status: 'connected', + accessToken: 'arcade-sdk-managed', // Token is managed by Arcade SDK + refreshToken: null, + expiresAt: null, + authorizedAt: new Date(), + }); + + return { success: true, connectionId }; + }), + + revoke: privateProcedure + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(20, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:revoke-arcade-${sessionUser?.id}`, + }), + ) + .input(z.object({ id: z.string() })) + .mutation(async ({ ctx, input }) => { + const { sessionUser } = ctx; + const { id } = input; + + const db = await getZeroDB(sessionUser.id); + + const connection = await db.findArcadeConnection(id); + if (!connection || connection.userId !== sessionUser.id) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Connection not found', + }); + } + + await db.deleteArcadeConnection(id); + + return { success: true }; + }), + + handleCallback: privateProcedure + .input( + z.object({ + code: z.string(), + state: z.string(), + }), + ) + .mutation(async ({ ctx, input }) => { + const { sessionUser } = ctx; + const { code, state } = input; + + const db = await getZeroDB(sessionUser.id); + + const authState = await db.verifyArcadeAuthState(state); + if (!authState || authState.userId !== sessionUser.id) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Invalid authorization state', + }); + } + + const env = ctx.c.env; + if (!env.ARCADE_CLIENT_ID || !env.ARCADE_CLIENT_SECRET) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Arcade credentials not configured', + }); + } + + const tokenUrl = 'https://app.arcade.ai/oauth/token'; + const redirectUri = `${env.BETTER_AUTH_URL}/api/arcade/callback`; + + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: redirectUri, + client_id: env.ARCADE_CLIENT_ID, + client_secret: env.ARCADE_CLIENT_SECRET, + }), + }); + + if (!response.ok) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Failed to exchange code for tokens', + }); + } + + const tokens = (await response.json()) as { + access_token: string; + refresh_token?: string; + expires_in?: number; + }; + + const connectionId = crypto.randomUUID(); + await db.createArcadeConnection({ + id: connectionId, + toolkit: authState.toolkit, + status: 'connected', + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || null, + expiresAt: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000) : null, + authorizedAt: new Date(), + }); + + await db.deleteArcadeAuthState(state); + + return { success: true, connectionId }; + }), +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9de70c003..a559050a97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: '@ai-sdk/react': specifier: 1.2.12 version: 1.2.12(react@19.1.0)(zod@3.25.67) + '@arcadeai/arcadejs': + specifier: 1.9.0 + version: 1.9.0 '@dnd-kit/core': specifier: 6.3.1 version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -137,6 +140,9 @@ importers: '@intercom/messenger-js-sdk': specifier: 0.0.14 version: 0.0.14 + '@langchain/core': + specifier: 0.3.72 + version: 0.3.72(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67)) '@react-email/components': specifier: ^0.0.36 version: 0.0.36(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -505,8 +511,8 @@ importers: specifier: 1.2.11 version: 1.2.11(zod@3.25.67) '@arcadeai/arcadejs': - specifier: 1.8.1 - version: 1.8.1 + specifier: 1.9.0 + version: 1.9.0 '@barkleapp/css-sanitizer': specifier: 1.0.0 version: 1.0.0 @@ -531,6 +537,12 @@ importers: '@hono/trpc-server': specifier: ^0.3.4 version: 0.3.4(@trpc/server@11.4.3(typescript@5.8.3))(hono@4.8.3) + '@langchain/core': + specifier: 0.3.72 + version: 0.3.72(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67)) + '@langchain/openai': + specifier: 0.6.9 + version: 0.6.9(@langchain/core@0.3.72(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67)))(ws@8.18.0) '@microlabs/otel-cf-workers': specifier: 1.0.0-rc.52 version: 1.0.0-rc.52(@opentelemetry/api@1.9.0) @@ -892,8 +904,8 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@arcadeai/arcadejs@1.8.1': - resolution: {integrity: sha512-ZTj2UvdfFmFn1as4gdDiZD8nbnEFZcZUzH9XtTmjRbgf/1V8s1wEtlzlI3vct+dA+KZ+NhS79AEw5lx/Ki0xSw==} + '@arcadeai/arcadejs@1.9.0': + resolution: {integrity: sha512-sV31uE+DBzzBdlF91PT/OGeYI7Mfmx4l3HVYkw/oDRZV04qJThzkDunoZKO14t1URgsi8FXrieaaOiFLgiiSKA==} '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -1110,6 +1122,9 @@ packages: '@cfcs/core@0.0.6': resolution: {integrity: sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==} + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + '@clack/core@0.4.2': resolution: {integrity: sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==} @@ -2244,6 +2259,16 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@langchain/core@0.3.72': + resolution: {integrity: sha512-WsGWVZYnlKffj2eEfDocPNiaTRoxyYiLSQdQ7oxZvxGZBqo/90vpjbC33UGK1uPNBM4kT+pkdaol/MnvKUh8TQ==} + engines: {node: '>=18'} + + '@langchain/openai@0.6.9': + resolution: {integrity: sha512-Dl+YVBTFia7WE4/jFemQEVchPbsahy/dD97jo6A9gLnYfTkWa/jh8Q78UjHQ3lobif84j2ebjHPcDHG1L0NUWg==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': '>=0.3.68 <0.4.0' + '@levischuck/tiny-cbor@0.2.11': resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} @@ -4746,6 +4771,9 @@ packages: '@types/react@19.1.6': resolution: {integrity: sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} @@ -5250,6 +5278,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} @@ -5437,6 +5469,9 @@ packages: resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} engines: {node: ^14.18.0 || >=16.10.0} + console-table-printer@2.14.6: + resolution: {integrity: sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -5641,6 +5676,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} @@ -7019,6 +7058,9 @@ packages: js-sha256@0.11.1: resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==} + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -7137,6 +7179,23 @@ packages: resolution: {integrity: sha512-rlB0I/c6FBDWPcQoDtkxi9zIvpmnV5xoIalfCMSMCa7nuA6VGA3F54TW9mEgX4DVf10sXAWCF5fDbamI/5ZpKA==} engines: {node: '>=20.0.0'} + langsmith@0.3.65: + resolution: {integrity: sha512-p9CWvc0R1fAARgPyaGt2JTz1FXq0Zlrq57uiOKZOoTHzAauhwU3PFtANK0EYSoHAJqJNIaO6GIaVj4q0a7IiLw==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -7819,6 +7878,18 @@ packages: zod: optional: true + openai@5.12.2: + resolution: {integrity: sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -7841,6 +7912,10 @@ packages: engines: {node: '>=8.*'} hasBin: true + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -7853,10 +7928,22 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + p-retry@6.2.1: resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} engines: {node: '>=16.17'} + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -8838,6 +8925,9 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-wcswidth@1.1.2: + resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -10014,7 +10104,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@arcadeai/arcadejs@1.8.1': + '@arcadeai/arcadejs@1.9.0': dependencies: '@types/node': 18.19.115 '@types/node-fetch': 2.6.12 @@ -10343,6 +10433,8 @@ snapshots: dependencies: '@egjs/component': 3.0.5 + '@cfworker/json-schema@4.1.1': {} + '@clack/core@0.4.2': dependencies: picocolors: 1.1.1 @@ -11222,6 +11314,55 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@langchain/core@0.3.72(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67))': + dependencies: + '@cfworker/json-schema': 4.1.1 + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.21 + langsmith: 0.3.65(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67)) + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 10.0.0 + zod: 3.25.67 + zod-to-json-schema: 3.24.6(zod@3.25.67) + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + + '@langchain/core@0.3.72(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67))': + dependencies: + '@cfworker/json-schema': 4.1.1 + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.21 + langsmith: 0.3.65(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67)) + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 10.0.0 + zod: 3.25.67 + zod-to-json-schema: 3.24.6(zod@3.25.67) + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + + '@langchain/openai@0.6.9(@langchain/core@0.3.72(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67)))(ws@8.18.0)': + dependencies: + '@langchain/core': 0.3.72(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67)) + js-tiktoken: 1.0.21 + openai: 5.12.2(ws@8.18.0)(zod@3.25.67) + zod: 3.25.67 + transitivePeerDependencies: + - ws + '@levischuck/tiny-cbor@0.2.11': {} '@livekit/mutex@1.1.1': {} @@ -13766,7 +13907,7 @@ snapshots: '@types/buffer-from@1.1.3': dependencies: - '@types/node': 22.15.29 + '@types/node': 22.13.8 '@types/canvas-confetti@1.9.0': {} @@ -13923,6 +14064,8 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/retry@0.12.0': {} + '@types/retry@0.12.2': {} '@types/sanitize-html@2.13.0': @@ -14583,6 +14726,8 @@ snapshots: callsites@3.1.0: {} + camelcase@6.3.0: {} + camelize@1.0.1: {} caniuse-lite@1.0.30001726: {} @@ -14810,6 +14955,10 @@ snapshots: consola@3.4.0: {} + console-table-printer@2.14.6: + dependencies: + simple-wcswidth: 1.1.2 + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -14998,6 +15147,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decimal.js-light@2.5.1: {} decimal.js@10.6.0: {} @@ -16567,6 +16718,10 @@ snapshots: js-sha256@0.11.1: {} + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -16713,6 +16868,34 @@ snapshots: kysely@0.28.5: {} + langsmith@0.3.65(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67)): + dependencies: + '@types/uuid': 10.0.0 + chalk: 4.1.2 + console-table-printer: 2.14.6 + p-queue: 6.6.2 + p-retry: 4.6.2 + semver: 7.7.2 + uuid: 10.0.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + openai: 5.12.2(ws@8.18.0)(zod@3.25.67) + + langsmith@0.3.65(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.0)(zod@3.25.67)): + dependencies: + '@types/uuid': 10.0.0 + chalk: 4.1.2 + console-table-printer: 2.14.6 + p-queue: 6.6.2 + p-retry: 4.6.2 + semver: 7.7.2 + uuid: 10.0.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + openai: 5.12.2(ws@8.18.0)(zod@3.25.67) + leac@0.6.0: {} levn@0.4.1: @@ -17579,6 +17762,11 @@ snapshots: transitivePeerDependencies: - encoding + openai@5.12.2(ws@8.18.0)(zod@3.25.67): + optionalDependencies: + ws: 8.18.0 + zod: 3.25.67 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -17613,6 +17801,8 @@ snapshots: '@oxlint/win32-arm64': 1.6.0 '@oxlint/win32-x64': 1.6.0 + p-finally@1.0.0: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -17625,12 +17815,26 @@ snapshots: dependencies: p-limit: 3.1.0 + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + p-retry@6.2.1: dependencies: '@types/retry': 0.12.2 is-network-error: 1.1.0 retry: 0.13.1 + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + package-json-from-dist@1.0.1: {} package-manager-detector@1.3.0: {} @@ -18783,6 +18987,8 @@ snapshots: dependencies: is-arrayish: 0.3.2 + simple-wcswidth@1.1.2: {} + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.29