build full example with persisting messages, adding necessary hooks, and documenting it all

This commit is contained in:
Eric Allam
2026-03-05 14:14:17 +00:00
parent c5ec4a034b
commit 4e6a5c83fd
20 changed files with 1472 additions and 45 deletions
+223 -3
View File
@@ -379,6 +379,47 @@ async function pipeChat(
* emits a control chunk and suspends via `messagesInput.wait()`. The frontend
* transport resumes the same run by sending the next message via input streams.
*/
/**
* Event passed to the `onChatStart` callback.
*/
export type ChatStartEvent = {
/** The unique identifier for the chat session. */
chatId: string;
/** The initial model-ready messages for this conversation. */
messages: ModelMessage[];
/** Custom data from the frontend (passed via `metadata` on `sendMessage()` or the transport). */
clientData: unknown;
};
/**
* Event passed to the `onTurnComplete` callback.
*/
export type TurnCompleteEvent = {
/** The unique identifier for the chat session. */
chatId: string;
/** The full accumulated conversation in model format (all turns so far). */
messages: ModelMessage[];
/**
* The full accumulated conversation in UI format (all turns so far).
* This is the format expected by `useChat` — store this for persistence.
*/
uiMessages: UIMessage[];
/**
* Only the new model messages from this turn (user message(s) + assistant response).
* Useful for appending to an existing conversation record.
*/
newMessages: ModelMessage[];
/**
* Only the new UI messages from this turn (user message(s) + assistant response).
* Useful for inserting individual message records instead of overwriting the full history.
*/
newUIMessages: UIMessage[];
/** The assistant's response for this turn (undefined if `pipeChat` was used manually). */
responseMessage: UIMessage | undefined;
/** The turn number (0-indexed). */
turn: number;
};
export type ChatTaskOptions<TIdentifier extends string> = Omit<
TaskOptions<TIdentifier, ChatTaskWirePayload, unknown>,
"run"
@@ -394,6 +435,35 @@ export type ChatTaskOptions<TIdentifier extends string> = Omit<
*/
run: (payload: ChatTaskRunPayload) => Promise<unknown>;
/**
* Called on the first turn (turn 0) of a new run, before the `run` function executes.
*
* Use this to create the chat record in your database when a new conversation starts.
*
* @example
* ```ts
* onChatStart: async ({ chatId, messages, clientData }) => {
* await db.chat.create({ data: { id: chatId, userId: clientData.userId } });
* }
* ```
*/
onChatStart?: (event: ChatStartEvent) => Promise<void> | void;
/**
* Called after each turn completes (after the response is captured, before waiting
* for the next message). Also fires on the final turn.
*
* Use this to persist the conversation to your database after each assistant response.
*
* @example
* ```ts
* onTurnComplete: async ({ chatId, messages }) => {
* await db.chat.update({ where: { id: chatId }, data: { messages } });
* }
* ```
*/
onTurnComplete?: (event: TurnCompleteEvent) => Promise<void> | void;
/**
* Maximum number of conversational turns (message round-trips) a single run
* will handle before ending. After this many turns the run completes
@@ -456,6 +526,8 @@ function chatTask<TIdentifier extends string>(
): Task<TIdentifier, ChatTaskWirePayload, unknown> {
const {
run: userRun,
onChatStart,
onTurnComplete,
maxTurns = 100,
turnTimeout = "1h",
warmTimeoutInSeconds = 30,
@@ -478,6 +550,10 @@ function chatTask<TIdentifier extends string>(
// user message(s) and the captured assistant response.
let accumulatedMessages: ModelMessage[] = [];
// Accumulated UI messages for persistence. Mirrors the model accumulator
// but in frontend-friendly UIMessage format (with parts, id, etc.).
let accumulatedUIMessages: UIMessage[] = [];
// Mutable reference to the current turn's stop controller so the
// stop input stream listener (registered once) can abort the right turn.
let currentStopController: AbortController | undefined;
@@ -549,15 +625,52 @@ function chatTask<TIdentifier extends string>(
// Turn 2+: only the new message(s) → appended to the accumulator.
const incomingModelMessages = await convertToModelMessages(uiMessages);
// Track new messages for this turn (user input + assistant response).
const turnNewModelMessages: ModelMessage[] = [];
const turnNewUIMessages: UIMessage[] = [];
if (turn === 0) {
accumulatedMessages = incomingModelMessages;
accumulatedUIMessages = [...uiMessages];
// On first turn, the "new" messages are just the last user message
// (the rest is history). We'll add the response after streaming.
if (uiMessages.length > 0) {
turnNewUIMessages.push(uiMessages[uiMessages.length - 1]!);
const lastModel = incomingModelMessages[incomingModelMessages.length - 1];
if (lastModel) turnNewModelMessages.push(lastModel);
}
} else if (currentWirePayload.trigger === "regenerate-message") {
// Regenerate: frontend sent full history with last assistant message
// removed. Reset the accumulator to match.
accumulatedMessages = incomingModelMessages;
accumulatedUIMessages = [...uiMessages];
// No new user messages for regenerate — just the response (added below)
} else {
// Submit: frontend sent only the new user message(s). Append to accumulator.
accumulatedMessages.push(...incomingModelMessages);
accumulatedUIMessages.push(...uiMessages);
turnNewModelMessages.push(...incomingModelMessages);
turnNewUIMessages.push(...uiMessages);
}
// Fire onChatStart on the first turn
if (turn === 0 && onChatStart) {
await tracer.startActiveSpan(
"onChatStart()",
async () => {
await onChatStart({
chatId: currentWirePayload.chatId,
messages: accumulatedMessages,
clientData: wireMetadata,
});
},
{
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
[SemanticInternalAttributes.COLLAPSED]: true,
},
}
);
}
// Captured by the onFinish callback below — works even on abort/stop.
@@ -602,11 +715,14 @@ function chatTask<TIdentifier extends string>(
// The onFinish callback fires even on abort/stop, so partial responses
// from stopped generation are captured correctly.
if (capturedResponseMessage) {
accumulatedUIMessages.push(capturedResponseMessage);
turnNewUIMessages.push(capturedResponseMessage);
try {
const responseModelMessages = await convertToModelMessages([
stripProviderMetadata(capturedResponseMessage),
]);
accumulatedMessages.push(...responseModelMessages);
turnNewModelMessages.push(...responseModelMessages);
} catch {
// Conversion failed — skip accumulation for this turn
}
@@ -618,6 +734,30 @@ function chatTask<TIdentifier extends string>(
if (runSignal.aborted) return "exit";
// Fire onTurnComplete after response capture
if (onTurnComplete) {
await tracer.startActiveSpan(
"onTurnComplete()",
async () => {
await onTurnComplete({
chatId: currentWirePayload.chatId,
messages: accumulatedMessages,
uiMessages: accumulatedUIMessages,
newMessages: turnNewModelMessages,
newUIMessages: turnNewUIMessages,
responseMessage: capturedResponseMessage,
turn,
});
},
{
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
[SemanticInternalAttributes.COLLAPSED]: true,
},
}
);
}
// Write turn-complete control chunk so frontend closes its stream
await writeTurnCompleteChunk(currentWirePayload.chatId);
@@ -629,9 +769,12 @@ function chatTask<TIdentifier extends string>(
// Phase 1: Keep the run warm for quick response to the next message.
// The run stays active (using compute) during this window.
if (warmTimeoutInSeconds > 0) {
const effectiveWarmTimeout =
(metadata.get(WARM_TIMEOUT_METADATA_KEY) as number | undefined) ?? warmTimeoutInSeconds;
if (effectiveWarmTimeout > 0) {
const warm = await messagesInput.once({
timeoutMs: warmTimeoutInSeconds * 1000,
timeoutMs: effectiveWarmTimeout * 1000,
spanName: "waiting (warm)",
});
@@ -643,8 +786,11 @@ function chatTask<TIdentifier extends string>(
}
// Phase 2: Suspend the task (frees compute) until the next message arrives
const effectiveTurnTimeout =
(metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout;
const next = await messagesInput.wait({
timeout: turnTimeout,
timeout: effectiveTurnTimeout,
spanName: "waiting (suspended)",
});
@@ -693,6 +839,74 @@ function chatTask<TIdentifier extends string>(
* const token = await chat.createAccessToken("my-chat");
* ```
*/
// ---------------------------------------------------------------------------
// Runtime configuration helpers
// ---------------------------------------------------------------------------
const TURN_TIMEOUT_METADATA_KEY = "chat.turnTimeout";
const WARM_TIMEOUT_METADATA_KEY = "chat.warmTimeout";
/**
* Override the turn timeout for subsequent turns in the current run.
*
* The turn timeout controls how long the run stays suspended (freeing compute)
* waiting for the next user message. When it expires, the run completes
* gracefully and the next message starts a fresh run.
*
* Call from inside a `chatTask` run function to adjust based on context.
*
* @param duration - A duration string (e.g. `"5m"`, `"1h"`, `"30s"`)
*
* @example
* ```ts
* run: async ({ messages, signal }) => {
* chat.setTurnTimeout("2h");
* return streamText({ model, messages, abortSignal: signal });
* }
* ```
*/
function setTurnTimeout(duration: string): void {
metadata.set(TURN_TIMEOUT_METADATA_KEY, duration);
}
/**
* Override the turn timeout in seconds for subsequent turns in the current run.
*
* @param seconds - Number of seconds to wait for the next message before ending the run
*
* @example
* ```ts
* run: async ({ messages, signal }) => {
* chat.setTurnTimeoutInSeconds(3600); // 1 hour
* return streamText({ model, messages, abortSignal: signal });
* }
* ```
*/
function setTurnTimeoutInSeconds(seconds: number): void {
metadata.set(TURN_TIMEOUT_METADATA_KEY, `${seconds}s`);
}
/**
* Override the warm timeout for subsequent turns in the current run.
*
* The warm timeout controls how long the run stays active (using compute)
* after each turn, waiting for the next message. During this window,
* responses are instant. After it expires, the run suspends.
*
* @param seconds - Number of seconds to stay warm (0 to suspend immediately)
*
* @example
* ```ts
* run: async ({ messages, signal }) => {
* chat.setWarmTimeoutInSeconds(60);
* return streamText({ model, messages, abortSignal: signal });
* }
* ```
*/
function setWarmTimeoutInSeconds(seconds: number): void {
metadata.set(WARM_TIMEOUT_METADATA_KEY, seconds);
}
export const chat = {
/** Create a chat task. See {@link chatTask}. */
task: chatTask,
@@ -700,6 +914,12 @@ export const chat = {
pipe: pipeChat,
/** Create a public access token for a chat task. See {@link createChatAccessToken}. */
createAccessToken: createChatAccessToken,
/** Override the turn timeout at runtime (duration string). See {@link setTurnTimeout}. */
setTurnTimeout,
/** Override the turn timeout at runtime (seconds). See {@link setTurnTimeoutInSeconds}. */
setTurnTimeoutInSeconds,
/** Override the warm timeout at runtime. See {@link setWarmTimeoutInSeconds}. */
setWarmTimeoutInSeconds,
};
/**
+11 -1
View File
@@ -23,7 +23,7 @@
* ```
*/
import { useRef } from "react";
import { useEffect, useRef } from "react";
import {
TriggerChatTransport,
type TriggerChatTransportOptions,
@@ -57,6 +57,9 @@ export type UseTriggerChatTransportOptions<TTask extends AnyTask = AnyTask> = Om
* For dynamic access tokens, pass a function — it will be called on each
* request without needing to recreate the transport.
*
* The `onSessionChange` callback is kept in a ref so the transport always
* calls the latest version without needing to be recreated.
*
* @example
* ```tsx
* import { useChat } from "@ai-sdk/react";
@@ -80,5 +83,12 @@ export function useTriggerChatTransport<TTask extends AnyTask = AnyTask>(
if (ref.current === null) {
ref.current = new TriggerChatTransport(options);
}
// Keep onSessionChange up to date without recreating the transport
const { onSessionChange } = options;
useEffect(() => {
ref.current?.setOnSessionChange(onSessionChange);
}, [onSessionChange]);
return ref.current;
}
+183
View File
@@ -1727,4 +1727,187 @@ describe("TriggerChatTransport", () => {
expect(triggerCallCount).toBe(2);
});
});
describe("onSessionChange", () => {
it("should fire when a new session is created", async () => {
const onSessionChange = vi.fn();
const triggerRunId = "run_session_new";
const publicToken = "pub_session_new";
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 = [
...sampleChunks,
{ type: "__trigger_turn_complete" },
];
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({
task: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
onSessionChange,
});
const stream = await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-1",
messageId: undefined,
messages: [createUserMessage("Hello")],
abortSignal: undefined,
});
// Session created notification should have fired
expect(onSessionChange).toHaveBeenCalledWith("chat-1", {
runId: triggerRunId,
publicAccessToken: publicToken,
lastEventId: undefined,
});
// Consume stream
const reader = stream.getReader();
while (!(await reader.read()).done) {}
// Should also fire with updated lastEventId on turn complete
const lastCall = onSessionChange.mock.calls[onSessionChange.mock.calls.length - 1]!;
expect(lastCall![0]).toBe("chat-1");
expect(lastCall![1]).not.toBeNull();
expect(lastCall![1].lastEventId).toBeDefined();
});
it("should fire with null when session is deleted (stream ends naturally)", async () => {
const onSessionChange = vi.fn();
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_end" }), {
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "pub_end",
},
});
}
if (urlStr.includes("/realtime/v1/streams/")) {
// No turn-complete chunk — stream ends naturally (run completed)
return new Response(createSSEStream(sseEncode(sampleChunks)), {
status: 200,
headers: {
"content-type": "text/event-stream",
"X-Stream-Version": "v1",
},
});
}
throw new Error(`Unexpected fetch URL: ${urlStr}`);
});
const transport = new TriggerChatTransport({
task: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
onSessionChange,
});
const stream = await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-end",
messageId: undefined,
messages: [createUserMessage("Hello")],
abortSignal: undefined,
});
// Consume the stream fully
const reader = stream.getReader();
while (!(await reader.read()).done) {}
// Session should have been created then deleted
expect(onSessionChange).toHaveBeenCalledWith("chat-end", expect.objectContaining({
runId: "run_end",
}));
expect(onSessionChange).toHaveBeenCalledWith("chat-end", null);
});
it("should be updatable via setOnSessionChange", async () => {
const onSessionChange1 = vi.fn();
const onSessionChange2 = vi.fn();
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_update" }), {
status: 200,
headers: {
"content-type": "application/json",
"x-trigger-jwt": "pub_update",
},
});
}
if (urlStr.includes("/realtime/v1/streams/")) {
const chunks = [
...sampleChunks,
{ type: "__trigger_turn_complete" },
];
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({
task: "my-task",
accessToken: "token",
baseURL: "https://api.test.trigger.dev",
onSessionChange: onSessionChange1,
});
// Update the callback before sending
transport.setOnSessionChange(onSessionChange2);
await transport.sendMessages({
trigger: "submit-message",
chatId: "chat-update",
messageId: undefined,
messages: [createUserMessage("Hello")],
abortSignal: undefined,
});
// Only onSessionChange2 should have been called
expect(onSessionChange1).not.toHaveBeenCalled();
expect(onSessionChange2).toHaveBeenCalled();
});
});
});
+136 -2
View File
@@ -100,6 +100,57 @@ export type TriggerChatTransportOptions = {
* ```
*/
metadata?: Record<string, unknown>;
/**
* Restore active chat sessions from external storage (e.g. localStorage).
*
* After a page refresh, pass previously persisted sessions here so the
* transport can reconnect to existing runs instead of starting new ones.
* Use `getSession()` to retrieve session state for persistence.
*
* @example
* ```ts
* new TriggerChatTransport({
* task: "my-chat",
* accessToken,
* sessions: {
* "chat-abc": { runId: "run_123", publicAccessToken: "...", lastEventId: "42" },
* },
* });
* ```
*/
sessions?: Record<string, { runId: string; publicAccessToken: string; lastEventId?: string }>;
/**
* Called whenever a chat session's state changes.
*
* Fires when:
* - A new session is created (after triggering a task)
* - A turn completes (lastEventId updated)
* - A session is removed (run ended or input stream send failed) — `session` will be `null`
*
* Use this to persist session state for reconnection after page refreshes,
* without needing to call `getSession()` manually.
*
* @example
* ```ts
* new TriggerChatTransport({
* task: "my-chat",
* accessToken,
* onSessionChange: (chatId, session) => {
* if (session) {
* localStorage.setItem(`session:${chatId}`, JSON.stringify(session));
* } else {
* localStorage.removeItem(`session:${chatId}`);
* }
* },
* });
* ```
*/
onSessionChange?: (
chatId: string,
session: { runId: string; publicAccessToken: string; lastEventId?: string } | null
) => void;
};
/**
@@ -151,6 +202,12 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
private readonly extraHeaders: Record<string, string>;
private readonly streamTimeoutSeconds: number;
private readonly defaultMetadata: Record<string, unknown> | undefined;
private _onSessionChange:
| ((
chatId: string,
session: { runId: string; publicAccessToken: string; lastEventId?: string } | null
) => void)
| undefined;
private sessions: Map<string, ChatSessionState> = new Map();
@@ -165,6 +222,18 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
this.extraHeaders = options.headers ?? {};
this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS;
this.defaultMetadata = options.metadata;
this._onSessionChange = options.onSessionChange;
// Restore sessions from external storage
if (options.sessions) {
for (const [chatId, session] of Object.entries(options.sessions)) {
this.sessions.set(chatId, {
runId: session.runId,
publicAccessToken: session.publicAccessToken,
lastEventId: session.lastEventId,
});
}
}
}
sendMessages = async (
@@ -216,6 +285,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
} catch {
// If sending fails (run died, etc.), fall through to trigger a new run.
this.sessions.delete(chatId);
this.notifySessionChange(chatId, null);
}
}
@@ -236,10 +306,12 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
? (triggerResponse as { publicAccessToken?: string }).publicAccessToken
: undefined;
this.sessions.set(chatId, {
const newSession: ChatSessionState = {
runId,
publicAccessToken: publicAccessToken ?? currentToken,
});
};
this.sessions.set(chatId, newSession);
this.notifySessionChange(chatId, newSession);
return this.subscribeToStream(
runId,
publicAccessToken ?? currentToken,
@@ -261,6 +333,62 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
return this.subscribeToStream(session.runId, session.publicAccessToken, undefined, options.chatId);
};
/**
* Get the current session state for a chat, suitable for external persistence.
*
* Returns `undefined` if no active session exists for this chatId.
* Persist the returned value to localStorage so it can be restored
* after a page refresh via `restoreSession()`.
*
* @example
* ```ts
* const session = transport.getSession(chatId);
* if (session) {
* localStorage.setItem(`session:${chatId}`, JSON.stringify(session));
* }
* ```
*/
getSession = (chatId: string): { runId: string; publicAccessToken: string; lastEventId?: string } | undefined => {
const session = this.sessions.get(chatId);
if (!session) return undefined;
return {
runId: session.runId,
publicAccessToken: session.publicAccessToken,
lastEventId: session.lastEventId,
};
};
/**
* Update the `onSessionChange` callback.
* Useful for React hooks that need to update the callback without recreating the transport.
*/
setOnSessionChange(
callback:
| ((
chatId: string,
session: { runId: string; publicAccessToken: string; lastEventId?: string } | null
) => void)
| undefined
): void {
this._onSessionChange = callback;
}
private notifySessionChange(
chatId: string,
session: ChatSessionState | null
): void {
if (!this._onSessionChange) return;
if (session) {
this._onSessionChange(chatId, {
runId: session.runId,
publicAccessToken: session.publicAccessToken,
lastEventId: session.lastEventId,
});
} else {
this._onSessionChange(chatId, null);
}
}
private subscribeToStream(
runId: string,
accessToken: string,
@@ -331,6 +459,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
// the next message via input streams.
if (chatId && !combinedSignal.aborted) {
this.sessions.delete(chatId);
this.notifySessionChange(chatId, null);
}
controller.close();
return;
@@ -348,6 +477,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
session.lastEventId = value.id;
}
// Guard against heartbeat or malformed SSE events
if (value.chunk != null && typeof value.chunk === "object") {
const chunk = value.chunk as Record<string, unknown>;
@@ -363,6 +493,10 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
}
if (chunk.type === "__trigger_turn_complete" && chatId) {
// Notify with updated lastEventId before closing
if (session) {
this.notifySessionChange(chatId, session);
}
internalAbort.abort();
try {
controller.close();
+386 -4
View File
@@ -2181,6 +2181,12 @@ importers:
'@ai-sdk/react':
specifier: ^3.0.0
version: 3.0.170(react@19.1.0)(zod@3.25.76)
'@prisma/adapter-pg':
specifier: ^7.4.2
version: 7.4.2
'@prisma/client':
specifier: ^7.4.2
version: 7.4.2(prisma@7.4.2(@types/react@19.0.12)(better-sqlite3@11.10.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4))(typescript@5.5.4)
'@trigger.dev/sdk':
specifier: workspace:*
version: link:../../packages/trigger-sdk
@@ -2190,6 +2196,9 @@ importers:
next:
specifier: 15.3.3
version: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.37.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
pg:
specifier: ^8.16.3
version: 8.16.3
react:
specifier: ^19.0.0
version: 19.1.0
@@ -2218,6 +2227,9 @@ importers:
'@types/react-dom':
specifier: ^19
version: 19.0.4(@types/react@19.2.14)
prisma:
specifier: ^7.4.2
version: 7.4.2(@types/react@19.0.12)(better-sqlite3@11.10.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4)
tailwindcss:
specifier: ^4
version: 4.0.17
@@ -4157,18 +4169,30 @@ packages:
'@changesets/write@0.2.3':
resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==}
'@chevrotain/cst-dts-gen@10.5.0':
resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==}
'@chevrotain/cst-dts-gen@11.0.3':
resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==}
'@chevrotain/gast@10.5.0':
resolution: {integrity: sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==}
'@chevrotain/gast@11.0.3':
resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==}
'@chevrotain/regexp-to-ast@11.0.3':
resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==}
'@chevrotain/types@10.5.0':
resolution: {integrity: sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==}
'@chevrotain/types@11.0.3':
resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==}
'@chevrotain/utils@10.5.0':
resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==}
'@chevrotain/utils@11.0.3':
resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==}
@@ -4362,6 +4386,20 @@ packages:
'@electric-sql/client@1.0.14':
resolution: {integrity: sha512-LtPAfeMxXRiYS0hyDQ5hue2PjljUiK9stvzsVyVb4nwxWQxfOWTSF42bHTs/o5i3x1T4kAQ7mwHpxa4A+f8X7Q==}
'@electric-sql/pglite-socket@0.0.20':
resolution: {integrity: sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==}
hasBin: true
peerDependencies:
'@electric-sql/pglite': 0.3.15
'@electric-sql/pglite-tools@0.2.20':
resolution: {integrity: sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==}
peerDependencies:
'@electric-sql/pglite': 0.3.15
'@electric-sql/pglite@0.3.15':
resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==}
'@electric-sql/react@0.3.5':
resolution: {integrity: sha512-qPrlF3BsRg5L8zAn1sLGzc3pkswfEHyQI3lNOu7Xllv1DBx85RvHR1zgGGPAUfC8iwyWupQu9pFPE63GdbeuhA==}
peerDependencies:
@@ -5910,6 +5948,10 @@ packages:
'@cfworker/json-schema':
optional: true
'@mrleebo/prisma-ast@0.13.1':
resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==}
engines: {node: '>=16'}
'@msgpack/msgpack@3.0.0-beta2':
resolution: {integrity: sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==}
engines: {node: '>= 14'}
@@ -6925,9 +6967,15 @@ packages:
'@prisma/adapter-pg@6.20.0-integration-next.8':
resolution: {integrity: sha512-5+ZjSPMzyfDYMmWLH1IaQIOQGa8eJrqEz5A9V4vS4+b6LV6qvCOHjqlnbRQ5IKSNCwFP055SJ54RsPES+0jOyA==}
'@prisma/adapter-pg@7.4.2':
resolution: {integrity: sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ==}
'@prisma/client-runtime-utils@6.20.0-integration-next.8':
resolution: {integrity: sha512-prENLjPislFvRWDHNgXmg9yzixQYsFPVQGtDv5zIMs4pV2KPdNc5pCiZ3n77hAinvqGJVafASa+eU4TfpVphdA==}
'@prisma/client-runtime-utils@7.4.2':
resolution: {integrity: sha512-cID+rzOEb38VyMsx5LwJMEY4NGIrWCNpKu/0ImbeooQ2Px7TI+kOt7cm0NelxUzF2V41UVVXAmYjANZQtCu1/Q==}
'@prisma/client@4.9.0':
resolution: {integrity: sha512-bz6QARw54sWcbyR1lLnF2QHvRW5R/Jxnbbmwh3u+969vUKXtBkXgSgjDA85nji31ZBlf7+FrHDy5x+5ydGyQDg==}
engines: {node: '>=14.17'}
@@ -6985,6 +7033,18 @@ packages:
typescript:
optional: true
'@prisma/client@7.4.2':
resolution: {integrity: sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA==}
engines: {node: ^20.19 || ^22.12 || >=24.0}
peerDependencies:
prisma: '*'
typescript: 5.5.4
peerDependenciesMeta:
prisma:
optional: true
typescript:
optional: true
'@prisma/config@6.14.0':
resolution: {integrity: sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==}
@@ -6997,6 +7057,9 @@ packages:
'@prisma/config@6.20.0-integration-next.8':
resolution: {integrity: sha512-nwf+tczfiGSn0tnuHmBpnK+wmaYzcC20sn9Zt8BSoJVCewJxf8ASHPxZEGgvFLl05zbCfFtq3rMc6ZnAiYjowg==}
'@prisma/config@7.4.2':
resolution: {integrity: sha512-CftBjWxav99lzY1Z4oDgomdb1gh9BJFAOmWF6P2v1xRfXqQb56DfBub+QKcERRdNoAzCb3HXy3Zii8Vb4AsXhg==}
'@prisma/debug@4.16.2':
resolution: {integrity: sha512-7L7WbG0qNNZYgLpsVB8rCHCXEyHFyIycRlRDNwkVfjQmACC2OW6AWCYCbfdjQhkF/t7+S3njj8wAWAocSs+Brw==}
@@ -7012,12 +7075,24 @@ packages:
'@prisma/debug@6.20.0-integration-next.8':
resolution: {integrity: sha512-PqUUFXf8MDoIrsKMzpF4NYqA3gHE8l/CUWVnYa4hNIbynCcEhvk7iT+6ve0u9w1TiGVUFnIVMuqFGEb2aHCuFw==}
'@prisma/debug@7.2.0':
resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==}
'@prisma/debug@7.4.2':
resolution: {integrity: sha512-aP7qzu+g/JnbF6U69LMwHoUkELiserKmWsE2shYuEpNUJ4GrtxBCvZwCyCBHFSH2kLTF2l1goBlBh4wuvRq62w==}
'@prisma/dev@0.20.0':
resolution: {integrity: sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==}
'@prisma/driver-adapter-utils@6.16.0':
resolution: {integrity: sha512-dsRHvEnifJ3xqpMKGBy1jRwR8yc+7Ko4TcHrdTQJIfq6NYN2gNoOf0k91hcbzs5AH19wDxjuHXCveklWq5AJdA==}
'@prisma/driver-adapter-utils@6.20.0-integration-next.8':
resolution: {integrity: sha512-TXpFugr3sCl2bHechoG3p9mvlq2Z3GgA0Cp73lUOEWQyUuoG8NW/4UA56Ax1r5fBUAs9hKbr20Ld6wKCZhnz8Q==}
'@prisma/driver-adapter-utils@7.4.2':
resolution: {integrity: sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw==}
'@prisma/engines-version@4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5':
resolution: {integrity: sha512-M16aibbxi/FhW7z1sJCX8u+0DriyQYY5AyeTH7plQm9MLnURoiyn3CZBqAyIoQ+Z1pS77usCIibYJWSgleBMBA==}
@@ -7033,6 +7108,9 @@ packages:
'@prisma/engines-version@6.20.0-11.next-80ee0a44bf5668992b0c909c946a755b86b56c95':
resolution: {integrity: sha512-DqrQqRIgeocvWpgN7t9PymiJdV8ISSSrZCuilAtpKEaKIt4JUGIxsAdWNMRSHk188hYA2W1YFG5KvWUYBaCO1A==}
'@prisma/engines-version@7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919':
resolution: {integrity: sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg==}
'@prisma/engines@6.14.0':
resolution: {integrity: sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==}
@@ -7045,6 +7123,9 @@ packages:
'@prisma/engines@6.20.0-integration-next.8':
resolution: {integrity: sha512-XdzTxN0PFLIW2DcprG9xlMy39FrsjxW5J2qtHQ58FBtbllHSZGD0pK2nzATw5dRh7nGhmX+uNA02cqHv5oND3A==}
'@prisma/engines@7.4.2':
resolution: {integrity: sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA==}
'@prisma/fetch-engine@6.14.0':
resolution: {integrity: sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==}
@@ -7057,6 +7138,9 @@ packages:
'@prisma/fetch-engine@6.20.0-integration-next.8':
resolution: {integrity: sha512-zVNM5Q1hFclpqD1y7wujDzyc3l01S8ZMuP0Zddzuda4LOA7/F2enjro48VcD2/fxkBgzkkmO/quLOGnbQDKO7g==}
'@prisma/fetch-engine@7.4.2':
resolution: {integrity: sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA==}
'@prisma/generator-helper@4.16.2':
resolution: {integrity: sha512-bMOH7y73Ui7gpQrioFeavMQA+Tf8ksaVf8Nhs9rQNzuSg8SSV6E9baczob0L5KGZTSgYoqnrRxuo03kVJYrnIg==}
@@ -7072,6 +7156,12 @@ packages:
'@prisma/get-platform@6.20.0-integration-next.8':
resolution: {integrity: sha512-21jEfhFpC8FuvPD7JEf1Qu02engBCBa3+1il3UiyHKcKS3Kbp9IgR+DVqqrqSWIGJg8+1oTfF/3AgbjunaQ1Ag==}
'@prisma/get-platform@7.2.0':
resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==}
'@prisma/get-platform@7.4.2':
resolution: {integrity: sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw==}
'@prisma/instrumentation@6.11.1':
resolution: {integrity: sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==}
peerDependencies:
@@ -7082,6 +7172,9 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.8
'@prisma/query-plan-executor@7.2.0':
resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==}
'@prisma/studio-core-licensed@0.6.0':
resolution: {integrity: sha512-LNC8ohLosuWz6n9oKNqfR5Ep/JYiPavk4RxrU6inOS4LEvMQts8N+Vtt7NAB9i06BaiIRKnPsg1Hcaao5pRjSw==}
peerDependencies:
@@ -7089,6 +7182,13 @@ packages:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
'@prisma/studio-core@0.13.1':
resolution: {integrity: sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==}
peerDependencies:
'@types/react': ^18.0.0 || ^19.0.0
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
'@protobuf-ts/runtime@2.11.1':
resolution: {integrity: sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==}
@@ -11867,6 +11967,10 @@ packages:
aws-sign2@0.7.0:
resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
aws-ssl-profiles@1.1.2:
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
engines: {node: '>= 6.0.0'}
aws4@1.12.0:
resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==}
@@ -12240,6 +12344,9 @@ packages:
peerDependencies:
chevrotain: ^11.0.0
chevrotain@10.5.0:
resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==}
chevrotain@11.0.3:
resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==}
@@ -14131,6 +14238,10 @@ packages:
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
engines: {node: '>=14'}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
forever-agent@0.6.1:
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
@@ -14254,6 +14365,9 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
generate-function@2.3.1:
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
generic-names@4.0.0:
resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==}
@@ -14273,6 +14387,9 @@ packages:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
get-port-please@3.2.0:
resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==}
get-port@5.1.1:
resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==}
engines: {node: '>=8'}
@@ -14422,6 +14539,9 @@ packages:
resolution: {integrity: sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==}
engines: {node: '>=10'}
grammex@3.1.12:
resolution: {integrity: sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==}
grapheme-splitter@1.0.4:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
@@ -14434,6 +14554,9 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
graphmatch@1.1.1:
resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==}
graphql@16.6.0:
resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
@@ -14562,6 +14685,10 @@ packages:
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
hono@4.11.4:
resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==}
engines: {node: '>=16.9.0'}
hono@4.11.8:
resolution: {integrity: sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==}
engines: {node: '>=16.9.0'}
@@ -14616,6 +14743,9 @@ packages:
resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
engines: {node: '>=0.8', npm: '>=1.3.7'}
http-status-codes@2.3.0:
resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
@@ -14947,6 +15077,9 @@ packages:
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
is-reference@3.0.3:
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
@@ -15621,6 +15754,10 @@ packages:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
lru.min@1.1.4:
resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==}
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
lucide-react@0.229.0:
resolution: {integrity: sha512-b0/KSFXhPi++vUbnYEDUgP8Z8Rw9MQpRfBr+dRZNPMT3FD1HrVgMHXhSpkm9ZrrEtuqIfHf/O+tAGmw4WOmIog==}
peerDependencies:
@@ -16263,9 +16400,17 @@ packages:
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
hasBin: true
mysql2@3.15.3:
resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==}
engines: {node: '>= 8.0'}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
named-placeholders@1.1.6:
resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==}
engines: {node: '>=8.0.0'}
nan@2.23.1:
resolution: {integrity: sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==}
@@ -17545,6 +17690,19 @@ packages:
typescript:
optional: true
prisma@7.4.2:
resolution: {integrity: sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA==}
engines: {node: ^20.19 || ^22.12 || >=24.0}
hasBin: true
peerDependencies:
better-sqlite3: '>=9.0.0'
typescript: 5.5.4
peerDependenciesMeta:
better-sqlite3:
optional: true
typescript:
optional: true
prismjs@1.29.0:
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
engines: {node: '>=6'}
@@ -18031,6 +18189,9 @@ packages:
regex@6.0.1:
resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==}
regexp-to-ast@0.5.0:
resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==}
regexp.prototype.flags@1.4.3:
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
engines: {node: '>= 0.4'}
@@ -18103,6 +18264,9 @@ packages:
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
remeda@2.33.4:
resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==}
remend@1.2.1:
resolution: {integrity: sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w==}
@@ -18445,6 +18609,9 @@ packages:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
seq-queue@0.0.5:
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
serialize-javascript@6.0.1:
resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==}
@@ -18717,6 +18884,10 @@ packages:
resolution: {integrity: sha512-mkpF+RG402P66VMsnQkWewTRzDBWfu9iLbOfxaW/nAKOS/2A9MheQmcU5cmX0D0At9azrorZwpvcBRNNBozACQ==}
hasBin: true
sqlstring@2.3.3:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
sqs-consumer@7.5.0:
resolution: {integrity: sha512-aY3akgMjuK1aj4E7ZVAURUUnC8aNgUBES+b4SN+6ccMmJhi37MamWl7g1JbPow8sjIp1fBPz1bXCCDJmtjOTAg==}
engines: {node: '>=18.0.0'}
@@ -18769,6 +18940,9 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
std-env@3.7.0:
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
@@ -19870,8 +20044,8 @@ packages:
typescript:
optional: true
valibot@1.3.1:
resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==}
valibot@1.2.0:
resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==}
peerDependencies:
typescript: 5.5.4
peerDependenciesMeta:
@@ -20357,6 +20531,9 @@ packages:
yup@1.7.0:
resolution: {integrity: sha512-VJce62dBd+JQvoc+fCVq+KZfPHr+hXaxCcVgotfwWvlR0Ja3ffYKaJBT8rptPOSKOGJDCUnW2C2JWpud7aRP6Q==}
zeptomatch@2.1.0:
resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==}
zip-stream@6.0.1:
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
engines: {node: '>= 14'}
@@ -23070,12 +23247,23 @@ snapshots:
human-id: 1.0.2
prettier: 2.8.8
'@chevrotain/cst-dts-gen@10.5.0':
dependencies:
'@chevrotain/gast': 10.5.0
'@chevrotain/types': 10.5.0
lodash: 4.17.23
'@chevrotain/cst-dts-gen@11.0.3':
dependencies:
'@chevrotain/gast': 11.0.3
'@chevrotain/types': 11.0.3
lodash-es: 4.18.1
'@chevrotain/gast@10.5.0':
dependencies:
'@chevrotain/types': 10.5.0
lodash: 4.17.23
'@chevrotain/gast@11.0.3':
dependencies:
'@chevrotain/types': 11.0.3
@@ -23083,8 +23271,12 @@ snapshots:
'@chevrotain/regexp-to-ast@11.0.3': {}
'@chevrotain/types@10.5.0': {}
'@chevrotain/types@11.0.3': {}
'@chevrotain/utils@10.5.0': {}
'@chevrotain/utils@11.0.3': {}
'@clack/core@0.5.0':
@@ -23295,6 +23487,16 @@ snapshots:
optionalDependencies:
'@rollup/rollup-darwin-arm64': 4.53.2
'@electric-sql/pglite-socket@0.0.20(@electric-sql/pglite@0.3.15)':
dependencies:
'@electric-sql/pglite': 0.3.15
'@electric-sql/pglite-tools@0.2.20(@electric-sql/pglite@0.3.15)':
dependencies:
'@electric-sql/pglite': 0.3.15
'@electric-sql/pglite@0.3.15': {}
'@electric-sql/react@0.3.5(react@18.2.0)':
dependencies:
'@electric-sql/client': 0.4.0
@@ -24002,6 +24204,10 @@ snapshots:
dependencies:
hono: 4.5.11
'@hono/node-server@1.19.9(hono@4.11.4)':
dependencies:
hono: 4.11.4
'@hono/node-server@1.19.9(hono@4.11.8)':
dependencies:
hono: 4.11.8
@@ -24516,6 +24722,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@mrleebo/prisma-ast@0.13.1':
dependencies:
chevrotain: 10.5.0
lilconfig: 2.1.0
'@msgpack/msgpack@3.0.0-beta2': {}
'@neondatabase/serverless@0.9.5':
@@ -25576,8 +25787,18 @@ snapshots:
transitivePeerDependencies:
- pg-native
'@prisma/adapter-pg@7.4.2':
dependencies:
'@prisma/driver-adapter-utils': 7.4.2
pg: 8.16.3
postgres-array: 3.0.4
transitivePeerDependencies:
- pg-native
'@prisma/client-runtime-utils@6.20.0-integration-next.8': {}
'@prisma/client-runtime-utils@7.4.2': {}
'@prisma/client@4.9.0(prisma@6.14.0(magicast@0.3.5)(typescript@5.5.4))':
dependencies:
'@prisma/engines-version': 4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5
@@ -25606,6 +25827,13 @@ snapshots:
prisma: 6.20.0-integration-next.8(@types/react@19.2.14)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4)
typescript: 5.5.4
'@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.0.12)(better-sqlite3@11.10.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4))(typescript@5.5.4)':
dependencies:
'@prisma/client-runtime-utils': 7.4.2
optionalDependencies:
prisma: 7.4.2(@types/react@19.0.12)(better-sqlite3@11.10.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4)
typescript: 5.5.4
'@prisma/config@6.14.0(magicast@0.3.5)':
dependencies:
c12: 3.1.0(magicast@0.3.5)
@@ -25642,6 +25870,15 @@ snapshots:
transitivePeerDependencies:
- magicast
'@prisma/config@7.4.2(magicast@0.3.5)':
dependencies:
c12: 3.1.0(magicast@0.3.5)
deepmerge-ts: 7.1.5
effect: 3.18.4
empathic: 2.0.0
transitivePeerDependencies:
- magicast
'@prisma/debug@4.16.2':
dependencies:
'@types/debug': 4.1.8
@@ -25658,6 +25895,32 @@ snapshots:
'@prisma/debug@6.20.0-integration-next.8': {}
'@prisma/debug@7.2.0': {}
'@prisma/debug@7.4.2': {}
'@prisma/dev@0.20.0(typescript@5.5.4)':
dependencies:
'@electric-sql/pglite': 0.3.15
'@electric-sql/pglite-socket': 0.0.20(@electric-sql/pglite@0.3.15)
'@electric-sql/pglite-tools': 0.2.20(@electric-sql/pglite@0.3.15)
'@hono/node-server': 1.19.9(hono@4.11.4)
'@mrleebo/prisma-ast': 0.13.1
'@prisma/get-platform': 7.2.0
'@prisma/query-plan-executor': 7.2.0
foreground-child: 3.3.1
get-port-please: 3.2.0
hono: 4.11.4
http-status-codes: 2.3.0
pathe: 2.0.3
proper-lockfile: 4.1.2
remeda: 2.33.4
std-env: 3.10.0
valibot: 1.2.0(typescript@5.5.4)
zeptomatch: 2.1.0
transitivePeerDependencies:
- typescript
'@prisma/driver-adapter-utils@6.16.0':
dependencies:
'@prisma/debug': 6.16.0
@@ -25666,6 +25929,10 @@ snapshots:
dependencies:
'@prisma/debug': 6.20.0-integration-next.8
'@prisma/driver-adapter-utils@7.4.2':
dependencies:
'@prisma/debug': 7.4.2
'@prisma/engines-version@4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5': {}
'@prisma/engines-version@6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49': {}
@@ -25676,6 +25943,8 @@ snapshots:
'@prisma/engines-version@6.20.0-11.next-80ee0a44bf5668992b0c909c946a755b86b56c95': {}
'@prisma/engines-version@7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919': {}
'@prisma/engines@6.14.0':
dependencies:
'@prisma/debug': 6.14.0
@@ -25704,6 +25973,13 @@ snapshots:
'@prisma/fetch-engine': 6.20.0-integration-next.8
'@prisma/get-platform': 6.20.0-integration-next.8
'@prisma/engines@7.4.2':
dependencies:
'@prisma/debug': 7.4.2
'@prisma/engines-version': 7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919
'@prisma/fetch-engine': 7.4.2
'@prisma/get-platform': 7.4.2
'@prisma/fetch-engine@6.14.0':
dependencies:
'@prisma/debug': 6.14.0
@@ -25728,6 +26004,12 @@ snapshots:
'@prisma/engines-version': 6.20.0-11.next-80ee0a44bf5668992b0c909c946a755b86b56c95
'@prisma/get-platform': 6.20.0-integration-next.8
'@prisma/fetch-engine@7.4.2':
dependencies:
'@prisma/debug': 7.4.2
'@prisma/engines-version': 7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919
'@prisma/get-platform': 7.4.2
'@prisma/generator-helper@4.16.2':
dependencies:
'@prisma/debug': 4.16.2
@@ -25753,6 +26035,14 @@ snapshots:
dependencies:
'@prisma/debug': 6.20.0-integration-next.8
'@prisma/get-platform@7.2.0':
dependencies:
'@prisma/debug': 7.2.0
'@prisma/get-platform@7.4.2':
dependencies:
'@prisma/debug': 7.4.2
'@prisma/instrumentation@6.11.1(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@@ -25767,12 +26057,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@prisma/query-plan-executor@7.2.0': {}
'@prisma/studio-core-licensed@0.6.0(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@types/react': 19.2.14
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@prisma/studio-core@0.13.1(@types/react@19.0.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@types/react': 19.0.12
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@protobuf-ts/runtime@2.11.1': {}
'@protobufjs/aspromise@1.1.2': {}
@@ -32472,6 +32770,8 @@ snapshots:
aws-sign2@0.7.0: {}
aws-ssl-profiles@1.1.2: {}
aws4@1.12.0: {}
aws4fetch@1.0.18: {}
@@ -32893,6 +33193,15 @@ snapshots:
chevrotain: 11.0.3
lodash-es: 4.18.1
chevrotain@10.5.0:
dependencies:
'@chevrotain/cst-dts-gen': 10.5.0
'@chevrotain/gast': 10.5.0
'@chevrotain/types': 10.5.0
'@chevrotain/utils': 10.5.0
lodash: 4.17.23
regexp-to-ast: 0.5.0
chevrotain@11.0.3:
dependencies:
'@chevrotain/cst-dts-gen': 11.0.3
@@ -35198,6 +35507,11 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
forever-agent@0.6.1: {}
form-data-encoder@1.7.2: {}
@@ -35322,6 +35636,10 @@ snapshots:
functions-have-names@1.2.3: {}
generate-function@2.3.1:
dependencies:
is-property: 1.0.2
generic-names@4.0.0:
dependencies:
loader-utils: 3.2.1
@@ -35345,6 +35663,8 @@ snapshots:
get-nonce@1.0.1: {}
get-port-please@3.2.0: {}
get-port@5.1.1: {}
get-port@7.2.0: {}
@@ -35537,6 +35857,8 @@ snapshots:
chalk: 4.1.2
tinygradient: 1.1.5
grammex@3.1.12: {}
grapheme-splitter@1.0.4: {}
graphile-config@0.0.1-beta.8:
@@ -35569,6 +35891,8 @@ snapshots:
- supports-color
- typescript
graphmatch@1.1.1: {}
graphql@16.6.0: {}
gunzip-maybe@1.4.2:
@@ -35781,6 +36105,8 @@ snapshots:
dependencies:
react-is: 16.13.1
hono@4.11.4: {}
hono@4.11.8: {}
hono@4.5.11: {}
@@ -35845,6 +36171,8 @@ snapshots:
jsprim: 1.4.2
sshpk: 1.18.0
http-status-codes@2.3.0: {}
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
@@ -36131,6 +36459,8 @@ snapshots:
is-promise@4.0.0: {}
is-property@1.0.2: {}
is-reference@3.0.3:
dependencies:
'@types/estree': 1.0.8
@@ -36723,6 +37053,8 @@ snapshots:
lru-cache@7.18.3: {}
lru.min@1.1.4: {}
lucide-react@0.229.0(react@18.2.0):
dependencies:
react: 18.2.0
@@ -37766,12 +38098,28 @@ snapshots:
mustache@4.2.0: {}
mysql2@3.15.3:
dependencies:
aws-ssl-profiles: 1.1.2
denque: 2.1.0
generate-function: 2.3.1
iconv-lite: 0.7.2
long: 5.2.3
lru.min: 1.1.4
named-placeholders: 1.1.6
seq-queue: 0.0.5
sqlstring: 2.3.3
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
named-placeholders@1.1.6:
dependencies:
lru.min: 1.1.4
nan@2.23.1:
optional: true
@@ -39060,6 +39408,23 @@ snapshots:
- react
- react-dom
prisma@7.4.2(@types/react@19.0.12)(better-sqlite3@11.10.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4):
dependencies:
'@prisma/config': 7.4.2(magicast@0.3.5)
'@prisma/dev': 0.20.0(typescript@5.5.4)
'@prisma/engines': 7.4.2
'@prisma/studio-core': 0.13.1(@types/react@19.0.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
mysql2: 3.15.3
postgres: 3.4.7
optionalDependencies:
better-sqlite3: 11.10.0
typescript: 5.5.4
transitivePeerDependencies:
- '@types/react'
- magicast
- react
- react-dom
prismjs@1.29.0: {}
prismjs@1.30.0: {}
@@ -39881,6 +40246,8 @@ snapshots:
dependencies:
regex-utilities: 2.3.0
regexp-to-ast@0.5.0: {}
regexp.prototype.flags@1.4.3:
dependencies:
call-bind: 1.0.8
@@ -40020,7 +40387,11 @@ snapshots:
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
remix-auth-email-link@2.0.2(@remix-run/server-runtime@2.17.4(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.17.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.17.4(typescript@5.5.4))):
remeda@2.33.4: {}
remend@1.2.1: {}
remix-auth-email-link@2.0.2(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))):
dependencies:
'@remix-run/server-runtime': 2.17.4(typescript@5.5.4)
crypto-js: 4.1.1
@@ -40425,6 +40796,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
seq-queue@0.0.5: {}
serialize-javascript@6.0.1:
dependencies:
randombytes: 2.1.0
@@ -40820,6 +41193,8 @@ snapshots:
argparse: 2.0.1
nearley: 2.20.1
sqlstring@2.3.3: {}
sqs-consumer@7.5.0(@aws-sdk/client-sqs@3.454.0):
dependencies:
'@aws-sdk/client-sqs': 3.454.0
@@ -40885,6 +41260,8 @@ snapshots:
statuses@2.0.2: {}
std-env@3.10.0: {}
std-env@3.7.0: {}
std-env@3.8.1: {}
@@ -42209,7 +42586,7 @@ snapshots:
optionalDependencies:
typescript: 5.5.4
valibot@1.3.1(typescript@5.5.4):
valibot@1.2.0(typescript@5.5.4):
optionalDependencies:
typescript: 5.5.4
@@ -42825,6 +43202,11 @@ snapshots:
toposort: 2.0.2
type-fest: 2.19.0
zeptomatch@2.1.0:
dependencies:
grammex: 3.1.12
graphmatch: 1.1.1
zip-stream@6.0.1:
dependencies:
archiver-utils: 5.0.2
+1
View File
@@ -0,0 +1 @@
lib/generated/
+9 -1
View File
@@ -6,15 +6,22 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"dev:trigger": "trigger dev"
"dev:trigger": "trigger dev",
"db:migrate": "prisma migrate dev",
"db:push": "prisma db push",
"db:generate": "prisma generate",
"postinstall": "prisma generate"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.0",
"@ai-sdk/openai": "^3.0.0",
"@ai-sdk/react": "^3.0.0",
"@prisma/adapter-pg": "^7.4.2",
"@prisma/client": "^7.4.2",
"@trigger.dev/sdk": "workspace:*",
"ai": "^6.0.0",
"next": "15.3.3",
"pg": "^8.16.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"streamdown": "^2.3.0",
@@ -27,6 +34,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"prisma": "^7.4.2",
"trigger.dev": "workspace:*",
"typescript": "^5"
}
+12
View File
@@ -0,0 +1,12 @@
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: env("DATABASE_URL"),
},
});
@@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "Chat" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"messages" JSONB NOT NULL DEFAULT '[]',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Chat_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ChatSession" (
"id" TEXT NOT NULL,
"runId" TEXT NOT NULL,
"publicAccessToken" TEXT NOT NULL,
"lastEventId" TEXT,
CONSTRAINT "ChatSession_pkey" PRIMARY KEY ("id")
);
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+23
View File
@@ -0,0 +1,23 @@
generator client {
provider = "prisma-client"
output = "../lib/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model Chat {
id String @id
title String
messages Json @default("[]")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ChatSession {
id String @id // chatId
runId String
publicAccessToken String
lastEventId String?
}
+74
View File
@@ -2,5 +2,79 @@
import { chat } from "@trigger.dev/sdk/ai";
import type { aiChat } from "@/trigger/chat";
import { prisma } from "@/lib/prisma";
export const getChatToken = async () => chat.createAccessToken<typeof aiChat>("ai-chat");
export async function getChatList() {
const chats = await prisma.chat.findMany({
select: { id: true, title: true, createdAt: true, updatedAt: true },
orderBy: { updatedAt: "desc" },
});
return chats.map((c) => ({
id: c.id,
title: c.title,
createdAt: c.createdAt.getTime(),
updatedAt: c.updatedAt.getTime(),
}));
}
export async function getChatMessages(chatId: string) {
const found = await prisma.chat.findUnique({ where: { id: chatId } });
if (!found) return [];
return found.messages as any[];
}
export async function saveChatMessages(chatId: string, messages: unknown[]) {
await prisma.chat.update({
where: { id: chatId },
data: { messages: messages as any },
}).catch(() => {});
}
export async function deleteChat(chatId: string) {
await prisma.chat.delete({ where: { id: chatId } }).catch(() => {});
await prisma.chatSession.delete({ where: { id: chatId } }).catch(() => {});
}
export async function updateChatTitle(chatId: string, title: string) {
await prisma.chat.update({ where: { id: chatId }, data: { title } }).catch(() => {});
}
export async function saveSessionAction(
chatId: string,
session: { runId: string; publicAccessToken: string; lastEventId?: string }
) {
await prisma.chatSession.upsert({
where: { id: chatId },
create: {
id: chatId,
runId: session.runId,
publicAccessToken: session.publicAccessToken,
lastEventId: session.lastEventId,
},
update: {
runId: session.runId,
publicAccessToken: session.publicAccessToken,
lastEventId: session.lastEventId,
},
});
}
export async function deleteSessionAction(chatId: string) {
await prisma.chatSession.delete({ where: { id: chatId } }).catch(() => {});
}
export async function getAllSessions() {
const sessions = await prisma.chatSession.findMany();
const result: Record<string, { runId: string; publicAccessToken: string; lastEventId?: string }> =
{};
for (const s of sessions) {
result[s.id] = {
runId: s.runId,
publicAccessToken: s.publicAccessToken,
lastEventId: s.lastEventId ?? undefined,
};
}
return result;
}
+53 -4
View File
@@ -1,9 +1,58 @@
import { Chat } from "@/components/chat";
"use client";
import type { UIMessage } from "ai";
import { useEffect, useState } from "react";
import { ChatApp } from "@/components/chat-app";
import {
getChatList,
getChatMessages,
getAllSessions,
} from "@/app/actions";
type ChatMeta = {
id: string;
title: string;
createdAt: number;
updatedAt: number;
};
export default function Home() {
const [chatList, setChatList] = useState<ChatMeta[]>([]);
const [activeChatId, setActiveChatId] = useState<string | null>(null);
const [initialMessages, setInitialMessages] = useState<UIMessage[]>([]);
const [initialSessions, setInitialSessions] = useState<
Record<string, { runId: string; publicAccessToken: string; lastEventId?: string }>
>({});
const [loaded, setLoaded] = useState(false);
useEffect(() => {
async function load() {
const [list, sessions] = await Promise.all([getChatList(), getAllSessions()]);
setChatList(list);
setInitialSessions(sessions);
let firstChatId: string | null = null;
let firstMessages: UIMessage[] = [];
if (list.length > 0) {
firstChatId = list[0]!.id;
firstMessages = await getChatMessages(firstChatId);
}
setActiveChatId(firstChatId);
setInitialMessages(firstMessages);
setLoaded(true);
}
load();
}, []);
if (!loaded) return null;
return (
<main className="flex h-screen flex-col">
<Chat />
</main>
<ChatApp
initialChatList={chatList}
initialActiveChatId={activeChatId}
initialMessages={initialMessages}
initialSessions={initialSessions}
/>
);
}
@@ -0,0 +1,155 @@
"use client";
import type { UIMessage } from "ai";
import { generateId } from "ai";
import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
import { useCallback, useEffect, useState } from "react";
import { Chat } from "@/components/chat";
import { ChatSidebar } from "@/components/chat-sidebar";
import {
getChatToken,
getChatList,
getChatMessages,
deleteChat as deleteChatAction,
updateChatTitle,
saveSessionAction,
deleteSessionAction,
saveChatMessages,
} from "@/app/actions";
type ChatMeta = {
id: string;
title: string;
createdAt: number;
updatedAt: number;
};
type ChatAppProps = {
initialChatList: ChatMeta[];
initialActiveChatId: string | null;
initialMessages: UIMessage[];
initialSessions: Record<
string,
{ runId: string; publicAccessToken: string; lastEventId?: string }
>;
};
export function ChatApp({
initialChatList,
initialActiveChatId,
initialMessages,
initialSessions,
}: ChatAppProps) {
const [chatList, setChatList] = useState<ChatMeta[]>(initialChatList);
const [activeChatId, setActiveChatId] = useState<string | null>(initialActiveChatId);
const [messages, setMessages] = useState<UIMessage[]>(initialMessages);
const handleSessionChange = useCallback(
(
chatId: string,
session: { runId: string; publicAccessToken: string; lastEventId?: string } | null
) => {
if (session) {
saveSessionAction(chatId, session);
} else {
deleteSessionAction(chatId);
}
},
[]
);
const transport = useTriggerChatTransport({
task: "ai-chat",
accessToken: getChatToken,
baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL,
sessions: initialSessions,
onSessionChange: handleSessionChange,
});
// Load messages when active chat changes
useEffect(() => {
if (!activeChatId) {
setMessages([]);
return;
}
// Don't reload if we already have the initial messages for the initial chat
if (activeChatId === initialActiveChatId && messages === initialMessages) {
return;
}
getChatMessages(activeChatId).then(setMessages);
}, [activeChatId]);
function handleNewChat() {
const id = generateId();
setActiveChatId(id);
setMessages([]);
}
function handleSelectChat(id: string) {
setActiveChatId(id);
}
async function handleDeleteChat(id: string) {
await deleteChatAction(id);
const list = await getChatList();
setChatList(list);
if (activeChatId === id) {
if (list.length > 0) {
setActiveChatId(list[0]!.id);
} else {
setActiveChatId(null);
}
}
}
const handleFirstMessage = useCallback(async (chatId: string, text: string) => {
const title = text.slice(0, 40).trim() || "New chat";
await updateChatTitle(chatId, title);
const list = await getChatList();
setChatList(list);
}, []);
const handleMessagesChange = useCallback(async (_chatId: string, _messages: UIMessage[]) => {
// Messages are persisted server-side via onTurnComplete.
// Refresh the chat list to update timestamps.
const list = await getChatList();
setChatList(list);
}, []);
return (
<main className="flex h-screen">
<ChatSidebar
chats={chatList}
activeChatId={activeChatId}
onSelectChat={handleSelectChat}
onNewChat={handleNewChat}
onDeleteChat={handleDeleteChat}
/>
<div className="flex-1">
{activeChatId ? (
<Chat
key={activeChatId}
chatId={activeChatId}
initialMessages={messages}
transport={transport}
onFirstMessage={handleFirstMessage}
onMessagesChange={handleMessagesChange}
/>
) : (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<p className="text-sm text-gray-400">No conversation selected</p>
<button
type="button"
onClick={handleNewChat}
className="mt-3 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Start a new chat
</button>
</div>
</div>
)}
</div>
</main>
);
}
@@ -0,0 +1,82 @@
"use client";
type ChatMeta = {
id: string;
title: string;
createdAt: number;
updatedAt: number;
};
function timeAgo(ts: number): string {
const seconds = Math.floor((Date.now() - ts) / 1000);
if (seconds < 60) return "just now";
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
type ChatSidebarProps = {
chats: ChatMeta[];
activeChatId: string | null;
onSelectChat: (id: string) => void;
onNewChat: () => void;
onDeleteChat: (id: string) => void;
};
export function ChatSidebar({
chats,
activeChatId,
onSelectChat,
onNewChat,
onDeleteChat,
}: ChatSidebarProps) {
const sorted = [...chats].sort((a, b) => b.updatedAt - a.updatedAt);
return (
<div className="flex h-full w-64 shrink-0 flex-col border-r border-gray-200 bg-gray-50">
<div className="p-3">
<button
type="button"
onClick={onNewChat}
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100"
>
+ New Chat
</button>
</div>
<div className="flex-1 overflow-y-auto">
{sorted.length === 0 && (
<p className="px-3 py-8 text-center text-xs text-gray-400">No conversations yet</p>
)}
{sorted.map((chat) => (
<button
key={chat.id}
type="button"
onClick={() => onSelectChat(chat.id)}
className={`group flex w-full items-start gap-2 px-3 py-2.5 text-left text-sm hover:bg-gray-100 ${
activeChatId === chat.id ? "bg-white" : ""
}`}
>
<div className="min-w-0 flex-1">
<div className="truncate font-medium text-gray-800">{chat.title}</div>
<div className="text-[10px] text-gray-400">{timeAgo(chat.updatedAt)}</div>
</div>
<span
onClick={(e) => {
e.stopPropagation();
onDeleteChat(chat.id);
}}
className="mt-0.5 hidden shrink-0 rounded p-0.5 text-xs text-gray-400 hover:bg-red-100 hover:text-red-600 group-hover:inline-block"
>
&times;
</span>
</button>
))}
</div>
</div>
);
}
+50 -27
View File
@@ -1,12 +1,11 @@
"use client";
import type { UIMessage } from "ai";
import { useChat } from "@ai-sdk/react";
import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
import type { TriggerChatTransport } from "@trigger.dev/sdk/chat";
import { useEffect, useRef, useState } from "react";
import { Streamdown } from "streamdown";
import { getChatToken } from "@/app/actions";
import { MODEL_OPTIONS, DEFAULT_MODEL } from "@/trigger/chat";
import type { aiChat } from "@/trigger/chat";
import { MODEL_OPTIONS, DEFAULT_MODEL } from "@/lib/models";
function ToolInvocation({ part }: { part: any }) {
const [expanded, setExpanded] = useState(false);
@@ -71,41 +70,73 @@ function ToolInvocation({ part }: { part: any }) {
);
}
export function Chat() {
type ChatProps = {
chatId: string;
initialMessages: UIMessage[];
transport: TriggerChatTransport;
onFirstMessage?: (chatId: string, text: string) => void;
onMessagesChange?: (chatId: string, messages: UIMessage[]) => void;
};
export function Chat({
chatId,
initialMessages,
transport,
onFirstMessage,
onMessagesChange,
}: ChatProps) {
const [input, setInput] = useState("");
const [model, setModel] = useState(DEFAULT_MODEL);
// Track which model was used for each assistant message (keyed by the preceding user message ID)
const modelByUserMsgId = useRef<Map<string, string>>(new Map());
const transport = useTriggerChatTransport<typeof aiChat>({
task: "ai-chat",
accessToken: getChatToken,
baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL,
});
const hasCalledFirstMessage = useRef(false);
const { messages, sendMessage, stop, status, error } = useChat({
id: chatId,
messages: initialMessages,
transport,
});
// Notify parent of first user message (for chat metadata creation)
useEffect(() => {
if (hasCalledFirstMessage.current) return;
const firstUser = messages.find((m) => m.role === "user");
if (firstUser) {
hasCalledFirstMessage.current = true;
const text = firstUser.parts
.filter((p: any) => p.type === "text")
.map((p: any) => p.text)
.join(" ");
onFirstMessage?.(chatId, text);
}
}, [messages, chatId, onFirstMessage]);
// Pending message to send after the current turn completes
const [pendingMessage, setPendingMessage] = useState<{ text: string; model: string } | null>(null);
// Auto-send the pending message when the turn completes
// Handle turn completion: persist messages and auto-send pending message
const prevStatus = useRef(status);
useEffect(() => {
if (prevStatus.current === "streaming" && status === "ready" && pendingMessage) {
const turnCompleted = prevStatus.current === "streaming" && status === "ready";
prevStatus.current = status;
if (!turnCompleted) return;
// Persist messages when a turn completes — this ensures the final assistant
// message content is saved (not the empty placeholder from mid-stream).
if (messages.length > 0) {
onMessagesChange?.(chatId, messages);
}
// Auto-send the pending message
if (pendingMessage) {
const { text, model: pendingMsgModel } = pendingMessage;
setPendingMessage(null);
pendingModel.current = pendingMsgModel;
sendMessage({ text }, { metadata: { model: pendingMsgModel } });
}
prevStatus.current = status;
}, [status, sendMessage, pendingMessage]);
}, [status, messages, chatId, onMessagesChange, sendMessage, pendingMessage]);
// Build a map of assistant message index -> model used
// Each assistant message follows a user message, so we track by position
function getModelForAssistantAt(index: number): string | undefined {
// Walk backwards to find the preceding user message
for (let i = index - 1; i >= 0; i--) {
if (messages[i]?.role === "user") {
return modelByUserMsgId.current.get(messages[i].id);
@@ -114,16 +145,13 @@ export function Chat() {
return undefined;
}
// When sending, record which model is selected for this user message
const originalSendMessage = sendMessage;
function trackedSendMessage(msg: Parameters<typeof sendMessage>[0], opts?: Parameters<typeof sendMessage>[1]) {
// We'll track it after the message appears — use a ref to store the pending model
pendingModel.current = model;
originalSendMessage(msg, opts);
}
const pendingModel = useRef<string>(model);
// Track model for new user messages as they appear
const trackedUserIds = useRef<Set<string>>(new Set());
for (const msg of messages) {
if (msg.role === "user" && !trackedUserIds.current.has(msg.id)) {
@@ -146,7 +174,6 @@ export function Chat() {
className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
>
<div className={`max-w-[80%] ${message.role === "user" ? "" : "w-full"}`}>
{/* Model badge for assistant messages */}
{message.role === "assistant" && (
<div className="mb-1 flex items-center gap-2 text-[10px] text-gray-400">
<span className="rounded bg-gray-200 px-1.5 py-0.5 font-medium text-gray-500">
@@ -199,7 +226,6 @@ export function Chat() {
</div>
)}
{/* Queued message indicator */}
{pendingMessage && (
<div className="flex justify-end">
<div className="max-w-[80%]">
@@ -214,20 +240,17 @@ export function Chat() {
)}
</div>
{/* Error */}
{error && (
<div className="shrink-0 border-t border-red-100 bg-red-50 px-4 py-2 text-sm text-red-600">
{error.message}
</div>
)}
{/* Input */}
<form
onSubmit={(e) => {
e.preventDefault();
if (!input.trim()) return;
if (status === "streaming") {
// Buffer the message — it will be sent when the current turn completes
setPendingMessage({ text: input, model });
} else {
trackedSendMessage({ text: input }, { metadata: { model } });
+8
View File
@@ -0,0 +1,8 @@
export const MODEL_OPTIONS = [
"gpt-4o-mini",
"gpt-4o",
"claude-sonnet-4-6",
"claude-opus-4-6",
];
export const DEFAULT_MODEL = "gpt-4o-mini";
+15
View File
@@ -0,0 +1,15 @@
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../../lib/generated/prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
function createClient() {
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
return new PrismaClient({ adapter });
}
export const prisma = globalForPrisma.prisma ?? createClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
+20 -3
View File
@@ -5,6 +5,13 @@ import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
import os from "node:os";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../../lib/generated/prisma/client";
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
const prisma = new PrismaClient({ adapter });
import { DEFAULT_MODEL } from "@/lib/models";
const MODELS: Record<string, () => LanguageModel> = {
"gpt-4o-mini": () => openai("gpt-4o-mini"),
@@ -13,9 +20,6 @@ const MODELS: Record<string, () => LanguageModel> = {
"claude-opus-4-6": () => anthropic("claude-opus-4-6"),
};
export const MODEL_OPTIONS = Object.keys(MODELS);
export const DEFAULT_MODEL = "gpt-4o-mini";
function getModel(modelId?: string): LanguageModel {
const factory = MODELS[modelId ?? DEFAULT_MODEL];
if (!factory) return MODELS[DEFAULT_MODEL]!();
@@ -83,6 +87,19 @@ declare const Deno: unknown;
export const aiChat = chat.task({
id: "ai-chat",
warmTimeoutInSeconds: 10,
onChatStart: async ({ chatId }) => {
await prisma.chat.upsert({
where: { id: chatId },
create: { id: chatId, title: "New chat" },
update: {},
});
},
onTurnComplete: async ({ chatId, uiMessages }) => {
await prisma.chat.update({
where: { id: chatId },
data: { messages: uiMessages as any },
});
},
run: async ({ messages, clientData, stopSignal }) => {
const { model: modelId } = z
.object({ model: z.string().optional() })
+8
View File
@@ -1,7 +1,15 @@
import { defineConfig } from "@trigger.dev/sdk";
import { prismaExtension } from "@trigger.dev/build/extensions/prisma";
export default defineConfig({
project: process.env.TRIGGER_PROJECT_REF!,
dirs: ["./src/trigger"],
maxDuration: 300,
build: {
extensions: [
prismaExtension({
mode: "modern",
}),
],
},
});