diff --git a/apps/app/package.json b/apps/app/package.json index fc14589..b4a71b1 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -18,11 +18,6 @@ "check-types": "next typegen && tsc --noEmit" }, "dependencies": { - "@pilot/core": "workspace:*", - "@pilot/db": "workspace:*", - "@pilot/instagram": "workspace:*", - "@pilot/types": "workspace:*", - "@pilot/ui": "workspace:*", "@ai-sdk/google": "^2.0.54", "@ai-sdk/react": "^2.0.139", "@hookform/resolvers": "^5.2.2", @@ -30,6 +25,11 @@ "@neondatabase/serverless": "^1.0.2", "@opentelemetry/winston-transport": "^0.16.2", "@origin-space/image-cropper": "^0.1.9", + "@pilot/core": "workspace:*", + "@pilot/db": "workspace:*", + "@pilot/instagram": "workspace:*", + "@pilot/types": "workspace:*", + "@pilot/ui": "workspace:*", "@polar-sh/better-auth": "^1.8.1", "@polar-sh/nextjs": "^0.4.11", "@polar-sh/sdk": "^0.34.17", @@ -50,7 +50,7 @@ "dotenv": "^17.3.1", "drizzle-orm": "^0.44.7", "import-in-the-middle": "^2.0.6", - "inngest": "^3.52.2", + "inngest": "^4.4.0", "lucide-react": "^0.542.0", "motion": "^12.34.3", "next": "16.1.6", diff --git a/apps/app/src/actions/sidekick/onboarding.ts b/apps/app/src/actions/sidekick/onboarding.ts index 176f0c7..6234c46 100644 --- a/apps/app/src/actions/sidekick/onboarding.ts +++ b/apps/app/src/actions/sidekick/onboarding.ts @@ -14,6 +14,10 @@ import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { and, eq } from "drizzle-orm"; import { enqueueBusinessKnowledgeSync } from "@/lib/supermemory/events"; +import { + getSidekickSetupStatusByUserId, + SIDEKICK_SETUP_STEPS, +} from "@pilot/core/sidekick/personalization"; export type SidekickOnboardingData = { offerLinks?: { @@ -39,7 +43,7 @@ export type SidekickOnboardingData = { }; export async function updateSidekickOnboardingData( - data: SidekickOnboardingData + data: SidekickOnboardingData, ) { const session = await auth.api.getSession({ headers: await headers(), @@ -69,8 +73,8 @@ export async function updateSidekickOnboardingData( and( eq(userOfferLink.userId, session.user.id), eq(userOfferLink.type, link.type), - eq(userOfferLink.url, link.url) - ) + eq(userOfferLink.url, link.url), + ), ); if (existingLinks.length === 0) { @@ -93,8 +97,8 @@ export async function updateSidekickOnboardingData( and( eq(userOffer.userId, session.user.id), eq(userOffer.name, offer.name), - eq(userOffer.content, offer.content) - ) + eq(userOffer.content, offer.content), + ), ); if (existingOffers.length === 0) { @@ -117,8 +121,8 @@ export async function updateSidekickOnboardingData( .where( and( eq(userFaq.userId, session.user.id), - eq(userFaq.question, faq.question) - ) + eq(userFaq.question, faq.question), + ), ); if (existingFaqs.length === 0) { @@ -195,7 +199,7 @@ export async function deleteOffer(offerId: string) { await db .delete(userOffer) .where( - and(eq(userOffer.id, offerId), eq(userOffer.userId, session.user.id)) + and(eq(userOffer.id, offerId), eq(userOffer.userId, session.user.id)), ); await enqueueBusinessKnowledgeSync(session.user.id, "deleteOffer"); @@ -228,8 +232,8 @@ export async function saveSidekickOfferLink(linkData: { and( eq(userOfferLink.url, linkData.url), eq(userOfferLink.userId, session.user.id), - eq(userOfferLink.type, linkData.type) - ) + eq(userOfferLink.type, linkData.type), + ), ); if (existingLinks.length === 0) { @@ -322,8 +326,8 @@ export async function saveSidekickOffer(offerData: { and( eq(userOffer.userId, session.user.id), eq(userOffer.name, offerData.name), - eq(userOffer.content, offerData.content) - ) + eq(userOffer.content, offerData.content), + ), ); if (existingOffers.length === 0) { @@ -336,10 +340,7 @@ export async function saveSidekickOffer(offerData: { }); } - await enqueueBusinessKnowledgeSync( - session.user.id, - "saveSidekickOffer", - ); + await enqueueBusinessKnowledgeSync(session.user.id, "saveSidekickOffer"); return { success: true }; } catch (error) { @@ -582,22 +583,40 @@ export async function checkSidekickOnboardingStatus() { try { const db = await getRLSDb(); - const userData = await db - .select({ - sidekick_onboarding_complete: user.sidekick_onboarding_complete, - }) - .from(user) - .where(eq(user.id, session.user.id)) - .then((res) => res[0]); + const result = await getSidekickSetupStatusByUserId(db, session.user.id); + + if (result.success) { + return result.data; + } return { - sidekick_onboarding_complete: - userData?.sidekick_onboarding_complete || false, + sidekick_onboarding_complete: false, + isReady: false, + resumeStep: 0, + resumeHref: "/sidekick-onboarding?step=0", + completedSteps: 0, + totalSteps: SIDEKICK_SETUP_STEPS.length, + missing: ["Sidekick setup data"], + steps: SIDEKICK_SETUP_STEPS.map((step) => ({ + ...step, + complete: false, + })), + error: result.error, }; } catch (error) { console.error("Error checking onboarding status:", error); return { sidekick_onboarding_complete: false, + isReady: false, + resumeStep: 0, + resumeHref: "/sidekick-onboarding?step=0", + completedSteps: 0, + totalSteps: SIDEKICK_SETUP_STEPS.length, + missing: ["Sidekick setup data"], + steps: SIDEKICK_SETUP_STEPS.map((step) => ({ + ...step, + complete: false, + })), error: "Failed to check onboarding status", }; } diff --git a/apps/app/src/app/(dashboard)/(workspace)/automations/page.tsx b/apps/app/src/app/(dashboard)/(workspace)/automations/page.tsx index 5cc89aa..3e75c17 100644 --- a/apps/app/src/app/(dashboard)/(workspace)/automations/page.tsx +++ b/apps/app/src/app/(dashboard)/(workspace)/automations/page.tsx @@ -1,6 +1,6 @@ import { Suspense } from "react"; import { Button } from "@pilot/ui/components/button"; -import { Plus } from "lucide-react"; +import { LockKeyhole, Plus } from "lucide-react"; import Link from "next/link"; import AutomationsList from "@/components/automations/list"; import AutomationsLogs from "@/components/automations/logs"; @@ -8,19 +8,43 @@ import { Skeleton } from "@pilot/ui/components/skeleton"; import { SidekickLayout } from "@/components/sidekick/layout"; import { getUser } from "@/lib/auth-utils"; import { getBillingStatus } from "@/lib/billing/enforce"; +import { getInstagramIntegration } from "@/actions/instagram"; + +function InstagramLockedAutomations() { + return ( +
+
+ ); +} export default async function AutomationsPage() { - const user = await getUser(); - const billingStatus = user ? await getBillingStatus(user.id) : null; + const [user, instagram] = await Promise.all([ + getUser(), + getInstagramIntegration(), + ]); + const billingStatus = + user && instagram.connected ? await getBillingStatus(user.id) : null; const isFrozen = billingStatus?.flags.isStructurallyFrozen ?? false; - const canCreateAutomation = billingStatus?.flags.canCreateAutomation ?? false; + const canCreateAutomation = + instagram.connected && (billingStatus?.flags.canCreateAutomation ?? false); return (

Automations

-

+

Build automated replies for common DM and comment questions.

@@ -40,42 +64,48 @@ export default async function AutomationsPage() {
{isFrozen && ( -

- Your workspace is frozen because it is above the current plan cap. Existing automations remain visible, but changes are disabled until usage is reduced or the plan is upgraded. +

+ Your workspace is frozen because it is above the current plan cap. + Existing automations remain visible, but changes are disabled until + usage is reduced or the plan is upgraded.

)} - - -
- - -
-
- - - + {instagram.connected ? ( + + +
+ + +
+
+ + + +
-
- } - > - - + } + > + + - - - - - - } - > - - - + + + + + + } + > + + + + ) : ( + + )} ); } diff --git a/apps/app/src/app/(dashboard)/(workspace)/contacts/page.tsx b/apps/app/src/app/(dashboard)/(workspace)/contacts/page.tsx index cfdf5a9..9c801f1 100644 --- a/apps/app/src/app/(dashboard)/(workspace)/contacts/page.tsx +++ b/apps/app/src/app/(dashboard)/(workspace)/contacts/page.tsx @@ -1,23 +1,50 @@ +import { getInstagramIntegration } from "@/actions/instagram"; import { fetchContacts } from "@/actions/contacts"; import ContactsTable from "@/components/contacts/contacts-table"; +import { LockKeyhole } from "lucide-react"; export const dynamic = "force-dynamic"; +function InstagramLockedContacts() { + return ( +
+
+ ); +} + export default async function ContactsPage() { + const instagram = await getInstagramIntegration(); + let contacts = null; let hasError = false; - try { - contacts = await fetchContacts(); - } catch (error) { - console.error("Error in ContactsPage:", error); - hasError = true; + + if (instagram.connected) { + try { + contacts = await fetchContacts(); + } catch (error) { + console.error("Error in ContactsPage:", error); + hasError = true; + } } if (hasError) { return (
-

Contacts

+

+ Contacts +

Failed to load contacts. Please try again later.

@@ -29,13 +56,19 @@ export default async function ContactsPage() { return (
-

Contacts

-

+

+ Contacts +

+

Keep your leads, notes, tags, and follow-ups in one view.

- + {instagram.connected ? ( + + ) : ( + + )}
); } diff --git a/apps/app/src/app/(dashboard)/(workspace)/page.tsx b/apps/app/src/app/(dashboard)/(workspace)/page.tsx index 938b5c0..980f265 100644 --- a/apps/app/src/app/(dashboard)/(workspace)/page.tsx +++ b/apps/app/src/app/(dashboard)/(workspace)/page.tsx @@ -1,29 +1,82 @@ -import { redirect } from "next/navigation"; +import { getInstagramIntegration } from "@/actions/instagram"; import { checkSidekickOnboardingStatus } from "@/actions/sidekick/onboarding"; import { getSidekickMemoryOverview } from "@/actions/sidekick/memory"; import { SidekickPanel } from "@/components/sidekick/sidekick-panel"; import { FollowUpList } from "@/components/sidekick/follow-up-list"; import { HRNList } from "@/components/sidekick/hrn-list"; import { SidekickLayout } from "@/components/sidekick/layout"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@pilot/ui/components/alert"; +import { CheckCircle2, LockKeyhole } from "lucide-react"; export const dynamic = "force-dynamic"; -export default async function SidekickPage() { - const { sidekick_onboarding_complete } = - await checkSidekickOnboardingStatus(); +function SidekickReadyBanner() { + return ( + + + ); +} - if (!sidekick_onboarding_complete) { - redirect("/sidekick-onboarding"); - } +function InstagramLockedSidekick() { + return ( +
+
+ ); +} + +function SidekickUnavailable() { + return ( +
+
+ ); +} + +export default async function SidekickPage() { + const [onboardingStatus, instagram] = await Promise.all([ + checkSidekickOnboardingStatus(), + getInstagramIntegration(), + ]); let overview = null; let hasError = false; let overviewResult; - try { - overviewResult = await getSidekickMemoryOverview(); - } catch (error) { - console.error("Error in SidekickPage:", error); - hasError = true; + if (onboardingStatus.isReady && instagram.connected) { + try { + overviewResult = await getSidekickMemoryOverview(); + } catch (error) { + console.error("Error in SidekickPage:", error); + hasError = true; + } } if (overviewResult && overviewResult.success && overviewResult.overview) { overview = overviewResult.overview; @@ -43,21 +96,34 @@ export default async function SidekickPage() { return (
-

Sidekick

-

- Your AI assistant for Instagram DMs. It helps you reply faster, follow up on time, and keep conversations moving. +

+ Sidekick +

+

+ Your AI assistant for Instagram DMs. It helps you reply faster, follow + up on time, and keep conversations moving.

- -
- -
-
- - -
-
+ {onboardingStatus.isReady && instagram.connected ? ( + + ) : null} + + {!instagram.connected ? ( + + ) : !onboardingStatus.isReady ? ( + + ) : ( + +
+ +
+
+ + +
+
+ )}
); } diff --git a/apps/app/src/app/(dashboard)/layout.tsx b/apps/app/src/app/(dashboard)/layout.tsx index 597ed8d..c7c0a08 100644 --- a/apps/app/src/app/(dashboard)/layout.tsx +++ b/apps/app/src/app/(dashboard)/layout.tsx @@ -5,11 +5,60 @@ import { redirect, unstable_rethrow } from "next/navigation"; import { db } from "@pilot/db"; import { user } from "@pilot/db/schema"; import { eq } from "drizzle-orm"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@pilot/ui/components/alert"; +import { Badge } from "@pilot/ui/components/badge"; +import { Button } from "@pilot/ui/components/button"; import { SidebarInset, SidebarProvider } from "@pilot/ui/components/sidebar"; import { AppSidebar } from "@/components/dashboard/sidebar"; import PageHeader from "@/components/dashboard/page-header"; import { SidekickToggle } from "@/components/sidekick/toggle"; import { SidekickProvider } from "@/components/sidekick/context"; +import { checkSidekickOnboardingStatus } from "@/actions/sidekick/onboarding"; +import { TriangleAlert } from "lucide-react"; +import Link from "next/link"; + +function SidekickSetupBanner({ + status, +}: { + status: Awaited>; +}) { + if (status.isReady) { + return null; + } + + const statusMessage = + status.missing.length > 0 + ? `Missing: ${status.missing.join(", ")}.` + : "Finish the final setup step to turn Sidekick on."; + + return ( + + + ); +} export default async function DashboardLayout({ children, @@ -45,6 +94,18 @@ export default async function DashboardLayout({ redirect("/onboarding"); } + let sidekickStatus: Awaited< + ReturnType + > | null = null; + let sidekickReady = false; + try { + sidekickStatus = await checkSidekickOnboardingStatus(); + sidekickReady = sidekickStatus.isReady; + } catch (error) { + unstable_rethrow(error); + console.error("Failed to fetch Sidekick setup status:", error); + } + return ( + {sidekickStatus ? ( + + ) : null}
{children}
- + {sidekickReady ? : null}
); diff --git a/apps/app/src/app/(onboarding)/onboarding/page.tsx b/apps/app/src/app/(onboarding)/onboarding/page.tsx index ef2104d..f1f0861 100644 --- a/apps/app/src/app/(onboarding)/onboarding/page.tsx +++ b/apps/app/src/app/(onboarding)/onboarding/page.tsx @@ -260,7 +260,8 @@ async function checkInstagramConnectionAction( setInstagramConnection(result); setStepValidationState((previousState) => ({ ...previousState, - 0: result.connected, + 0: true, + 1: true, })); if (result.connected) { @@ -365,8 +366,8 @@ async function submitStep2Action( return; } - toast.success("Quick setup complete. Next up: Sidekick."); - router.push("/sidekick-onboarding"); + toast.success("Quick setup complete."); + router.push("/"); } catch (error) { console.error("Error submitting step 2:", error); toast.error("Hmm, something's not right. Give it another shot?"); @@ -394,8 +395,8 @@ function OnboardingPageContent() { const [stepValidationState, setStepValidationState] = useState< Record >({ - 0: false, - 1: false, + 0: true, + 1: true, 2: false, 3: false, 4: false, @@ -573,6 +574,15 @@ function OnboardingPageContent() { window.location.href = "/api/auth/instagram?returnTo=/onboarding"; }; + const handleSkipInstagram = () => { + setStepValidationState((previousState) => ({ + ...previousState, + 0: true, + 1: true, + })); + setActiveStep(2); + }; + const handleRefreshPreview = async () => { setInstagramPreview(null); setPreviewError(null); @@ -662,12 +672,12 @@ function OnboardingPageContent() {
-

+

Turn your next DM into revenue in 2 minutes.

-

- Connect Instagram first so Pilot can show you what the - product feels like before asking for setup details. +

+ Connect Instagram now for a live preview, or skip it and + finish your basic setup first.

@@ -698,8 +708,18 @@ function OnboardingPageContent() { Connect Instagram -

- We'll only read DMs to train your AI. + + +

+ You can connect Instagram later from Settings.

@@ -711,10 +731,10 @@ function OnboardingPageContent() {
- + Setting things up... - + We're pulling recent Instagram context and showing you how Sidekick would jump into a real thread. @@ -727,6 +747,31 @@ function OnboardingPageContent() {
+ {!instagramConnection.connected ? ( +
+

+ Instagram is optional +

+

+ Skip the live preview for now and finish your basic setup. + You can connect Instagram later from Settings. +

+
+ + +
+
+ ) : null} + {isPreparingPreview ? (
@@ -1360,7 +1405,7 @@ function OnboardingPageContent() { diff --git a/apps/app/src/app/(onboarding)/sidekick-onboarding/page.tsx b/apps/app/src/app/(onboarding)/sidekick-onboarding/page.tsx index 2be73ee..7a66a14 100644 --- a/apps/app/src/app/(onboarding)/sidekick-onboarding/page.tsx +++ b/apps/app/src/app/(onboarding)/sidekick-onboarding/page.tsx @@ -458,7 +458,7 @@ export default function SidekickOnboardingPage() { return; } - if (status.sidekick_onboarding_complete) { + if (status.isReady) { router.replace("/"); if (isMounted) { setIsInitializing(false); @@ -565,6 +565,17 @@ export default function SidekickOnboardingPage() { 2: initialFaqs.length > 0, 3: !!toneProfileResult?.success && !!toneProfileResult.data?.toneType, }); + + const stepParam = new URLSearchParams(window.location.search).get("step"); + const requestedStep = stepParam !== null ? Number(stepParam) : null; + const stepFromUrl = + requestedStep !== null && + Number.isInteger(requestedStep) && + requestedStep >= 0 && + requestedStep <= 3 + ? requestedStep + : null; + setActiveStep(stepFromUrl ?? status.resumeStep ?? 0); setIsInitializing(false); } @@ -642,6 +653,10 @@ export default function SidekickOnboardingPage() { setActiveStep((prev) => prev + 1); }; + const handleSkip = () => { + router.push("/"); + }; + const isStepValid = (step: number) => { if (step === 1) { return offers.length > 0 && hasStoredMainOffering; @@ -662,6 +677,21 @@ export default function SidekickOnboardingPage() {
+
+
+

+ Set up Sidekick +

+

+ Add the business details Sidekick needs before it can reply for + you. +

+
+ +
+ { diff --git a/apps/app/src/app/api/chat/route.ts b/apps/app/src/app/api/chat/route.ts index 56bdd23..728727c 100644 --- a/apps/app/src/app/api/chat/route.ts +++ b/apps/app/src/app/api/chat/route.ts @@ -21,6 +21,7 @@ import { z } from "zod"; import { DEFAULT_SIDEKICK_PROMPT, getBusinessKnowledgeSnapshotByUserId, + getSidekickSetupStatusByUserId, } from "@pilot/core/sidekick/personalization"; import { getUserProfile, @@ -86,6 +87,31 @@ export async function POST(req: Request) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } + const setupStatusResult = await getSidekickSetupStatusByUserId(db, user.id); + + if (!setupStatusResult.success) { + console.error( + "Failed to check Sidekick setup status:", + setupStatusResult.error, + ); + return Response.json( + { error: "Unable to verify Sidekick setup status. Please try again." }, + { status: 503 }, + ); + } + + const setupStatus = setupStatusResult.data; + + if (!setupStatus?.isReady) { + return Response.json( + { + error: "Complete Sidekick setup before using chat.", + resumeHref: setupStatus?.resumeHref || "/sidekick-onboarding?step=0", + }, + { status: 403 }, + ); + } + try { await assertBillingAllowed(user.id, "sidekick:chat"); } catch (error) { @@ -128,25 +154,29 @@ export async function POST(req: Request) { db, user.id, ).catch(() => null); - const [knowledgeProfile, workspaceProfile, knowledgeResults, workspaceResults] = - await Promise.all([ - getMemoryProfile({ - containerTag: getKnowledgeContainerTag(user.id), - q: latestUserText, - }).catch(() => null), - getMemoryProfile({ - containerTag: getWorkspaceContainerTag(user.id), - q: latestUserText, - }).catch(() => null), - searchMemory({ - containerTag: getKnowledgeContainerTag(user.id), - q: latestUserText, - }).catch(() => []), - searchMemory({ - containerTag: getWorkspaceContainerTag(user.id), - q: latestUserText, - }).catch(() => []), - ]); + const [ + knowledgeProfile, + workspaceProfile, + knowledgeResults, + workspaceResults, + ] = await Promise.all([ + getMemoryProfile({ + containerTag: getKnowledgeContainerTag(user.id), + q: latestUserText, + }).catch(() => null), + getMemoryProfile({ + containerTag: getWorkspaceContainerTag(user.id), + q: latestUserText, + }).catch(() => null), + searchMemory({ + containerTag: getKnowledgeContainerTag(user.id), + q: latestUserText, + }).catch(() => []), + searchMemory({ + containerTag: getWorkspaceContainerTag(user.id), + q: latestUserText, + }).catch(() => []), + ]); const knowledgeMemoryContext = formatMemoryContext({ title: "Business knowledge memory", @@ -506,7 +536,9 @@ export async function POST(req: Request) { await saveChatSession({ sessionId: id, messages }); console.log(`Saved ${messages.length} messages to session ${id}`); - const lastUserMessage = [...messages].reverse().find((item) => item.role === "user"); + const lastUserMessage = [...messages] + .reverse() + .find((item) => item.role === "user"); const lastAssistantMessage = [...messages] .reverse() .find((item) => item.role === "assistant"); diff --git a/apps/app/src/components/instagram-connection-banner.tsx b/apps/app/src/components/instagram-connection-banner.tsx index 551f771..01e63bf 100644 --- a/apps/app/src/components/instagram-connection-banner.tsx +++ b/apps/app/src/components/instagram-connection-banner.tsx @@ -2,10 +2,14 @@ import { useEffect, useState } from "react"; import { usePathname } from "next/navigation"; -import { Alert, AlertDescription, AlertTitle } from "@pilot/ui/components/alert"; -import { cn } from "@pilot/ui/lib/utils"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@pilot/ui/components/alert"; +import { Button } from "@pilot/ui/components/button"; import Link from "next/link"; -import { ArrowRight } from "lucide-react"; +import { ArrowRight, Instagram } from "lucide-react"; type InstagramStatus = { connected: boolean; @@ -14,7 +18,7 @@ type InstagramStatus = { async function loadInstagramStatus( setStatus: (s: InstagramStatus) => void, - signal: AbortSignal + signal: AbortSignal, ) { try { const res = await fetch("/api/auth/instagram/status", { @@ -57,24 +61,31 @@ export default function InstagramConnectionBanner() { return ( - - Instagram not connected - - - Connect your Instagram account in Settings to use automations and contact sync. - +
+
{!isOnSettings && ( - - Go to Settings - + + Settings + + )}
); diff --git a/apps/app/src/lib/auth.ts b/apps/app/src/lib/auth.ts index 711bda2..6b58c01 100644 --- a/apps/app/src/lib/auth.ts +++ b/apps/app/src/lib/auth.ts @@ -62,11 +62,10 @@ export const auth = betterAuth({ checkout({ products: checkoutProducts, authenticatedUsersOnly: true, - successUrl: "/sidekick-onboarding", + successUrl: "/", }), portal(), ], }), ], }); - diff --git a/apps/app/src/lib/constants/sidekick-onboarding.ts b/apps/app/src/lib/constants/sidekick-onboarding.ts index 226d8f2..2ca3b93 100644 --- a/apps/app/src/lib/constants/sidekick-onboarding.ts +++ b/apps/app/src/lib/constants/sidekick-onboarding.ts @@ -1,20 +1,5 @@ -export const sidekickSteps = [ - { - id: 0, - name: "Your Links", - }, - { - id: 1, - name: "What You Sell", - }, - { - id: 2, - name: "Common Questions", - }, - { - id: 3, - name: "Your Voice", - }, -]; +import { SIDEKICK_SETUP_STEPS } from "@pilot/core/sidekick/personalization"; + +export const sidekickSteps = [...SIDEKICK_SETUP_STEPS]; export const tone_options = ["Friendly", "Direct", "Like Me", "Custom"]; diff --git a/apps/app/src/lib/inngest/client.ts b/apps/app/src/lib/inngest/client.ts index 423d205..f0454a7 100644 --- a/apps/app/src/lib/inngest/client.ts +++ b/apps/app/src/lib/inngest/client.ts @@ -1,4 +1,3 @@ import { Inngest } from "inngest"; -import { realtimeMiddleware } from "@inngest/realtime"; -export const inngest = new Inngest({ id: "pilot", middleware: [realtimeMiddleware()] }); \ No newline at end of file +export const inngest = new Inngest({ id: "pilot" }); diff --git a/apps/app/src/lib/inngest/functions.ts b/apps/app/src/lib/inngest/functions.ts index f418b42..10c26b3 100644 --- a/apps/app/src/lib/inngest/functions.ts +++ b/apps/app/src/lib/inngest/functions.ts @@ -23,8 +23,8 @@ export const syncInstagramContacts = inngest.createFunction( { id: "sync-instagram-contacts", name: "Sync Instagram Contacts", + triggers: [{ event: "contacts/sync" }], }, - { event: "contacts/sync" }, async ({ event, step }) => { const { userId, fullSync = false } = event.data as { userId?: string; @@ -148,8 +148,11 @@ export const syncInstagramContacts = inngest.createFunction( ); export const scheduleContactsSync = inngest.createFunction( - { id: "schedule-contacts-sync", name: "Schedule Contacts Sync" }, - { cron: "0 * * * *" }, + { + id: "schedule-contacts-sync", + name: "Schedule Contacts Sync", + triggers: [{ cron: "0 * * * *" }], + }, async ({ step }) => { const integrations = await step.run("load-integrations", async () => { return db.query.instagramIntegration.findMany({}); @@ -184,8 +187,8 @@ export const syncBusinessKnowledge = inngest.createFunction( id: "sync-business-knowledge-memory", name: "Sync Business Knowledge Memory", retries: 1, + triggers: [{ event: "memory/knowledge.sync" }], }, - { event: "memory/knowledge.sync" }, async ({ event, step }) => { const { userId } = event.data as { userId?: string }; @@ -204,8 +207,8 @@ export const backfillActiveContactMemory = inngest.createFunction( id: "backfill-active-contact-memory", name: "Backfill Active Contact Memory", retries: 0, + triggers: [{ event: "memory/contact.backfill" }], }, - { event: "memory/contact.backfill" }, async ({ event, step }) => { const { userId } = event.data as { userId?: string }; @@ -328,8 +331,11 @@ export const backfillActiveContactMemory = inngest.createFunction( ); export const refreshInstagramTokens = inngest.createFunction( - { id: "refresh-instagram-tokens", name: "Refresh Instagram Tokens" }, - { cron: "0 3 * * *" }, + { + id: "refresh-instagram-tokens", + name: "Refresh Instagram Tokens", + triggers: [{ cron: "0 3 * * *" }], + }, async ({ step }) => { const sevenDaysFromNow = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); diff --git a/apps/app/src/lib/inngest/retry-failed-send.ts b/apps/app/src/lib/inngest/retry-failed-send.ts index b37c18c..a46e808 100644 --- a/apps/app/src/lib/inngest/retry-failed-send.ts +++ b/apps/app/src/lib/inngest/retry-failed-send.ts @@ -20,8 +20,8 @@ export const retryFailedInstagramSend = inngest.createFunction( id: "retry-failed-instagram-send", name: "Retry Failed Instagram Send", retries: 0, + triggers: [{ event: "instagram/send-failed" }], }, - { event: "instagram/send-failed" }, async ({ event, step }) => { const { igUserId, diff --git a/packages/core/src/sidekick/personalization.ts b/packages/core/src/sidekick/personalization.ts index 3c739d0..3e09ea1 100644 --- a/packages/core/src/sidekick/personalization.ts +++ b/packages/core/src/sidekick/personalization.ts @@ -9,6 +9,50 @@ import type { BusinessKnowledgeSnapshot } from "../memory/supermemory"; import type { Offer, UserPersonalizationData } from "@pilot/types/user"; import { eq } from "drizzle-orm"; +export const SIDEKICK_SETUP_STEPS = [ + { + id: 0, + name: "Your Links", + }, + { + id: 1, + name: "What You Sell", + }, + { + id: 2, + name: "Common Questions", + }, + { + id: 3, + name: "Your Voice", + }, +] as const; + +export type SidekickSetupStatus = { + sidekick_onboarding_complete: boolean; + isReady: boolean; + resumeStep: number; + resumeHref: string; + completedSteps: number; + totalSteps: number; + missing: string[]; + steps: Array<{ + id: number; + name: string; + complete: boolean; + }>; +}; + +export type SidekickSetupStatusResult = + | { + success: true; + data: SidekickSetupStatus; + } + | { + success: false; + error: string; + }; + export const DEFAULT_SIDEKICK_PROMPT = "You are Sidekick, Pilot's sales assistant. Reply as the business owner. Rely on retrieved memory and current context. Never invent business facts. Write like a real human: professional, natural, clear, direct, and conversational. Sound like you are explaining something to a smart friend over coffee. Use simple words and short sentences. Stay on point. Do not use buzzwords, corporate jargon, press-release language, fluff, or em dashes. Do not use the 'no x, no y, but z' pattern."; @@ -30,6 +74,123 @@ const PROMPTS = { }, } as const; +export async function getSidekickSetupStatusByUserId( + dbClient: any, + userId: string, +): Promise { + try { + const [userData, links, offers, toneProfiles, faqs] = await Promise.all([ + dbClient + .select({ + main_offering: user.main_offering, + sidekick_onboarding_complete: user.sidekick_onboarding_complete, + }) + .from(user) + .where(eq(user.id, userId)) + .then((rows: Array>) => rows[0]), + dbClient + .select() + .from(userOfferLink) + .where(eq(userOfferLink.userId, userId)), + dbClient.select().from(userOffer).where(eq(userOffer.userId, userId)), + dbClient + .select() + .from(userToneProfile) + .where(eq(userToneProfile.userId, userId)) + .limit(1), + dbClient.select().from(userFaq).where(eq(userFaq.userId, userId)), + ]); + + if (!userData) { + return { success: false, error: "User not found" } as const; + } + + const hasPrimaryOfferLink = links.some( + (link: { type?: string; url?: string | null }) => + link.type === "primary" && !!link.url?.trim(), + ); + const hasOffer = offers.some( + (offer: { name?: string | null; content?: string | null }) => + !!offer.name?.trim() && !!offer.content?.trim(), + ); + const hasMainOffering = + typeof userData.main_offering === "string" && + userData.main_offering.trim().length > 0; + const hasFaq = faqs.some( + (faq: { question?: string | null }) => !!faq.question?.trim(), + ); + const hasToneProfile = !!toneProfiles[0]?.toneType; + + const steps = SIDEKICK_SETUP_STEPS.map((step) => { + const complete = + step.id === 0 + ? hasPrimaryOfferLink + : step.id === 1 + ? hasOffer && hasMainOffering + : step.id === 2 + ? hasFaq + : hasToneProfile; + + return { + ...step, + complete, + }; + }); + const missing = [ + !hasPrimaryOfferLink ? "main offer page" : null, + !hasOffer ? "at least one offer" : null, + !hasMainOffering ? "main offering" : null, + !hasFaq ? "at least one common question" : null, + !hasToneProfile ? "tone profile" : null, + ].filter((item): item is string => Boolean(item)); + const firstIncompleteStep = steps.find((step) => !step.complete); + const hasRequiredData = missing.length === 0; + const resumeStep = firstIncompleteStep?.id ?? 0; + const persistedSidekickOnboardingComplete = Boolean( + userData.sidekick_onboarding_complete, + ); + + if (hasRequiredData && !persistedSidekickOnboardingComplete) { + try { + await dbClient + .update(user) + .set({ sidekick_onboarding_complete: true }) + .where(eq(user.id, userId)); + } catch (error) { + console.error( + "Failed to persist Sidekick onboarding completion:", + error, + ); + } + } + + const sidekick_onboarding_complete = + persistedSidekickOnboardingComplete || hasRequiredData; + + return { + success: true, + data: { + sidekick_onboarding_complete, + isReady: hasRequiredData, + resumeStep, + resumeHref: hasRequiredData + ? "/" + : `/sidekick-onboarding?step=${resumeStep}`, + completedSteps: steps.filter((step) => step.complete).length, + totalSteps: steps.length, + missing, + steps, + }, + } as const; + } catch (error) { + console.error("Error checking sidekick setup status:", error); + return { + success: false, + error: "Failed to check sidekick setup status", + } as const; + } +} + export async function getPersonalizedSidekickDataByUserId( dbClient: any, userId: string, @@ -240,7 +401,8 @@ function buildToneGuidanceFromProfile( const samples = Array.isArray(toneProfileRecord?.sampleText) ? toneProfileRecord.sampleText.filter(Boolean).slice(0, 3) : []; - const sampleSuffix = samples.length > 0 ? ` Examples: ${samples.join(" | ")}` : ""; + const sampleSuffix = + samples.length > 0 ? ` Examples: ${samples.join(" | ")}` : ""; switch (toneType) { case "direct": @@ -266,13 +428,20 @@ export async function getBusinessKnowledgeSnapshotByUserId( return { mainOffering: result.data.user?.main_offering || null, - faqs: result.data.faqs.map((faq: { id: string; question: string; answer?: string | null }) => ({ - id: faq.id, - question: faq.question, - answer: faq.answer ?? null, - })), + faqs: result.data.faqs.map( + (faq: { id: string; question: string; answer?: string | null }) => ({ + id: faq.id, + question: faq.question, + answer: faq.answer ?? null, + }), + ), offers: result.data.offers.map( - (offer: { id: string; name: string; content: string; value?: number | null }) => ({ + (offer: { + id: string; + name: string; + content: string; + value?: number | null; + }) => ({ id: offer.id, name: offer.name, content: offer.content, diff --git a/packages/core/src/workflows/instagram-webhook.ts b/packages/core/src/workflows/instagram-webhook.ts index 26e3410..cd7e5f5 100644 --- a/packages/core/src/workflows/instagram-webhook.ts +++ b/packages/core/src/workflows/instagram-webhook.ts @@ -1,4 +1,9 @@ -import { automation, contact, instagramIntegration, sidekickActionLog } from "@pilot/db/schema"; +import { + automation, + contact, + instagramIntegration, + sidekickActionLog, +} from "@pilot/db/schema"; import { and, desc, eq, gt, or } from "drizzle-orm"; import { postPublicCommentReply, @@ -12,6 +17,7 @@ import { generateAutomationResponse } from "../automation/response"; import { classifyHumanResponseNeeded } from "../ai/hrn"; import { appendContactTranscriptMemory } from "../memory/supermemory"; import { generateReply } from "../sidekick/reply"; +import { getSidekickSetupStatusByUserId } from "../sidekick/personalization"; type GenericTemplateButton = { type: string; @@ -103,14 +109,17 @@ async function upsertContactState(params: { }); } -function isValidTemplateElement(value: unknown): value is GenericTemplateElement { +function isValidTemplateElement( + value: unknown, +): value is GenericTemplateElement { if (!value || typeof value !== "object") { return false; } const record = value as Record; const titleOrText = record.title ?? record.text; - const hasTitleOrText = typeof titleOrText === "string" && titleOrText.trim().length > 0; + const hasTitleOrText = + typeof titleOrText === "string" && titleOrText.trim().length > 0; if (!hasTitleOrText) { return false; } @@ -124,8 +133,12 @@ function normalizeTemplateElements(rawElements: GenericTemplateElement[]) { typeof rawElement.title === "string" && rawElement.title ? rawElement.title : (rawElement.text as string), - subtitle: typeof rawElement.subtitle === "string" ? rawElement.subtitle : undefined, - image_url: typeof rawElement.image_url === "string" ? rawElement.image_url : undefined, + subtitle: + typeof rawElement.subtitle === "string" ? rawElement.subtitle : undefined, + image_url: + typeof rawElement.image_url === "string" + ? rawElement.image_url + : undefined, default_action: rawElement.default_action && rawElement.default_action.type === "web_url" && @@ -135,7 +148,9 @@ function normalizeTemplateElements(rawElements: GenericTemplateElement[]) { buttons: Array.isArray(rawElement.buttons) ? rawElement.buttons .filter( - (button): button is { type: "web_url"; url: string; title: string } => + ( + button, + ): button is { type: "web_url"; url: string; title: string } => !!button && typeof button === "object" && button.type === "web_url" && @@ -173,7 +188,8 @@ async function processCommentChanges(params: { : typeof value.comment_id === "string" ? value.comment_id : undefined; - const commenterId = typeof value.from?.id === "string" ? value.from.id : undefined; + const commenterId = + typeof value.from?.id === "string" ? value.from.id : undefined; const messageText = typeof value.text === "string" ? value.text : ""; const mediaId = typeof value.media?.id === "string" @@ -186,12 +202,13 @@ async function processCommentChanges(params: { continue; } - const integration = await params.dbClient.query.instagramIntegration.findFirst({ - where: or( - eq(instagramIntegration.instagramUserId, params.igUserId), - eq(instagramIntegration.appScopedUserId, params.igUserId), - ), - }); + const integration = + await params.dbClient.query.instagramIntegration.findFirst({ + where: or( + eq(instagramIntegration.instagramUserId, params.igUserId), + eq(instagramIntegration.appScopedUserId, params.igUserId), + ), + }); if (!integration) { continue; } @@ -221,7 +238,8 @@ async function processCommentChanges(params: { prompt: matchedAutomation.responseContent, userMessage: messageText, }); - replyText = aiResponse?.text || "Thanks for your comment! We'll follow up in DMs."; + replyText = + aiResponse?.text || "Thanks for your comment! We'll follow up in DMs."; } let sendResponse: @@ -231,7 +249,9 @@ async function processCommentChanges(params: { if (matchedAutomation.responseType === "generic_template") { try { const parsed = JSON.parse(matchedAutomation.responseContent) as unknown; - const elements = Array.isArray(parsed) ? parsed.filter(isValidTemplateElement) : []; + const elements = Array.isArray(parsed) + ? parsed.filter(isValidTemplateElement) + : []; if (elements.length > 0) { const normalized = normalizeTemplateElements(elements); sendResponse = await sendInstagramCommentGenericTemplate({ @@ -270,7 +290,9 @@ async function processCommentChanges(params: { messageId, }); - const commentReplyText = (matchedAutomation as { commentReplyText?: string | null }).commentReplyText; + const commentReplyText = ( + matchedAutomation as { commentReplyText?: string | null } + ).commentReplyText; if (commentReplyText && commentReplyText.trim()) { try { await postPublicCommentReply({ @@ -294,12 +316,13 @@ async function processDirectMessage(params: { webhookMid?: string | null; resolveBillingStatus: (userId: string) => Promise; }) { - const integration = await params.dbClient.query.instagramIntegration.findFirst({ - where: or( - eq(instagramIntegration.instagramUserId, params.igUserId), - eq(instagramIntegration.appScopedUserId, params.igUserId), - ), - }); + const integration = + await params.dbClient.query.instagramIntegration.findFirst({ + where: or( + eq(instagramIntegration.instagramUserId, params.igUserId), + eq(instagramIntegration.appScopedUserId, params.igUserId), + ), + }); if (!integration) { return { status: "ok" } as const; @@ -367,7 +390,10 @@ async function processDirectMessage(params: { } const existingContact = await params.dbClient.query.contact.findFirst({ - where: and(eq(contact.userId, integration.userId), eq(contact.id, params.senderId)), + where: and( + eq(contact.userId, integration.userId), + eq(contact.id, params.senderId), + ), }); if (!existingContact && !billingStatus.flags.canCreateContact) { @@ -390,7 +416,9 @@ async function processDirectMessage(params: { return { status: "ok", hrn: true } as const; } - const hrnDecision = await classifyHumanResponseNeeded({ message: params.messageText }); + const hrnDecision = await classifyHumanResponseNeeded({ + message: params.messageText, + }); if (hrnDecision.hrn) { await upsertContactState({ dbClient: params.dbClient, @@ -440,6 +468,31 @@ async function processDirectMessage(params: { } if (!replyText) { + const setupStatusResult = await getSidekickSetupStatusByUserId( + params.dbClient, + integration.userId, + ); + + if (!setupStatusResult.success) { + console.error( + "Failed to check Sidekick setup status", + setupStatusResult.error, + ); + } else if (!setupStatusResult.data.isReady) { + await upsertContactState({ + dbClient: params.dbClient, + contactId: params.senderId, + userId: integration.userId, + messageText: params.messageText, + stage: existingContact?.stage ?? "new", + sentiment: existingContact?.sentiment ?? "neutral", + leadScore: existingContact?.leadScore ?? 50, + requiresHRN: existingContact?.requiresHumanResponse ?? false, + humanResponseSetAt: existingContact?.humanResponseSetAt ?? null, + }); + return { status: "ok", sidekickBlocked: true } as const; + } + const reply = await generateReply({ dbClient: params.dbClient, userId: integration.userId, @@ -480,8 +533,10 @@ async function processDirectMessage(params: { const delivered = sendResponse.status >= 200 && sendResponse.status < 300; const messageId = - (sendResponse.data as { id?: string; message_id?: string } | undefined)?.id || - (sendResponse.data as { id?: string; message_id?: string } | undefined)?.message_id; + (sendResponse.data as { id?: string; message_id?: string } | undefined) + ?.id || + (sendResponse.data as { id?: string; message_id?: string } | undefined) + ?.message_id; const now = new Date(); await params.dbClient @@ -623,7 +678,13 @@ export async function processInstagramWebhook(params: { const messageText = message?.message?.text || ""; const isEcho = Boolean(message?.message?.is_echo); - if (!entry?.id || !senderId || !messageText || isEcho || senderId === entry.id) { + if ( + !entry?.id || + !senderId || + !messageText || + isEcho || + senderId === entry.id + ) { return { status: "ok" } as const; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 925377d..6528905 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,7 +75,7 @@ importers: version: 0.34.17 '@sentry/nextjs': specifier: ^10.39.0 - version: 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.101.2) + version: 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.101.2(esbuild@0.25.12)) '@t3-oss/env-nextjs': specifier: ^0.13.10 version: 0.13.10(typescript@5.9.3)(zod@4.3.6) @@ -93,10 +93,10 @@ importers: version: 8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) '@vercel/analytics': specifier: ^1.6.1 - version: 1.6.1(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 1.6.1(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) '@vercel/speed-insights': specifier: ^1.3.1 - version: 1.3.1(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 1.3.1(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) ai: specifier: ^5.0.137 version: 5.0.137(zod@4.3.6) @@ -125,8 +125,8 @@ importers: specifier: ^2.0.6 version: 2.0.6 inngest: - specifier: ^3.52.2 - version: 3.52.2(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(zod@4.3.6) + specifier: ^4.4.0 + version: 4.4.0(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(zod@4.3.6) lucide-react: specifier: ^0.542.0 version: 0.542.0(react@19.2.4) @@ -226,10 +226,10 @@ importers: version: link:../../packages/ui '@vercel/analytics': specifier: ^1.5.0 - version: 1.6.1(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 1.6.1(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) '@vercel/speed-insights': specifier: ^1.2.0 - version: 1.3.1(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 1.3.1(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -275,7 +275,7 @@ importers: version: 9.39.3(jiti@2.6.1) eslint-config-next: specifier: 16.1.6 - version: 16.1.6(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + version: 16.1.6(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5303,6 +5303,46 @@ packages: typescript: optional: true + inngest@4.4.0: + resolution: {integrity: sha512-OdNedhkjTK1EPij6k4nbQ70uWjxxjES1xJJPNFPp7XQJMyr0PrKg0SOoCL+FNsXp1Zrb7ZZG06sru9WHgFdE/g==} + engines: {node: '>=20'} + peerDependencies: + '@sveltejs/kit': '>=1.27.3' + '@vercel/node': '>=2.15.9' + aws-lambda: '>=1.0.7' + express: '>=4.19.2' + fastify: '>=4.21.0' + h3: '>=1.8.1' + hono: '>=4.2.7' + koa: '>=2.14.2' + next: '>=12.0.0' + react: '>=18.0.0' + typescript: '>=5.8.0' + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + '@vercel/node': + optional: true + aws-lambda: + optional: true + express: + optional: true + fastify: + optional: true + h3: + optional: true + hono: + optional: true + koa: + optional: true + next: + optional: true + react: + optional: true + typescript: + optional: true + input-otp@1.4.2: resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} peerDependencies: @@ -9868,7 +9908,7 @@ snapshots: '@sentry/core@10.39.0': {} - '@sentry/nextjs@10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.101.2)': + '@sentry/nextjs@10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.101.2(esbuild@0.25.12))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.39.0 @@ -9880,7 +9920,7 @@ snapshots: '@sentry/opentelemetry': 10.39.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0) '@sentry/react': 10.39.0(react@19.2.4) '@sentry/vercel-edge': 10.39.0 - '@sentry/webpack-plugin': 4.9.1(webpack@5.101.2) + '@sentry/webpack-plugin': 4.9.1(webpack@5.101.2(esbuild@0.25.12)) next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) rollup: 4.58.0 stacktrace-parser: 0.1.11 @@ -9971,12 +10011,12 @@ snapshots: '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) '@sentry/core': 10.39.0 - '@sentry/webpack-plugin@4.9.1(webpack@5.101.2)': + '@sentry/webpack-plugin@4.9.1(webpack@5.101.2(esbuild@0.25.12))': dependencies: '@sentry/bundler-plugin-core': 4.9.1 unplugin: 1.0.1 uuid: 9.0.1 - webpack: 5.101.2 + webpack: 5.101.2(esbuild@0.25.12) transitivePeerDependencies: - encoding - supports-color @@ -10172,11 +10212,11 @@ snapshots: '@types/bunyan@1.8.11': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@types/connect@3.4.38': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@types/d3-array@3.2.2': {} @@ -10337,13 +10377,13 @@ snapshots: '@types/memcached@2.2.10': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@types/ms@2.1.0': {} '@types/mysql@2.15.27': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@types/node@20.19.35': dependencies: @@ -10371,7 +10411,7 @@ snapshots: '@types/oracledb@6.5.2': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@types/pg-pool@2.0.7': dependencies: @@ -10379,7 +10419,7 @@ snapshots: '@types/pg@8.15.6': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 pg-protocol: 1.11.0 pg-types: 2.2.0 @@ -10403,7 +10443,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@types/triple-beam@1.3.5': {} @@ -10649,14 +10689,14 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vercel/analytics@1.6.1(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + '@vercel/analytics@1.6.1(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': optionalDependencies: next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 '@vercel/oidc@3.1.0': {} - '@vercel/speed-insights@1.3.1(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + '@vercel/speed-insights@1.3.1(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': optionalDependencies: next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 @@ -11762,8 +11802,28 @@ snapshots: '@next/eslint-plugin-next': 16.1.6 eslint: 9.39.3(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.3(jiti@2.6.1)) + globals: 16.4.0 + typescript-eslint: 8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-config-next@16.1.6(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 16.1.6 + eslint: 9.39.3(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.3(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.3(jiti@2.6.1)) @@ -11785,7 +11845,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -11796,22 +11856,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.3(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11822,7 +11882,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.3(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11840,6 +11900,33 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.3(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.3(jiti@2.6.1)): dependencies: aria-query: 5.3.2 @@ -12464,7 +12551,7 @@ snapshots: - encoding - supports-color - inngest@3.52.2(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(zod@4.3.6): + inngest@4.4.0(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(zod@4.3.6): dependencies: '@bufbuild/protobuf': 2.11.0 '@inngest/ai': 0.1.7 @@ -12481,19 +12568,18 @@ snapshots: '@types/debug': 4.1.12 '@types/ms': 2.1.0 canonicalize: 1.0.8 - chalk: 4.1.2 cross-fetch: 4.1.0 debug: 4.4.3 hash.js: 1.1.7 json-stringify-safe: 5.0.1 ms: 2.1.3 serialize-error-cjs: 0.1.4 - strip-ansi: 5.2.0 temporal-polyfill: 0.2.5 ulid: 2.4.0 zod: 4.3.6 optionalDependencies: next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 typescript: 5.9.3 transitivePeerDependencies: - '@opentelemetry/core' @@ -13679,7 +13765,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 24.12.2 + '@types/node': 25.6.0 long: 5.3.2 protobufjs@8.0.0: @@ -13694,7 +13780,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 24.12.2 + '@types/node': 25.6.0 long: 5.3.2 proxy-from-env@1.1.0: {} @@ -14426,13 +14512,15 @@ snapshots: temporal-spec@0.2.4: {} - terser-webpack-plugin@5.4.0(webpack@5.101.2): + terser-webpack-plugin@5.4.0(esbuild@0.25.12)(webpack@5.101.2(esbuild@0.25.12)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.1 - webpack: 5.101.2 + webpack: 5.101.2(esbuild@0.25.12) + optionalDependencies: + esbuild: 0.25.12 terser@5.46.1: dependencies: @@ -14784,7 +14872,7 @@ snapshots: webpack-virtual-modules@0.5.0: {} - webpack@5.101.2: + webpack@5.101.2(esbuild@0.25.12): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -14808,7 +14896,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.2 - terser-webpack-plugin: 5.4.0(webpack@5.101.2) + terser-webpack-plugin: 5.4.0(esbuild@0.25.12)(webpack@5.101.2(esbuild@0.25.12)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: