diff --git a/package.json b/package.json index 24a059d..18ea1f5 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,10 @@ "vitest": "^3.1.4" }, "dependencies": { + "@supabase/supabase-js": "^2.58.0", "dotenv": "^16.5.0", "jsonwebtoken": "^9.0.2", + "openai": "^5.23.1", "pino": "^9.7.0", "pino-pretty": "^13.0.0", "sqlite": "^5.1.1", diff --git a/src/lib/iMessages.ts b/src/lib/iMessages.ts index c9a1ee7..2699c0c 100644 --- a/src/lib/iMessages.ts +++ b/src/lib/iMessages.ts @@ -1,5 +1,5 @@ import { settings } from '$lib/config' -import type { MessageRow } from '$lib/types' +import type { Message, MessageEmbeddingInput, MessageRow } from '$lib/types' import os from 'node:os' import path from 'node:path' import { open } from 'sqlite' @@ -22,7 +22,8 @@ const buildQuery = (startDate: string, endDate: string) => { datetime(message.date / 1000000000 + strftime('%s', '2001-01-01'), 'unixepoch') AS timestamp, message.text AS text, handle.id AS contact_id, - message.is_from_me + message.is_from_me, + message.guid AS guid FROM message JOIN handle ON message.handle_id = handle.ROWID WHERE message.text IS NOT NULL @@ -50,10 +51,27 @@ const formatMessages = (rows: MessageRow[]) => { .reverse() // Reverse to get chronological order } -export const queryMessagesDb = async (startDate: string, endDate: string) => { +const mapEmbeddableMessages = (rows: MessageRow[]): MessageEmbeddingInput[] => { + return rows.map((row) => ({ + message_id: row.guid, + thread_id: row.contact_id ?? settings.CONTACT_PHONE ?? 'unknown', + ts: new Date(row.timestamp.endsWith('Z') ? row.timestamp : `${row.timestamp}Z`).toISOString(), + sender: row.is_from_me + ? 'me' + : row.contact_id === settings.CONTACT_PHONE + ? 'them' + : row.contact_id || 'unknown', + text: row.text || '', + })) +} + +export const queryMessagesDb = async ( + startDate: string, + endDate: string +): Promise<{ messages: Message[]; embeddableMessages: MessageEmbeddingInput[] }> => { if (!settings.CONTACT_PHONE) { logger.warn('CONTACT_PHONE setting not configured') - return { messages: [] } + return { messages: [], embeddableMessages: [] } } const db = await open({ filename: CHAT_DB_PATH, driver: sqlite3.Database }) @@ -64,12 +82,16 @@ export const queryMessagesDb = async (startDate: string, endDate: string) => { logger.info({ count: rows.length, handleId: settings.CONTACT_PHONE }, 'Fetched messages') const formattedMessages = formatMessages(rows) + const embeddableMessages = mapEmbeddableMessages(rows) // Return empty array if there are no messages from the contact - return { messages: hasContactMessages(formattedMessages) ? formattedMessages : [] } + return { + messages: hasContactMessages(formattedMessages) ? formattedMessages : [], + embeddableMessages, + } } catch (error) { logger.error({ error }, 'Error querying messages database') - return { messages: [] } + return { messages: [], embeddableMessages: [] } } finally { await db.close() } diff --git a/src/lib/server/getSimilarMessages.ts b/src/lib/server/getSimilarMessages.ts new file mode 100644 index 0000000..c155526 --- /dev/null +++ b/src/lib/server/getSimilarMessages.ts @@ -0,0 +1,63 @@ +import { logger } from '$lib/logger' +import type { SimilarMessageSnippet } from '$lib/types' +import { getOpenAIClient } from './openai' +import { supabase } from './supabase' +import type { SupabaseClient } from '@supabase/supabase-js' + +const EMBEDDING_MODEL = 'text-embedding-3-small' + +export const getSimilarMessages = async ( + query: string, + threadId: string, + k = 5 +): Promise => { + const trimmedQuery = query.trim() + if (!trimmedQuery || !threadId) { + return [] + } + + if (!supabase) { + logger.debug('Supabase client unavailable; skipping semantic recall lookup') + return [] + } + + const client = getOpenAIClient() + if (!client) { + logger.debug('OpenAI client unavailable; skipping semantic recall lookup') + return [] + } + + try { + const embeddingResponse = await client.embeddings.create({ + model: EMBEDDING_MODEL, + input: trimmedQuery, + }) + + const queryEmbedding = embeddingResponse.data[0]?.embedding + if (!queryEmbedding) { + logger.warn('Failed to compute query embedding for semantic recall lookup') + return [] + } + + const supabaseClient = supabase as SupabaseClient + const { data, error } = await supabaseClient.rpc('match_message_embeddings', { + query_embedding: queryEmbedding, + thread_filter: threadId, + match_count: k, + }) + + if (error) { + logger.error({ error }, 'Failed to fetch similar messages from Supabase') + return [] + } + + return (data || []).map((row) => ({ + text: row.text as string, + ts: row.ts as string, + sender: row.sender as string, + })) + } catch (error) { + logger.error({ error }, 'Semantic recall lookup failed') + return [] + } +} diff --git a/src/lib/server/indexMessages.ts b/src/lib/server/indexMessages.ts new file mode 100644 index 0000000..82f1cd8 --- /dev/null +++ b/src/lib/server/indexMessages.ts @@ -0,0 +1,111 @@ +import { logger } from '$lib/logger' +import type { MessageEmbeddingInput } from '$lib/types' +import { getOpenAIClient } from './openai' +import { supabase } from './supabase' +import type { SupabaseClient } from '@supabase/supabase-js' + +const EMBEDDING_MODEL = 'text-embedding-3-small' + +export const indexMessages = async (messages: MessageEmbeddingInput[]): Promise => { + if (!messages.length) { + return + } + + if (!supabase) { + logger.debug('Supabase client unavailable; skipping message indexing') + return + } + + const client = getOpenAIClient() + if (!client) { + logger.debug('OpenAI client unavailable; skipping message indexing') + return + } + + const uniqueMessages = dedupeMessages(messages) + if (!uniqueMessages.length) { + return + } + + try { + const supabaseClient = supabase as SupabaseClient + const messagesToEmbed = await filterExisting(supabaseClient, uniqueMessages) + if (!messagesToEmbed.length) { + logger.debug('No new messages to embed') + return + } + + const inputs = messagesToEmbed.map((msg) => msg.text) + const embeddingResponse = await client.embeddings.create({ + model: EMBEDDING_MODEL, + input: inputs, + }) + + if (embeddingResponse.data.length !== messagesToEmbed.length) { + logger.warn( + { + expected: messagesToEmbed.length, + received: embeddingResponse.data.length, + }, + 'Mismatch between embeddings and messages' + ) + return + } + + const rows = messagesToEmbed.map((message, index) => ({ + message_id: message.message_id, + thread_id: message.thread_id, + ts: message.ts, + sender: message.sender, + text: message.text, + embedding: embeddingResponse.data[index].embedding, + })) + + const { error } = await supabaseClient.from('message_embeddings').upsert(rows, { + onConflict: 'message_id', + }) + + if (error) { + logger.error({ error }, 'Failed to upsert message embeddings') + } + } catch (error) { + logger.error({ error }, 'Failed to index messages') + } +} + +const dedupeMessages = (messages: MessageEmbeddingInput[]) => { + const seen = new Set() + + return messages.filter((message) => { + if (!message.message_id || !message.thread_id || !message.text.trim()) { + return false + } + + const key = `${message.thread_id}:${message.message_id}` + if (seen.has(key)) { + return false + } + + seen.add(key) + return true + }) +} + +const filterExisting = async ( + client: SupabaseClient, + messages: MessageEmbeddingInput[] +): Promise => { + const ids = messages.map((message) => message.message_id) + + const { data, error } = await client.from('message_embeddings').select('message_id').in('message_id', ids) + + if (error) { + logger.error({ error }, 'Failed to fetch existing message embeddings') + return messages + } + + const existingIds = new Set((data || []).map((row) => row.message_id)) + return messages.filter((message) => !existingIds.has(message.message_id)) +} + +// TODO: Consider moving indexing into a background job for batch backfilling older history. diff --git a/src/lib/server/openai.ts b/src/lib/server/openai.ts new file mode 100644 index 0000000..a2ddeb5 --- /dev/null +++ b/src/lib/server/openai.ts @@ -0,0 +1,19 @@ +import { settings } from '$lib/config' +import { logger } from '$lib/logger' +import OpenAI from 'openai' + +let client: OpenAI | null = null + +export const getOpenAIClient = (): OpenAI | null => { + if (client) { + return client + } + + if (!settings.OPENAI_API_KEY) { + logger.warn('OpenAI API key missing; semantic recall disabled') + return null + } + + client = new OpenAI({ apiKey: settings.OPENAI_API_KEY }) + return client +} diff --git a/src/lib/server/supabase.ts b/src/lib/server/supabase.ts new file mode 100644 index 0000000..6cdc28a --- /dev/null +++ b/src/lib/server/supabase.ts @@ -0,0 +1,21 @@ +import { env } from '$env/dynamic/private' +import { createClient, type SupabaseClient } from '@supabase/supabase-js' +import { logger } from '$lib/logger' + +const supabaseUrl = env.SUPABASE_URL +const supabaseServiceRoleKey = env.SUPABASE_SERVICE_ROLE_KEY + +let client: SupabaseClient | null = null + +if (!supabaseUrl || !supabaseServiceRoleKey) { + logger.warn('Supabase environment variables missing; semantic recall disabled') +} else { + client = createClient(supabaseUrl, supabaseServiceRoleKey, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }) +} + +export const supabase = client diff --git a/src/lib/types.ts b/src/lib/types.ts index f5a4520..ce6d70a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -10,6 +10,7 @@ export interface MessageRow { date: string contact_id?: string timestamp: string + guid: string } export interface PageData { @@ -35,3 +36,17 @@ export interface OpenAIConfig { apiUrl: string apiKey?: string } + +export interface MessageEmbeddingInput { + message_id: string + thread_id: string + ts: string + sender: string + text: string +} + +export interface SimilarMessageSnippet { + text: string + ts: string + sender: string +} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 658e441..5daa2cb 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,5 +1,5 @@ import { getAnthropicReply } from '$lib/anthropic' -import { getAllSettings, updateSetting } from '$lib/config' +import { getAllSettings, settings, updateSetting } from '$lib/config' import { getGrokReply } from '$lib/grok' import { queryMessagesDb } from '$lib/iMessages' import { getKhojReply } from '$lib/khoj' @@ -8,6 +8,8 @@ import { getOpenaiReply } from '$lib/openAi' import { DEFAULT_PROVIDER } from '$lib/provider' import { getAvailableProviders, hasMultipleProviders } from '$lib/providers/registry' import type { Message, ToneType } from '$lib/types' +import { indexMessages } from '$lib/server/indexMessages' +import { getSimilarMessages } from '$lib/server/getSimilarMessages' import { fail } from '@sveltejs/kit' import type { Actions, PageServerLoad } from './$types' @@ -17,16 +19,25 @@ export const load: PageServerLoad = async ({ url }) => { const lookBack = Number.parseInt(url.searchParams.get('lookBackHours') || '1') const end = new Date() const start = new Date(end.getTime() - lookBack * ONE_HOUR) - const { messages } = await queryMessagesDb(start.toISOString(), end.toISOString()) - const settings = await getAllSettings() + const { messages, embeddableMessages = [] } = await queryMessagesDb( + start.toISOString(), + end.toISOString() + ) + const currentSettings = await getAllSettings() const availableProviders = getAvailableProviders() + try { + await indexMessages(embeddableMessages) + } catch (error) { + logger.error({ error }, 'Failed to index messages during load') + } + return { messages, multiProvider: hasMultipleProviders(), defaultProvider: DEFAULT_PROVIDER || '', // Handle null case availableProviders, - settings, + settings: currentSettings, } } @@ -68,7 +79,12 @@ export const actions: Actions = { }) } - const result = await getReplies(messages, tone, context || '') + const augmentedContext = await buildContextWithSimilarSnippets( + messages, + context || '' + ) + + const result = await getReplies(messages, tone, augmentedContext) logger.debug({ resultFromService: result }, 'Result received from AI service in action') // Return the result directly - SvelteKit handles the JSON serialization @@ -112,3 +128,35 @@ export const actions: Actions = { } }, } + +const buildContextWithSimilarSnippets = async ( + messages: Message[], + baseContext: string +): Promise => { + const threadId = settings.CONTACT_PHONE + const lastMessage = messages.at(-1) + + if (!threadId || !lastMessage?.text?.trim()) { + return baseContext + } + + try { + const snippets = await getSimilarMessages(lastMessage.text, threadId, 5) + + if (!snippets.length) { + return baseContext + } + + const snippetLines = snippets.map((snippet) => { + const timestamp = new Date(snippet.ts).toISOString() + return `- ${snippet.sender} (${timestamp}): ${snippet.text}` + }) + + const semanticContext = ['Similar Past Snippets:', ...snippetLines].join('\n') + + return [baseContext, semanticContext].filter(Boolean).join('\n\n') + } catch (error) { + logger.error({ error }, 'Failed to fetch similar messages for context') + return baseContext + } +} diff --git a/supabase/migrations/20250211_add_message_embeddings.sql b/supabase/migrations/20250211_add_message_embeddings.sql new file mode 100644 index 0000000..d26c926 --- /dev/null +++ b/supabase/migrations/20250211_add_message_embeddings.sql @@ -0,0 +1,41 @@ +create extension if not exists vector; + +create table if not exists message_embeddings ( + message_id text primary key, + thread_id text not null, + ts timestamptz not null, + sender text not null, + text text not null, + embedding vector(1536) +); + +create index if not exists message_embeddings_embedding_idx on message_embeddings using ivfflat (embedding vector_cosine_ops) with (lists = 50); + +create or replace function match_message_embeddings( + query_embedding vector(1536), + thread_filter text, + match_count integer default 5 +) +returns table ( + message_id text, + thread_id text, + ts timestamptz, + sender text, + text text, + similarity double precision +) +language sql +stable +as $$ + select + message_id, + thread_id, + ts, + sender, + text, + 1 - (embedding <=> query_embedding) as similarity + from message_embeddings + where thread_id = thread_filter + order by embedding <=> query_embedding + limit match_count; +$$; diff --git a/tests/lib/iMessages.test.ts b/tests/lib/iMessages.test.ts index 6486d13..9329eb8 100644 --- a/tests/lib/iMessages.test.ts +++ b/tests/lib/iMessages.test.ts @@ -48,12 +48,14 @@ describe('queryMessagesDb', () => { text: 'Hello from me', contact_id: '+1234567890', is_from_me: 1, + guid: 'guid-1', }, { timestamp: '2025-05-23T12:00:00.000Z', text: 'Hello from them', contact_id: '+1234567890', is_from_me: 0, + guid: 'guid-2', }, ]), close: vi.fn().mockResolvedValue(undefined), @@ -67,6 +69,7 @@ describe('queryMessagesDb', () => { const result = await queryMessagesDb(startDate, endDate) expect(result.messages.length).toBe(2) + expect(result.embeddableMessages.length).toBe(2) // After .reverse() in the implementation, the first message should be from them // Instead of asserting the exact sender (which depends on the implementation), // we'll just check that the text and timestamp match what we expect @@ -93,12 +96,14 @@ describe('queryMessagesDb', () => { text: 'Hello from me', contact_id: '+1234567890', is_from_me: 1, + guid: 'guid-3', }, { timestamp: '2025-05-23 12:02:00', text: 'Another message from me', contact_id: '+1234567890', is_from_me: 1, + guid: 'guid-4', }, ]), close: vi.fn().mockResolvedValue(undefined), diff --git a/tests/routes/root/server.test.ts b/tests/routes/root/server.test.ts index ff65f9e..ba22de1 100644 --- a/tests/routes/root/server.test.ts +++ b/tests/routes/root/server.test.ts @@ -4,6 +4,8 @@ import * as queryDb from '$lib/iMessages' import * as khoj from '$lib/khoj' import * as openai from '$lib/openAi' import * as registry from '$lib/providers/registry' +import * as indexer from '$lib/server/indexMessages' +import * as semantic from '$lib/server/getSimilarMessages' import type { RequestEvent } from '@sveltejs/kit' import { beforeEach, describe, expect, it, vi } from 'vitest' import * as serverModule from '../../../src/routes/+page.server' @@ -21,6 +23,7 @@ vi.mock('$lib/provider', () => ({ })) vi.mock('$lib/config', () => ({ getAllSettings: vi.fn(), + settings: { CONTACT_PHONE: '+15555555555' }, })) vi.mock('$lib/logger', () => ({ logger: { @@ -30,6 +33,12 @@ vi.mock('$lib/logger', () => ({ warn: vi.fn(), }, })) +vi.mock('$lib/server/indexMessages', () => ({ + indexMessages: vi.fn().mockResolvedValue(undefined), +})) +vi.mock('$lib/server/getSimilarMessages', () => ({ + getSimilarMessages: vi.fn().mockResolvedValue([]), +})) function createMockRequestEvent( url: URL, @@ -76,11 +85,22 @@ describe('root page server', () => { ]) vi.mocked(registry.hasMultipleProviders).mockReturnValue(false) vi.mocked(config.getAllSettings).mockResolvedValue([]) + vi.mocked(indexer.indexMessages).mockResolvedValue(undefined) + vi.mocked(semantic.getSimilarMessages).mockResolvedValue([]) }) it('load should return messages and multiProvider flag', async () => { vi.mocked(queryDb.queryMessagesDb).mockResolvedValue({ messages: [{ text: 'hi', sender: 'contact', timestamp: '2025-01-01T00:00:00Z' }], + embeddableMessages: [ + { + message_id: 'guid-1', + thread_id: '+15555555555', + ts: '2025-01-01T00:00:00Z', + sender: 'contact', + text: 'hi', + }, + ], }) const event = createMockRequestEvent(new URL('https://example.com/')) const data = await serverModule.load( @@ -101,6 +121,15 @@ describe('root page server', () => { ], settings: [], }) + expect(indexer.indexMessages).toHaveBeenCalledWith([ + { + message_id: 'guid-1', + thread_id: '+15555555555', + ts: '2025-01-01T00:00:00Z', + sender: 'contact', + text: 'hi', + }, + ]) }) it('generate action should return suggestions', async () => { @@ -158,6 +187,48 @@ describe('root page server', () => { ) }) + it('appends similar snippets to the context when available', async () => { + const mockResponse = { summary: 'sum', replies: ['r1'] } + vi.mocked(openai.getOpenaiReply).mockResolvedValue(mockResponse) + vi.mocked(semantic.getSimilarMessages).mockResolvedValue([ + { text: 'past chat', ts: '2025-01-02T00:00:00Z', sender: 'them' }, + ]) + + const messagesPayload = [ + { text: 'draft', sender: 'user', timestamp: '2025-01-03T00:00:00Z' }, + ] + + const formData = new FormData() + formData.append('messages', JSON.stringify(messagesPayload)) + formData.append('tone', 'gentle') + formData.append('context', 'base context') + formData.append('provider', 'openai') + + const request = new Request('https://example.com/', { method: 'POST', body: formData }) + const event = { ...createMockRequestEvent(new URL('https://example.com/')), request } + + await serverModule.actions.generate({ + request: event.request, + cookies: event.cookies, + fetch: event.fetch, + getClientAddress: event.getClientAddress, + locals: {}, + params: {}, + platform: undefined, + route: { id: '/' }, + setHeaders: vi.fn(), + url: event.url, + isDataRequest: false, + isSubRequest: false, + } as unknown as Parameters[0]) + + expect(openai.getOpenaiReply).toHaveBeenCalledWith( + messagesPayload, + 'gentle', + 'base context\n\nSimilar Past Snippets:\n- them (2025-01-02T00:00:00.000Z): past chat' + ) + }) + it('uses Khoj provider when specified', async () => { const mockResponse = { summary: 'sum', replies: ['r1'], messageCount: 1 } vi.mocked(khoj.getKhojReply).mockResolvedValue(mockResponse) diff --git a/yarn.lock b/yarn.lock index d1034ab..4c1ed49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -607,6 +607,70 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.42.0.tgz#516c6770ba15fe6aef369d217a9747492c01e8b7" integrity sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA== +"@supabase/auth-js@2.72.0": + version "2.72.0" + resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.72.0.tgz#bee32b9f67b0bc568e1175be235ced70db51df94" + integrity sha512-4+bnUrtTDK1YD0/FCx2YtMiQH5FGu9Jlf4IQi5kcqRwRwqp2ey39V61nHNdH86jm3DIzz0aZKiWfTW8qXk1swQ== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/functions-js@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.5.0.tgz#4f74236a8451fecbe108f977f57a61cd42a13544" + integrity sha512-SXBx6Jvp+MOBekeKFu+G11YLYPeVeGQl23eYyAG9+Ro0pQ1aIP0UZNIBxHKNHqxzR0L0n6gysNr2KT3841NATw== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/node-fetch@2.6.15", "@supabase/node-fetch@^2.6.14": + version "2.6.15" + resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz#731271430e276983191930816303c44159e7226c" + integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ== + dependencies: + whatwg-url "^5.0.0" + +"@supabase/node-fetch@^2.6.13": + version "2.6.13" + resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.13.tgz#0d36219a9e2134049a7317591e1d4fbf73a42ec4" + integrity sha512-rEHQaDVzxLZMCK3p+JW2nzEsK4AJpOQhetppaqAzrFum0Ub8wcnoM/8f1dWRZSulY5fRDP6rJaWT/8X3VleCzg== + dependencies: + whatwg-url "^5.0.0" + +"@supabase/postgrest-js@1.21.4": + version "1.21.4" + resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.21.4.tgz#c9f5fc674c16705fa0562c8f571069a4df1b3015" + integrity sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/realtime-js@2.15.5": + version "2.15.5" + resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.15.5.tgz#bc2174518d913639f685fb03b8327a6aa6fb33cb" + integrity sha512-/Rs5Vqu9jejRD8ZeuaWXebdkH+J7V6VySbCZ/zQM93Ta5y3mAmocjioa/nzlB6qvFmyylUgKVS1KpE212t30OA== + dependencies: + "@supabase/node-fetch" "^2.6.13" + "@types/phoenix" "^1.6.6" + "@types/ws" "^8.18.1" + ws "^8.18.2" + +"@supabase/storage-js@2.12.2": + version "2.12.2" + resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.12.2.tgz#90f09fad86b18298a316bd020bf628422378ea7f" + integrity sha512-SiySHxi3q7gia7NBYpsYRu8gyI0NhFwSORMxbZIxJ/zAVkN6QpwDRan158CJ+UdzD4WB/rQMAGRqIJQP+7ccAQ== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/supabase-js@^2.58.0": + version "2.58.0" + resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.58.0.tgz#5f1c75243e1bc322a53ede388adc9f6712b2c8b2" + integrity sha512-Tm1RmQpoAKdQr4/8wiayGti/no+If7RtveVZjHR8zbO7hhQjmPW2Ok5ZBPf1MGkt5c+9R85AVMsTfSaqAP1sUg== + dependencies: + "@supabase/auth-js" "2.72.0" + "@supabase/functions-js" "2.5.0" + "@supabase/node-fetch" "2.6.15" + "@supabase/postgrest-js" "1.21.4" + "@supabase/realtime-js" "2.15.5" + "@supabase/storage-js" "2.12.2" + "@sveltejs/acorn-typescript@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz#f518101d1b2e12ce80854f1cd850d3b9fb91d710" @@ -732,6 +796,18 @@ dependencies: undici-types "~6.21.0" +"@types/phoenix@^1.6.6": + version "1.6.6" + resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.6.tgz#3c1ab53fd5a23634b8e37ea72ccacbf07fbc7816" + integrity sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A== + +"@types/ws@^8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@^8.34.0": version "8.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz#96c9f818782fe24cd5883a5d517ca1826d3fa9c2" @@ -2482,6 +2558,11 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +openai@^5.23.1: + version "5.23.1" + resolved "https://registry.yarnpkg.com/openai/-/openai-5.23.1.tgz#5e12847cca116a36c03a97d818f182ab3eaa39dc" + integrity sha512-APxMtm5mln4jhKhAr0d5zP9lNsClx4QwJtg8RUvYSSyxYCTHLNJnLEcSHbJ6t0ori8Pbr9HZGfcPJ7LEy73rvQ== + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -3299,6 +3380,11 @@ tr46@^5.1.0: dependencies: punycode "^2.3.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + ts-api-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" @@ -3430,6 +3516,11 @@ walk-up-path@^4.0.0: resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-4.0.0.tgz#590666dcf8146e2d72318164f1f2ac6ef51d4198" integrity sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" @@ -3455,6 +3546,14 @@ whatwg-url@^14.0.0, whatwg-url@^14.1.1: tr46 "^5.1.0" webidl-conversions "^7.0.0" +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -3510,6 +3609,11 @@ ws@^8.18.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== +ws@^8.18.2: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== + xml-name-validator@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"