From f0dd5e0907614e02ddcbc69bde7bcda2096c85d3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 15 Feb 2026 12:56:17 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20delete=20packages/ai/=20=E2=80=94?= =?UTF-8?q?=20moved=20to=20@trigger.dev/sdk=20subpaths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All functionality now lives in: - @trigger.dev/sdk/chat (frontend transport) - @trigger.dev/sdk/ai (backend chatTask, pipeChat) Co-authored-by: Eric Allam --- packages/ai/package.json | 74 --- packages/ai/src/chatTask.ts | 132 ----- packages/ai/src/index.ts | 3 - packages/ai/src/pipeChat.ts | 137 ----- packages/ai/src/transport.test.ts | 842 ------------------------------ packages/ai/src/transport.ts | 256 --------- packages/ai/src/types.ts | 117 ----- packages/ai/src/version.ts | 1 - packages/ai/tsconfig.json | 10 - packages/ai/vitest.config.ts | 8 - pnpm-lock.yaml | 188 +------ 11 files changed, 6 insertions(+), 1762 deletions(-) delete mode 100644 packages/ai/package.json delete mode 100644 packages/ai/src/chatTask.ts delete mode 100644 packages/ai/src/index.ts delete mode 100644 packages/ai/src/pipeChat.ts delete mode 100644 packages/ai/src/transport.test.ts delete mode 100644 packages/ai/src/transport.ts delete mode 100644 packages/ai/src/types.ts delete mode 100644 packages/ai/src/version.ts delete mode 100644 packages/ai/tsconfig.json delete mode 100644 packages/ai/vitest.config.ts diff --git a/packages/ai/package.json b/packages/ai/package.json deleted file mode 100644 index c6cee5d72..000000000 --- a/packages/ai/package.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "name": "@trigger.dev/ai", - "version": "4.3.3", - "description": "AI SDK integration for Trigger.dev - Custom ChatTransport for running AI chat as durable tasks", - "license": "MIT", - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "https://github.com/triggerdotdev/trigger.dev", - "directory": "packages/ai" - }, - "type": "module", - "files": [ - "dist" - ], - "tshy": { - "selfLink": false, - "main": true, - "module": true, - "project": "./tsconfig.json", - "exports": { - "./package.json": "./package.json", - ".": "./src/index.ts" - }, - "sourceDialects": [ - "@triggerdotdev/source" - ] - }, - "scripts": { - "clean": "rimraf dist .tshy .tshy-build .turbo", - "build": "tshy && pnpm run update-version", - "dev": "tshy --watch", - "typecheck": "tsc --noEmit", - "test": "vitest", - "update-version": "tsx ../../scripts/updateVersion.ts", - "check-exports": "attw --pack ." - }, - "dependencies": { - "@trigger.dev/core": "workspace:4.3.3" - }, - "peerDependencies": { - "ai": "^5.0.0 || ^6.0.0" - }, - "devDependencies": { - "@arethetypeswrong/cli": "^0.15.4", - "ai": "^6.0.0", - "rimraf": "^3.0.2", - "tshy": "^3.0.2", - "tsx": "4.17.0", - "vitest": "^2.1.0" - }, - "engines": { - "node": ">=18.20.0" - }, - "exports": { - "./package.json": "./package.json", - ".": { - "import": { - "@triggerdotdev/source": "./src/index.ts", - "types": "./dist/esm/index.d.ts", - "default": "./dist/esm/index.js" - }, - "require": { - "types": "./dist/commonjs/index.d.ts", - "default": "./dist/commonjs/index.js" - } - } - }, - "main": "./dist/commonjs/index.js", - "types": "./dist/commonjs/index.d.ts", - "module": "./dist/esm/index.js" -} diff --git a/packages/ai/src/chatTask.ts b/packages/ai/src/chatTask.ts deleted file mode 100644 index 7f3eb9261..000000000 --- a/packages/ai/src/chatTask.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { task as createTask } from "@trigger.dev/sdk"; -import type { Task } from "@trigger.dev/core/v3"; -import type { ChatTaskPayload } from "./types.js"; -import { pipeChat } from "./pipeChat.js"; - -/** - * Options for defining a chat task. - * - * This is a simplified version of the standard task options with the payload - * pre-typed as `ChatTaskPayload`. - */ -export type ChatTaskOptions = { - /** Unique identifier for the task */ - id: TIdentifier; - - /** Optional description of the task */ - description?: string; - - /** Retry configuration */ - retry?: { - maxAttempts?: number; - factor?: number; - minTimeoutInMs?: number; - maxTimeoutInMs?: number; - randomize?: boolean; - }; - - /** Queue configuration */ - queue?: { - name?: string; - concurrencyLimit?: number; - }; - - /** Machine preset for the task */ - machine?: { - preset?: string; - }; - - /** Maximum duration in seconds */ - maxDuration?: number; - - /** - * The main run function for the chat task. - * - * Receives a `ChatTaskPayload` with the conversation messages, chat session ID, - * and trigger type. - * - * **Auto-piping:** If this function returns a value that has a `.toUIMessageStream()` method - * (like a `StreamTextResult` from `streamText()`), the stream will automatically be piped - * to the frontend via the chat realtime stream. If you need to pipe from deeper in your - * code, use `pipeChat()` instead and don't return the result. - */ - run: (payload: ChatTaskPayload) => Promise; -}; - -/** - * An object that has a `toUIMessageStream()` method, like the result of `streamText()`. - */ -type UIMessageStreamable = { - toUIMessageStream: (...args: any[]) => AsyncIterable | ReadableStream; -}; - -function isUIMessageStreamable(value: unknown): value is UIMessageStreamable { - return ( - typeof value === "object" && - value !== null && - "toUIMessageStream" in value && - typeof (value as any).toUIMessageStream === "function" - ); -} - -/** - * Creates a Trigger.dev task pre-configured for AI SDK chat. - * - * This is a convenience wrapper around `task()` from `@trigger.dev/sdk` that: - * - **Pre-types the payload** as `ChatTaskPayload` — no manual typing needed - * - **Auto-pipes the stream** if the `run` function returns a `StreamTextResult` - * - * Requires `@trigger.dev/sdk` to be installed (it's a peer dependency). - * - * @example - * ```ts - * import { chatTask } from "@trigger.dev/ai"; - * import { streamText, convertToModelMessages } from "ai"; - * import { openai } from "@ai-sdk/openai"; - * - * // Simple: return streamText result — auto-piped to the frontend - * export const myChatTask = chatTask({ - * id: "my-chat-task", - * run: async ({ messages }) => { - * return streamText({ - * model: openai("gpt-4o"), - * messages: convertToModelMessages(messages), - * }); - * }, - * }); - * ``` - * - * @example - * ```ts - * import { chatTask, pipeChat } from "@trigger.dev/ai"; - * - * // Complex: use pipeChat() from deep inside your agent code - * export const myAgentTask = chatTask({ - * id: "my-agent-task", - * run: async ({ messages }) => { - * await runComplexAgentLoop(messages); - * // pipeChat() called internally by the agent loop - * }, - * }); - * ``` - */ -export function chatTask( - options: ChatTaskOptions -): Task { - const { run: userRun, ...restOptions } = options; - - return createTask({ - ...restOptions, - run: async (payload: ChatTaskPayload) => { - const result = await userRun(payload); - - // If the run function returned a StreamTextResult or similar, - // automatically pipe it to the chat stream - if (isUIMessageStreamable(result)) { - await pipeChat(result); - } - - return result; - }, - }); -} diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts deleted file mode 100644 index 7e673894f..000000000 --- a/packages/ai/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { TriggerChatTransport, createChatTransport } from "./transport.js"; -export type { TriggerChatTransportOptions, ChatTaskPayload } from "./types.js"; -export { VERSION } from "./version.js"; diff --git a/packages/ai/src/pipeChat.ts b/packages/ai/src/pipeChat.ts deleted file mode 100644 index 885951c59..000000000 --- a/packages/ai/src/pipeChat.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { realtimeStreams } from "@trigger.dev/core/v3"; - -/** - * The default stream key used for chat transport communication. - * - * Both `TriggerChatTransport` (frontend) and `pipeChat` (backend) use this key - * by default to ensure they communicate over the same stream. - */ -export const CHAT_STREAM_KEY = "chat"; - -/** - * Options for `pipeChat`. - */ -export type PipeChatOptions = { - /** - * Override the stream key to pipe to. - * Must match the `streamKey` option on `TriggerChatTransport`. - * - * @default "chat" - */ - streamKey?: string; - - /** - * An AbortSignal to cancel the stream. - */ - signal?: AbortSignal; - - /** - * The target run ID to pipe the stream to. - * @default "self" (current run) - */ - target?: string; -}; - -/** - * An object that has a `toUIMessageStream()` method, like the result of `streamText()` from the AI SDK. - */ -type UIMessageStreamable = { - toUIMessageStream: (...args: any[]) => AsyncIterable | ReadableStream; -}; - -function isUIMessageStreamable(value: unknown): value is UIMessageStreamable { - return ( - typeof value === "object" && - value !== null && - "toUIMessageStream" in value && - typeof (value as any).toUIMessageStream === "function" - ); -} - -function isAsyncIterable(value: unknown): value is AsyncIterable { - return ( - typeof value === "object" && - value !== null && - Symbol.asyncIterator in value - ); -} - -function isReadableStream(value: unknown): value is ReadableStream { - return ( - typeof value === "object" && - value !== null && - typeof (value as any).getReader === "function" - ); -} - -/** - * Pipes a chat stream to the realtime stream, making it available to the - * `TriggerChatTransport` on the frontend. - * - * Accepts any of: - * - A `StreamTextResult` from the AI SDK (has `.toUIMessageStream()`) - * - An `AsyncIterable` of `UIMessageChunk`s - * - A `ReadableStream` of `UIMessageChunk`s - * - * This must be called from inside a Trigger.dev task's `run` function. - * - * @example - * ```ts - * import { task } from "@trigger.dev/sdk"; - * import { pipeChat, type ChatTaskPayload } from "@trigger.dev/ai"; - * import { streamText, convertToModelMessages } from "ai"; - * - * export const myChatTask = task({ - * id: "my-chat-task", - * run: async (payload: ChatTaskPayload) => { - * const result = streamText({ - * model: openai("gpt-4o"), - * messages: convertToModelMessages(payload.messages), - * }); - * - * await pipeChat(result); - * }, - * }); - * ``` - * - * @example - * ```ts - * // Deep inside your agent library — pipeChat works from anywhere inside a task - * async function runAgentLoop(messages: CoreMessage[]) { - * const result = streamText({ model, messages }); - * await pipeChat(result); - * } - * ``` - * - * @param source - A StreamTextResult, AsyncIterable, or ReadableStream of UIMessageChunks - * @param options - Optional configuration - * @returns A promise that resolves when the stream has been fully piped - */ -export async function pipeChat( - source: UIMessageStreamable | AsyncIterable | ReadableStream, - options?: PipeChatOptions -): Promise { - const streamKey = options?.streamKey ?? CHAT_STREAM_KEY; - - // Resolve the source to an AsyncIterable or ReadableStream - let stream: AsyncIterable | ReadableStream; - - if (isUIMessageStreamable(source)) { - stream = source.toUIMessageStream(); - } else if (isAsyncIterable(source) || isReadableStream(source)) { - stream = source; - } else { - throw new Error( - "pipeChat: source must be a StreamTextResult (with .toUIMessageStream()), " + - "an AsyncIterable, or a ReadableStream" - ); - } - - // Pipe to the realtime stream - const instance = realtimeStreams.pipe(streamKey, stream, { - signal: options?.signal, - target: options?.target, - }); - - await instance.wait(); -} diff --git a/packages/ai/src/transport.test.ts b/packages/ai/src/transport.test.ts deleted file mode 100644 index 53d3ab868..000000000 --- a/packages/ai/src/transport.test.ts +++ /dev/null @@ -1,842 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import type { UIMessage, UIMessageChunk } from "ai"; -import { TriggerChatTransport, createChatTransport } from "./transport.js"; - -// Helper: encode text as SSE format -function sseEncode(chunks: UIMessageChunk[]): string { - return chunks.map((chunk, i) => `id: ${i}\ndata: ${JSON.stringify(chunk)}\n\n`).join(""); -} - -// Helper: create a ReadableStream from SSE text -function createSSEStream(sseText: string): ReadableStream { - const encoder = new TextEncoder(); - return new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(sseText)); - controller.close(); - }, - }); -} - -// Helper: create test UIMessages -function createUserMessage(text: string): UIMessage { - return { - id: `msg-${Date.now()}`, - role: "user", - parts: [{ type: "text", text }], - }; -} - -function createAssistantMessage(text: string): UIMessage { - return { - id: `msg-${Date.now()}`, - role: "assistant", - parts: [{ type: "text", text }], - }; -} - -// Sample UIMessageChunks as the AI SDK would produce -const sampleChunks: UIMessageChunk[] = [ - { type: "text-start", id: "part-1" }, - { type: "text-delta", id: "part-1", delta: "Hello" }, - { type: "text-delta", id: "part-1", delta: " world" }, - { type: "text-delta", id: "part-1", delta: "!" }, - { type: "text-end", id: "part-1" }, -]; - -describe("TriggerChatTransport", () => { - let originalFetch: typeof global.fetch; - - beforeEach(() => { - originalFetch = global.fetch; - }); - - afterEach(() => { - global.fetch = originalFetch; - vi.restoreAllMocks(); - }); - - describe("constructor", () => { - it("should create transport with required options", () => { - const transport = new TriggerChatTransport({ - taskId: "my-chat-task", - accessToken: "test-token", - }); - - expect(transport).toBeInstanceOf(TriggerChatTransport); - }); - - it("should accept optional configuration", () => { - const transport = new TriggerChatTransport({ - taskId: "my-chat-task", - accessToken: "test-token", - baseURL: "https://custom.trigger.dev", - streamKey: "custom-stream", - headers: { "X-Custom": "value" }, - }); - - expect(transport).toBeInstanceOf(TriggerChatTransport); - }); - - it("should accept a function for accessToken", () => { - let tokenCallCount = 0; - const transport = new TriggerChatTransport({ - taskId: "my-chat-task", - accessToken: () => { - tokenCallCount++; - return `dynamic-token-${tokenCallCount}`; - }, - }); - - expect(transport).toBeInstanceOf(TriggerChatTransport); - }); - }); - - describe("sendMessages", () => { - it("should trigger the task and return a ReadableStream of UIMessageChunks", async () => { - const triggerRunId = "run_abc123"; - const publicToken = "pub_token_xyz"; - - // Mock fetch to handle both the trigger request and the SSE stream request - global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - // Handle the task trigger request - if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) { - return new Response( - JSON.stringify({ id: triggerRunId }), - { - status: 200, - headers: { - "content-type": "application/json", - "x-trigger-jwt": publicToken, - }, - } - ); - } - - // Handle the SSE stream request - if (urlStr.includes("/realtime/v1/streams/")) { - const sseText = sseEncode(sampleChunks); - return new Response(createSSEStream(sseText), { - status: 200, - headers: { - "content-type": "text/event-stream", - "X-Stream-Version": "v1", - }, - }); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - const transport = new TriggerChatTransport({ - taskId: "my-chat-task", - accessToken: "test-token", - baseURL: "https://api.test.trigger.dev", - }); - - const messages: UIMessage[] = [createUserMessage("Hello!")]; - - const stream = await transport.sendMessages({ - trigger: "submit-message", - chatId: "chat-1", - messageId: undefined, - messages, - abortSignal: undefined, - }); - - expect(stream).toBeInstanceOf(ReadableStream); - - // Read all chunks from the stream - const reader = stream.getReader(); - const receivedChunks: UIMessageChunk[] = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - receivedChunks.push(value); - } - - expect(receivedChunks).toHaveLength(sampleChunks.length); - expect(receivedChunks[0]).toEqual({ type: "text-start", id: "part-1" }); - expect(receivedChunks[1]).toEqual({ type: "text-delta", id: "part-1", delta: "Hello" }); - expect(receivedChunks[4]).toEqual({ type: "text-end", id: "part-1" }); - }); - - it("should send the correct payload to the trigger API", async () => { - const fetchSpy = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) { - return new Response( - JSON.stringify({ id: "run_test" }), - { - status: 200, - headers: { - "content-type": "application/json", - "x-trigger-jwt": "pub_token", - }, - } - ); - } - - if (urlStr.includes("/realtime/v1/streams/")) { - return new Response(createSSEStream(""), { - status: 200, - headers: { - "content-type": "text/event-stream", - "X-Stream-Version": "v1", - }, - }); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - global.fetch = fetchSpy; - - const transport = new TriggerChatTransport({ - taskId: "my-chat-task", - accessToken: "test-token", - baseURL: "https://api.test.trigger.dev", - }); - - const messages: UIMessage[] = [createUserMessage("Hello!")]; - - await transport.sendMessages({ - trigger: "submit-message", - chatId: "chat-123", - messageId: undefined, - messages, - abortSignal: undefined, - metadata: { custom: "data" }, - }); - - // Verify the trigger fetch call - const triggerCall = fetchSpy.mock.calls.find((call: any[]) => - (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger") - ); - - expect(triggerCall).toBeDefined(); - const triggerUrl = typeof triggerCall![0] === "string" ? triggerCall![0] : triggerCall![0].toString(); - expect(triggerUrl).toContain("/api/v1/tasks/my-chat-task/trigger"); - - const triggerBody = JSON.parse(triggerCall![1]?.body as string); - const payload = JSON.parse(triggerBody.payload); - expect(payload.messages).toEqual(messages); - expect(payload.chatId).toBe("chat-123"); - expect(payload.trigger).toBe("submit-message"); - expect(payload.metadata).toEqual({ custom: "data" }); - }); - - it("should use the correct stream URL with custom streamKey", async () => { - const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - if (urlStr.includes("/trigger")) { - return new Response( - JSON.stringify({ id: "run_custom" }), - { - status: 200, - headers: { - "content-type": "application/json", - "x-trigger-jwt": "token", - }, - } - ); - } - - if (urlStr.includes("/realtime/v1/streams/")) { - return new Response(createSSEStream(""), { - status: 200, - headers: { - "content-type": "text/event-stream", - "X-Stream-Version": "v1", - }, - }); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - global.fetch = fetchSpy; - - const transport = new TriggerChatTransport({ - taskId: "my-task", - accessToken: "token", - baseURL: "https://api.test.trigger.dev", - streamKey: "my-custom-stream", - }); - - await transport.sendMessages({ - trigger: "submit-message", - chatId: "chat-1", - messageId: undefined, - messages: [createUserMessage("test")], - abortSignal: undefined, - }); - - // Verify the stream URL uses the custom stream key - const streamCall = fetchSpy.mock.calls.find((call: any[]) => - (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/realtime/v1/streams/") - ); - - expect(streamCall).toBeDefined(); - const streamUrl = typeof streamCall![0] === "string" ? streamCall![0] : streamCall![0].toString(); - expect(streamUrl).toContain("/realtime/v1/streams/run_custom/my-custom-stream"); - }); - - it("should include extra headers in stream requests", async () => { - const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - if (urlStr.includes("/trigger")) { - return new Response( - JSON.stringify({ id: "run_hdrs" }), - { - status: 200, - headers: { - "content-type": "application/json", - "x-trigger-jwt": "token", - }, - } - ); - } - - if (urlStr.includes("/realtime/v1/streams/")) { - return new Response(createSSEStream(""), { - status: 200, - headers: { - "content-type": "text/event-stream", - "X-Stream-Version": "v1", - }, - }); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - global.fetch = fetchSpy; - - const transport = new TriggerChatTransport({ - taskId: "my-task", - accessToken: "token", - baseURL: "https://api.test.trigger.dev", - headers: { "X-Custom-Header": "custom-value" }, - }); - - await transport.sendMessages({ - trigger: "submit-message", - chatId: "chat-1", - messageId: undefined, - messages: [createUserMessage("test")], - abortSignal: undefined, - }); - - // Verify the stream request includes custom headers - const streamCall = fetchSpy.mock.calls.find((call: any[]) => - (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/realtime/v1/streams/") - ); - - expect(streamCall).toBeDefined(); - const requestHeaders = streamCall![1]?.headers as Record; - expect(requestHeaders["X-Custom-Header"]).toBe("custom-value"); - }); - }); - - describe("reconnectToStream", () => { - it("should return null when no session exists for chatId", async () => { - const transport = new TriggerChatTransport({ - taskId: "my-task", - accessToken: "token", - }); - - const result = await transport.reconnectToStream({ - chatId: "nonexistent-chat", - }); - - expect(result).toBeNull(); - }); - - it("should reconnect to an existing session", async () => { - const triggerRunId = "run_reconnect"; - const publicToken = "pub_reconnect_token"; - - global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - if (urlStr.includes("/trigger")) { - return new Response( - JSON.stringify({ id: triggerRunId }), - { - status: 200, - headers: { - "content-type": "application/json", - "x-trigger-jwt": publicToken, - }, - } - ); - } - - if (urlStr.includes("/realtime/v1/streams/")) { - const chunks: UIMessageChunk[] = [ - { type: "text-start", id: "part-1" }, - { type: "text-delta", id: "part-1", delta: "Reconnected!" }, - { type: "text-end", id: "part-1" }, - ]; - return new Response(createSSEStream(sseEncode(chunks)), { - status: 200, - headers: { - "content-type": "text/event-stream", - "X-Stream-Version": "v1", - }, - }); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - const transport = new TriggerChatTransport({ - taskId: "my-task", - accessToken: "token", - baseURL: "https://api.test.trigger.dev", - }); - - // First, send messages to establish a session - await transport.sendMessages({ - trigger: "submit-message", - chatId: "chat-reconnect", - messageId: undefined, - messages: [createUserMessage("Hello")], - abortSignal: undefined, - }); - - // Now reconnect - const stream = await transport.reconnectToStream({ - chatId: "chat-reconnect", - }); - - expect(stream).toBeInstanceOf(ReadableStream); - - // Read the stream - const reader = stream!.getReader(); - const receivedChunks: UIMessageChunk[] = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - receivedChunks.push(value); - } - - expect(receivedChunks.length).toBeGreaterThan(0); - }); - }); - - describe("createChatTransport", () => { - it("should create a TriggerChatTransport instance", () => { - const transport = createChatTransport({ - taskId: "my-task", - accessToken: "token", - }); - - expect(transport).toBeInstanceOf(TriggerChatTransport); - }); - - it("should pass options through to the transport", () => { - const transport = createChatTransport({ - taskId: "custom-task", - accessToken: "custom-token", - baseURL: "https://custom.example.com", - streamKey: "custom-key", - headers: { "X-Test": "value" }, - }); - - expect(transport).toBeInstanceOf(TriggerChatTransport); - }); - }); - - describe("error handling", () => { - it("should propagate trigger API errors", async () => { - global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - if (urlStr.includes("/trigger")) { - return new Response( - JSON.stringify({ error: "Task not found" }), - { - status: 404, - headers: { "content-type": "application/json" }, - } - ); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - const transport = new TriggerChatTransport({ - taskId: "nonexistent-task", - accessToken: "token", - baseURL: "https://api.test.trigger.dev", - }); - - await expect( - transport.sendMessages({ - trigger: "submit-message", - chatId: "chat-error", - messageId: undefined, - messages: [createUserMessage("test")], - abortSignal: undefined, - }) - ).rejects.toThrow(); - }); - }); - - describe("abort signal", () => { - it("should close the stream gracefully when aborted", async () => { - let streamResolve: (() => void) | undefined; - const streamWait = new Promise((resolve) => { - streamResolve = resolve; - }); - - global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - if (urlStr.includes("/trigger")) { - return new Response( - JSON.stringify({ id: "run_abort" }), - { - status: 200, - headers: { - "content-type": "application/json", - "x-trigger-jwt": "token", - }, - } - ); - } - - if (urlStr.includes("/realtime/v1/streams/")) { - // Create a slow stream that waits before sending data - const stream = new ReadableStream({ - async start(controller) { - const encoder = new TextEncoder(); - controller.enqueue( - encoder.encode(`id: 0\ndata: ${JSON.stringify({ type: "text-start", id: "p1" })}\n\n`) - ); - // Wait for the test to signal it's done - await streamWait; - controller.close(); - }, - }); - - return new Response(stream, { - status: 200, - headers: { - "content-type": "text/event-stream", - "X-Stream-Version": "v1", - }, - }); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - const abortController = new AbortController(); - - const transport = new TriggerChatTransport({ - taskId: "my-task", - accessToken: "token", - baseURL: "https://api.test.trigger.dev", - }); - - const stream = await transport.sendMessages({ - trigger: "submit-message", - chatId: "chat-abort", - messageId: undefined, - messages: [createUserMessage("test")], - abortSignal: abortController.signal, - }); - - // Read the first chunk - const reader = stream.getReader(); - const first = await reader.read(); - expect(first.done).toBe(false); - - // Abort and clean up - abortController.abort(); - streamResolve?.(); - - // The stream should close — reading should return done - const next = await reader.read(); - expect(next.done).toBe(true); - }); - }); - - describe("multiple sessions", () => { - it("should track multiple chat sessions independently", async () => { - let callCount = 0; - - global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - if (urlStr.includes("/trigger")) { - callCount++; - return new Response( - JSON.stringify({ id: `run_multi_${callCount}` }), - { - status: 200, - headers: { - "content-type": "application/json", - "x-trigger-jwt": `token_${callCount}`, - }, - } - ); - } - - if (urlStr.includes("/realtime/v1/streams/")) { - return new Response(createSSEStream(""), { - status: 200, - headers: { - "content-type": "text/event-stream", - "X-Stream-Version": "v1", - }, - }); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - const transport = new TriggerChatTransport({ - taskId: "my-task", - accessToken: "token", - baseURL: "https://api.test.trigger.dev", - }); - - // Start two independent chat sessions - await transport.sendMessages({ - trigger: "submit-message", - chatId: "session-a", - messageId: undefined, - messages: [createUserMessage("Hello A")], - abortSignal: undefined, - }); - - await transport.sendMessages({ - trigger: "submit-message", - chatId: "session-b", - messageId: undefined, - messages: [createUserMessage("Hello B")], - abortSignal: undefined, - }); - - // Both sessions should be independently reconnectable - const streamA = await transport.reconnectToStream({ chatId: "session-a" }); - const streamB = await transport.reconnectToStream({ chatId: "session-b" }); - const streamC = await transport.reconnectToStream({ chatId: "nonexistent" }); - - expect(streamA).toBeInstanceOf(ReadableStream); - expect(streamB).toBeInstanceOf(ReadableStream); - expect(streamC).toBeNull(); - }); - }); - - describe("dynamic accessToken", () => { - it("should call the accessToken function for each sendMessages call", async () => { - let tokenCallCount = 0; - - global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - if (urlStr.includes("/trigger")) { - return new Response( - JSON.stringify({ id: `run_dyn_${tokenCallCount}` }), - { - status: 200, - headers: { - "content-type": "application/json", - "x-trigger-jwt": "stream-token", - }, - } - ); - } - - if (urlStr.includes("/realtime/v1/streams/")) { - const chunks: UIMessageChunk[] = [ - { type: "text-start", id: "p1" }, - { type: "text-end", id: "p1" }, - ]; - return new Response(createSSEStream(sseEncode(chunks)), { - status: 200, - headers: { - "content-type": "text/event-stream", - "X-Stream-Version": "v1", - }, - }); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - const transport = new TriggerChatTransport({ - taskId: "my-task", - accessToken: () => { - tokenCallCount++; - return `dynamic-token-${tokenCallCount}`; - }, - baseURL: "https://api.test.trigger.dev", - }); - - // First call — the token function should be invoked - await transport.sendMessages({ - trigger: "submit-message", - chatId: "chat-dyn-1", - messageId: undefined, - messages: [createUserMessage("first")], - abortSignal: undefined, - }); - - const firstCount = tokenCallCount; - expect(firstCount).toBeGreaterThanOrEqual(1); - - // Second call — the token function should be invoked again - await transport.sendMessages({ - trigger: "submit-message", - chatId: "chat-dyn-2", - messageId: undefined, - messages: [createUserMessage("second")], - abortSignal: undefined, - }); - - // Token function was called at least once more - expect(tokenCallCount).toBeGreaterThan(firstCount); - }); - }); - - describe("body merging", () => { - it("should merge ChatRequestOptions.body into the task payload", async () => { - const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - if (urlStr.includes("/trigger")) { - return new Response( - JSON.stringify({ id: "run_body" }), - { - status: 200, - headers: { - "content-type": "application/json", - "x-trigger-jwt": "token", - }, - } - ); - } - - if (urlStr.includes("/realtime/v1/streams/")) { - return new Response(createSSEStream(""), { - status: 200, - headers: { - "content-type": "text/event-stream", - "X-Stream-Version": "v1", - }, - }); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - global.fetch = fetchSpy; - - const transport = new TriggerChatTransport({ - taskId: "my-task", - accessToken: "token", - baseURL: "https://api.test.trigger.dev", - }); - - await transport.sendMessages({ - trigger: "submit-message", - chatId: "chat-body", - messageId: undefined, - messages: [createUserMessage("test")], - abortSignal: undefined, - body: { systemPrompt: "You are helpful", temperature: 0.7 }, - }); - - const triggerCall = fetchSpy.mock.calls.find((call: any[]) => - (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger") - ); - - const triggerBody = JSON.parse(triggerCall![1]?.body as string); - const payload = JSON.parse(triggerBody.payload); - - // body properties should be merged into the payload - expect(payload.systemPrompt).toBe("You are helpful"); - expect(payload.temperature).toBe(0.7); - // Standard fields should still be present - expect(payload.chatId).toBe("chat-body"); - expect(payload.trigger).toBe("submit-message"); - }); - }); - - describe("message types", () => { - it("should handle regenerate-message trigger", async () => { - const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => { - const urlStr = typeof url === "string" ? url : url.toString(); - - if (urlStr.includes("/trigger")) { - return new Response( - JSON.stringify({ id: "run_regen" }), - { - status: 200, - headers: { - "content-type": "application/json", - "x-trigger-jwt": "token", - }, - } - ); - } - - if (urlStr.includes("/realtime/v1/streams/")) { - return new Response(createSSEStream(""), { - status: 200, - headers: { - "content-type": "text/event-stream", - "X-Stream-Version": "v1", - }, - }); - } - - throw new Error(`Unexpected fetch URL: ${urlStr}`); - }); - - global.fetch = fetchSpy; - - const transport = new TriggerChatTransport({ - taskId: "my-task", - accessToken: "token", - baseURL: "https://api.test.trigger.dev", - }); - - const messages: UIMessage[] = [ - createUserMessage("Hello!"), - createAssistantMessage("Hi there!"), - ]; - - await transport.sendMessages({ - trigger: "regenerate-message", - chatId: "chat-regen", - messageId: "msg-to-regen", - messages, - abortSignal: undefined, - }); - - // Verify the payload includes the regenerate trigger type and messageId - const triggerCall = fetchSpy.mock.calls.find((call: any[]) => - (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger") - ); - - const triggerBody = JSON.parse(triggerCall![1]?.body as string); - const payload = JSON.parse(triggerBody.payload); - expect(payload.trigger).toBe("regenerate-message"); - expect(payload.messageId).toBe("msg-to-regen"); - }); - }); -}); diff --git a/packages/ai/src/transport.ts b/packages/ai/src/transport.ts deleted file mode 100644 index ff4b2c47a..000000000 --- a/packages/ai/src/transport.ts +++ /dev/null @@ -1,256 +0,0 @@ -import type { ChatTransport, UIMessage, UIMessageChunk, ChatRequestOptions } from "ai"; -import { - ApiClient, - SSEStreamSubscription, - type SSEStreamPart, -} from "@trigger.dev/core/v3"; -import type { TriggerChatTransportOptions, ChatSessionState } from "./types.js"; - -const DEFAULT_STREAM_KEY = "chat"; -const DEFAULT_BASE_URL = "https://api.trigger.dev"; -const DEFAULT_STREAM_TIMEOUT_SECONDS = 120; - -/** - * A custom AI SDK `ChatTransport` implementation that bridges the Vercel AI SDK's - * `useChat` hook with Trigger.dev's durable task execution and realtime streams. - * - * When `sendMessages` is called, the transport: - * 1. Triggers a Trigger.dev task with the chat messages as payload - * 2. Subscribes to the task's realtime stream to receive `UIMessageChunk` data - * 3. Returns a `ReadableStream` that the AI SDK processes natively - * - * The task receives a `ChatTaskPayload` containing the conversation messages, - * chat session ID, trigger type, and any custom metadata. Your task should use - * the AI SDK's `streamText` (or similar) to generate a response, then pipe - * the resulting `UIMessageStream` to the `"chat"` realtime stream key - * (or a custom key matching the `streamKey` option). - * - * @example - * ```tsx - * // Frontend — use with AI SDK's useChat hook - * import { useChat } from "@ai-sdk/react"; - * import { TriggerChatTransport } from "@trigger.dev/ai"; - * - * function Chat({ accessToken }: { accessToken: string }) { - * const { messages, sendMessage, status } = useChat({ - * transport: new TriggerChatTransport({ - * accessToken, - * taskId: "my-chat-task", - * }), - * }); - * - * // ... render messages - * } - * ``` - * - * @example - * ```ts - * // Backend — Trigger.dev task that handles chat - * import { task, streams } from "@trigger.dev/sdk"; - * import { streamText, convertToModelMessages } from "ai"; - * import type { ChatTaskPayload } from "@trigger.dev/ai"; - * - * export const myChatTask = task({ - * id: "my-chat-task", - * run: async (payload: ChatTaskPayload) => { - * const result = streamText({ - * model: openai("gpt-4o"), - * messages: convertToModelMessages(payload.messages), - * }); - * - * const { waitUntilComplete } = streams.pipe("chat", result.toUIMessageStream()); - * await waitUntilComplete(); - * }, - * }); - * ``` - */ -export class TriggerChatTransport implements ChatTransport { - private readonly taskId: string; - private readonly resolveAccessToken: () => string; - private readonly baseURL: string; - private readonly streamKey: string; - private readonly extraHeaders: Record; - private readonly streamTimeoutSeconds: number; - - /** - * Tracks active chat sessions for reconnection support. - * Maps chatId → session state (runId, publicAccessToken). - */ - private sessions: Map = new Map(); - - constructor(options: TriggerChatTransportOptions) { - this.taskId = options.taskId; - this.resolveAccessToken = - typeof options.accessToken === "function" - ? options.accessToken - : () => options.accessToken as string; - this.baseURL = options.baseURL ?? DEFAULT_BASE_URL; - this.streamKey = options.streamKey ?? DEFAULT_STREAM_KEY; - this.extraHeaders = options.headers ?? {}; - this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS; - } - - /** - * Sends messages to a Trigger.dev task and returns a streaming response. - * - * This method: - * 1. Triggers the configured task with the chat messages as payload - * 2. Subscribes to the task's realtime stream for UIMessageChunk events - * 3. Returns a ReadableStream that the AI SDK's useChat hook processes - */ - sendMessages = async ( - options: { - trigger: "submit-message" | "regenerate-message"; - chatId: string; - messageId: string | undefined; - messages: UIMessage[]; - abortSignal: AbortSignal | undefined; - } & ChatRequestOptions - ): Promise> => { - const { trigger, chatId, messageId, messages, abortSignal, body, metadata } = options; - - // Build the payload for the task — this becomes the ChatTaskPayload - const payload = { - messages, - chatId, - trigger, - messageId, - metadata, - ...(body ?? {}), - }; - - const currentToken = this.resolveAccessToken(); - - // Trigger the task — use the already-resolved token directly - const apiClient = new ApiClient(this.baseURL, currentToken); - const triggerResponse = await apiClient.triggerTask(this.taskId, { - payload: JSON.stringify(payload), - options: { - payloadType: "application/json", - }, - }); - - const runId = triggerResponse.id; - const publicAccessToken = - "publicAccessToken" in triggerResponse - ? (triggerResponse as { publicAccessToken?: string }).publicAccessToken - : undefined; - - // Store session state for reconnection - this.sessions.set(chatId, { - runId, - publicAccessToken: publicAccessToken ?? currentToken, - }); - - // Subscribe to the realtime stream for this run - return this.subscribeToStream(runId, publicAccessToken ?? currentToken, abortSignal); - }; - - /** - * Reconnects to an existing streaming response for the specified chat session. - * - * Returns a ReadableStream if an active session exists, or null if no session is found. - */ - reconnectToStream = async ( - options: { - chatId: string; - } & ChatRequestOptions - ): Promise | null> => { - const session = this.sessions.get(options.chatId); - if (!session) { - return null; - } - - return this.subscribeToStream(session.runId, session.publicAccessToken, undefined); - }; - - /** - * Creates a ReadableStream by subscribing to the realtime SSE stream - * for a given run. - */ - private subscribeToStream( - runId: string, - accessToken: string, - abortSignal: AbortSignal | undefined - ): ReadableStream { - const headers: Record = { - Authorization: `Bearer ${accessToken}`, - ...this.extraHeaders, - }; - - const subscription = new SSEStreamSubscription( - `${this.baseURL}/realtime/v1/streams/${runId}/${this.streamKey}`, - { - headers, - signal: abortSignal, - timeoutInSeconds: this.streamTimeoutSeconds, - } - ); - - return new ReadableStream({ - start: async (controller) => { - try { - const sseStream = await subscription.subscribe(); - const reader = sseStream.getReader(); - - try { - while (true) { - const { done, value } = await reader.read(); - - if (done) { - controller.close(); - return; - } - - if (abortSignal?.aborted) { - reader.cancel(); - reader.releaseLock(); - controller.close(); - return; - } - - // Each SSE part's chunk is a UIMessageChunk - controller.enqueue(value.chunk as UIMessageChunk); - } - } catch (readError) { - reader.releaseLock(); - throw readError; - } - } catch (error) { - // Don't error the stream for abort errors — just close gracefully - if (error instanceof Error && error.name === "AbortError") { - controller.close(); - return; - } - - controller.error(error); - } - }, - }); - } -} - -/** - * Creates a new `TriggerChatTransport` instance. - * - * This is a convenience factory function equivalent to `new TriggerChatTransport(options)`. - * - * @example - * ```tsx - * import { useChat } from "@ai-sdk/react"; - * import { createChatTransport } from "@trigger.dev/ai"; - * - * const transport = createChatTransport({ - * taskId: "my-chat-task", - * accessToken: publicAccessToken, - * }); - * - * function Chat() { - * const { messages, sendMessage } = useChat({ transport }); - * // ... - * } - * ``` - */ -export function createChatTransport(options: TriggerChatTransportOptions): TriggerChatTransport { - return new TriggerChatTransport(options); -} diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts deleted file mode 100644 index 91ae99388..000000000 --- a/packages/ai/src/types.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { UIMessage } from "ai"; - -/** - * Options for creating a TriggerChatTransport. - */ -export type TriggerChatTransportOptions = { - /** - * The Trigger.dev task ID to trigger for chat completions. - * This task will receive the chat messages as its payload. - */ - task: string; - - /** - * An access token for authenticating with the Trigger.dev API. - * - * This must be a token with permission to trigger the task. You can use: - * - A **trigger public token** created via `auth.createTriggerPublicToken(taskId)` (recommended for frontend use) - * - A **secret API key** (for server-side use only — never expose in the browser) - * - * The token returned from triggering the task (`publicAccessToken`) is automatically - * used for subscribing to the realtime stream. - * - * Can also be a function that returns a token string, useful for dynamic token refresh: - * ```ts - * accessToken: () => getLatestToken() - * ``` - */ - accessToken: string | (() => string); - - /** - * Base URL for the Trigger.dev API. - * - * @default "https://api.trigger.dev" - */ - baseURL?: string; - - /** - * The stream key where the task pipes UIMessageChunk data. - * When using `chatTask()` or `pipeChat()`, this is handled automatically. - * Only set this if you're using a custom stream key. - * - * @default "chat" - */ - streamKey?: string; - - /** - * Additional headers to include in API requests to Trigger.dev. - */ - headers?: Record; - - /** - * The number of seconds to wait for the realtime stream to produce data - * before timing out. If no data arrives within this period, the stream - * will be closed. - * - * @default 120 - */ - streamTimeoutSeconds?: number; -}; - -/** - * The payload shape that the transport sends to the triggered task. - * - * When using `chatTask()`, the payload is automatically typed — you don't need - * to import this type. When using `task()` directly, use this type to annotate - * your payload: - * - * @example - * ```ts - * import { task } from "@trigger.dev/sdk"; - * import { pipeChat, type ChatTaskPayload } from "@trigger.dev/ai"; - * - * export const myChatTask = task({ - * id: "my-chat-task", - * run: async (payload: ChatTaskPayload) => { - * const result = streamText({ - * model: openai("gpt-4o"), - * messages: convertToModelMessages(payload.messages), - * }); - * await pipeChat(result); - * }, - * }); - * ``` - */ -export type ChatTaskPayload = { - /** The array of UI messages representing the conversation history */ - messages: TMessage[]; - - /** The unique identifier for the chat session */ - chatId: string; - - /** - * The type of message submission: - * - `"submit-message"`: A new user message was submitted - * - `"regenerate-message"`: The user wants to regenerate the last assistant response - */ - trigger: "submit-message" | "regenerate-message"; - - /** - * The ID of the message to regenerate (only present for `"regenerate-message"` trigger). - */ - messageId?: string; - - /** - * Custom metadata attached to the chat request by the frontend. - */ - metadata?: unknown; -}; - -/** - * Internal state for tracking active chat sessions, used for stream reconnection. - * @internal - */ -export type ChatSessionState = { - runId: string; - publicAccessToken: string; -}; diff --git a/packages/ai/src/version.ts b/packages/ai/src/version.ts deleted file mode 100644 index 2e47a8868..000000000 --- a/packages/ai/src/version.ts +++ /dev/null @@ -1 +0,0 @@ -export const VERSION = "0.0.0"; diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json deleted file mode 100644 index ec09e52a4..000000000 --- a/packages/ai/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../.configs/tsconfig.base.json", - "compilerOptions": { - "isolatedDeclarations": false, - "composite": true, - "sourceMap": true, - "stripInternal": true - }, - "include": ["./src/**/*.ts"] -} diff --git a/packages/ai/vitest.config.ts b/packages/ai/vitest.config.ts deleted file mode 100644 index c497b8ec9..000000000 --- a/packages/ai/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - include: ["src/**/*.test.ts"], - globals: true, - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14fe8b414..f9afb46c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1147,7 +1147,7 @@ importers: version: 18.3.1 react-email: specifier: ^2.1.1 - version: 2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(bufferutil@4.0.9)(eslint@8.31.0) + version: 2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(eslint@8.31.0) resend: specifier: ^3.2.0 version: 3.2.0 @@ -1435,31 +1435,6 @@ importers: specifier: 8.6.6 version: 8.6.6 - packages/ai: - dependencies: - '@trigger.dev/core': - specifier: workspace:4.3.3 - version: link:../core - devDependencies: - '@arethetypeswrong/cli': - specifier: ^0.15.4 - version: 0.15.4 - ai: - specifier: ^6.0.0 - version: 6.0.116(zod@3.25.76) - rimraf: - specifier: ^3.0.2 - version: 3.0.2 - tshy: - specifier: ^3.0.2 - version: 3.0.2 - tsx: - specifier: 4.17.0 - version: 4.17.0 - vitest: - specifier: ^2.1.0 - version: 2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3) - packages/build: dependencies: '@prisma/config': @@ -11167,23 +11142,9 @@ packages: '@vitest/browser': optional: true - '@vitest/expect@2.1.9': - resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - '@vitest/expect@3.1.4': resolution: {integrity: sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==} - '@vitest/mocker@2.1.9': - resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.4.2 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - '@vitest/mocker@3.1.4': resolution: {integrity: sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==} peerDependencies: @@ -11207,15 +11168,9 @@ packages: '@vitest/runner@3.1.4': resolution: {integrity: sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==} - '@vitest/snapshot@2.1.9': - resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - '@vitest/snapshot@3.1.4': resolution: {integrity: sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==} - '@vitest/spy@2.1.9': - resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} - '@vitest/spy@3.1.4': resolution: {integrity: sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==} @@ -19753,11 +19708,6 @@ packages: engines: {node: '>=v14.16.0'} hasBin: true - vite-node@2.1.9: - resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - vite-node@3.1.4: resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -19834,31 +19784,6 @@ packages: yaml: optional: true - vitest@2.1.9: - resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': 20.14.14 - '@vitest/browser': 2.1.9 - '@vitest/ui': 2.1.9 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vitest@3.1.4: resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -31573,13 +31498,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/expect@2.1.9': - dependencies: - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.2.0 - tinyrainbow: 1.2.0 - '@vitest/expect@3.1.4': dependencies: '@vitest/spy': 3.1.4 @@ -31587,14 +31505,6 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@2.1.9(vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3))': - dependencies: - '@vitest/spy': 2.1.9 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3) - '@vitest/mocker@3.1.4(vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@3.12.2)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.1.4 @@ -31621,22 +31531,12 @@ snapshots: '@vitest/utils': 3.1.4 pathe: 2.0.3 - '@vitest/snapshot@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - magic-string: 0.30.21 - pathe: 1.1.2 - '@vitest/snapshot@3.1.4': dependencies: '@vitest/pretty-format': 3.1.4 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@2.1.9': - dependencies: - tinyspy: 3.0.2 - '@vitest/spy@3.1.4': dependencies: tinyspy: 3.0.2 @@ -39157,7 +39057,7 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-email@2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(bufferutil@4.0.9)(eslint@8.31.0): + react-email@2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(eslint@8.31.0): dependencies: '@babel/parser': 7.24.1 '@radix-ui/colors': 1.0.1 @@ -39194,8 +39094,8 @@ snapshots: react: 18.3.1 react-dom: 18.2.0(react@18.3.1) shelljs: 0.8.5 - socket.io: 4.7.3(bufferutil@4.0.9) - socket.io-client: 4.7.3(bufferutil@4.0.9) + socket.io: 4.7.3 + socket.io-client: 4.7.3 sonner: 1.3.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1) source-map-js: 1.0.2 stacktrace-parser: 0.1.10 @@ -40422,7 +40322,7 @@ snapshots: - supports-color - utf-8-validate - socket.io-client@4.7.3(bufferutil@4.0.9): + socket.io-client@4.7.3: dependencies: '@socket.io/component-emitter': 3.1.0 debug: 4.3.7(supports-color@10.0.0) @@ -40451,7 +40351,7 @@ snapshots: transitivePeerDependencies: - supports-color - socket.io@4.7.3(bufferutil@4.0.9): + socket.io@4.7.3: dependencies: accepts: 1.3.8 base64id: 2.0.0 @@ -42018,27 +41918,6 @@ snapshots: - supports-color - terser - vite-node@2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3): - dependencies: - cac: 6.7.14 - debug: 4.4.3(supports-color@10.0.0) - es-module-lexer: 1.7.0 - pathe: 1.1.2 - vite: 6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vite-node@3.1.4(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@3.12.2)(yaml@2.8.3): dependencies: cac: 6.7.14 @@ -42118,23 +41997,6 @@ snapshots: tsx: 3.12.2 yaml: 2.8.3 - vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3): - dependencies: - esbuild: 0.25.1 - fdir: 6.4.4(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.10 - rollup: 4.60.1 - tinyglobby: 0.2.13 - optionalDependencies: - '@types/node': 20.14.14 - fsevents: 2.3.3 - jiti: 2.4.2 - lightningcss: 1.29.2 - terser: 5.44.1 - tsx: 4.17.0 - yaml: 2.8.3 - vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3): dependencies: esbuild: 0.25.1 @@ -42152,44 +42014,6 @@ snapshots: tsx: 4.20.6 yaml: 2.8.3 - vitest@2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3): - dependencies: - '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)) - '@vitest/pretty-format': 2.1.9 - '@vitest/runner': 2.1.9 - '@vitest/snapshot': 2.1.9 - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.2.0 - debug: 4.4.3(supports-color@10.0.0) - expect-type: 1.2.1 - magic-string: 0.30.21 - pathe: 1.1.2 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.0.2 - tinyrainbow: 1.2.0 - vite: 6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3) - vite-node: 2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 20.14.14 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@3.12.2)(yaml@2.8.3): dependencies: '@vitest/expect': 3.1.4