From 3d56ba51464a8516d9211492365240a36cec5f88 Mon Sep 17 00:00:00 2001
From: Tim Raderschad <tim.raderschad@gmail.com>
Date: Sun, 25 Aug 2024 21:13:52 +0200
Subject: [PATCH] feat(web): add auto flag removal (#161)

---
 apps/web/package.json                         |  10 +-
 apps/web/prisma/schema.prisma                 |  21 +-
 apps/web/src/api/helpers.ts                   |  33 +
 apps/web/src/api/index.ts                     |   4 +-
 apps/web/src/api/routes/integrations.ts       |  94 +++
 .../src/components/AddFeatureFlagModal.tsx    |   4 +-
 apps/web/src/components/FlagPage.tsx          |  42 +-
 .../src/components/settings/Integrations.tsx  | 174 +++++
 apps/web/src/components/ui/select.tsx         |  49 +-
 apps/web/src/env/schema.mjs                   |   4 +
 .../pages/projects/[projectId]/settings.tsx   |  42 +-
 apps/web/src/server/common/auth.ts            |  22 +
 apps/web/src/server/common/github-app.ts      |  50 ++
 apps/web/src/server/common/integrations.ts    |  12 +
 .../server/services/AiFlagRemovalService.ts   |  32 +
 .../src/server/services/IntegrationService.ts |   3 +
 apps/web/src/server/trpc/router/flags.ts      | 235 ++++++-
 apps/web/src/server/trpc/router/project.ts    | 101 +++
 apps/web/tsconfig.json                        |   2 +-
 pnpm-lock.yaml                                | 593 +++++++++---------
 20 files changed, 1173 insertions(+), 354 deletions(-)
 create mode 100644 apps/web/src/api/helpers.ts
 create mode 100644 apps/web/src/api/routes/integrations.ts
 create mode 100644 apps/web/src/components/settings/Integrations.tsx
 create mode 100644 apps/web/src/server/common/auth.ts
 create mode 100644 apps/web/src/server/common/github-app.ts
 create mode 100644 apps/web/src/server/common/integrations.ts
 create mode 100644 apps/web/src/server/services/AiFlagRemovalService.ts
 create mode 100644 apps/web/src/server/services/IntegrationService.ts

diff --git a/apps/web/package.json b/apps/web/package.json
index 803331c2..9c962484 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -34,7 +34,7 @@
     "@monaco-editor/react": "^4.5.1",
     "@next-auth/prisma-adapter": "1.0.5",
     "@next/mdx": "14.0.4",
-    "@prisma/client": "5.6.0",
+    "@prisma/client": "5.18.0",
     "@radix-ui/react-avatar": "^1.0.3",
     "@radix-ui/react-dialog": "^1.0.5",
     "@radix-ui/react-dropdown-menu": "^2.0.6",
@@ -91,6 +91,7 @@
     "lodash-es": "^4.17.21",
     "logsnag": "^0.1.6",
     "lucide-react": "0.320.0",
+    "memoize": "^10.0.0",
     "micro": "^10.0.1",
     "ms": "^2.1.3",
     "next": "14.1.1",
@@ -102,7 +103,9 @@
     "nextjs-cors": "^2.1.2",
     "nodemailer": "^6.9.1",
     "nuqs": "^1.17.8",
-    "octokit": "^2.0.18",
+    "octokit": "^4.0.2",
+    "openai": "^4.56.0",
+    "prettier": "^2.8.7",
     "rate-limiter-flexible": "^5.0.3",
     "react": "18.2.0",
     "react-dom": "18.2.0",
@@ -138,9 +141,8 @@
     "autoprefixer": "^10.4.14",
     "jsdom": "^20.0.3",
     "postcss": "^8.4.21",
-    "prettier": "^2.8.7",
     "prettier-plugin-tailwindcss": "^0.1.13",
-    "prisma": "5.6.0",
+    "prisma": "5.18.0",
     "tailwindcss": "^3.3.1",
     "ts-node": "^10.9.1",
     "tsconfig-paths": "^4.2.0",
diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma
index ab3bb4d0..95b4c2a6 100644
--- a/apps/web/prisma/schema.prisma
+++ b/apps/web/prisma/schema.prisma
@@ -107,7 +107,8 @@ model Project {
   stripePriceId        String?
   currentPeriodEnd     DateTime @default(dbgenerated("(CURRENT_TIMESTAMP(3) + INTERVAL 30 DAY)"))
 
-  apiRequests ApiRequest[]
+  apiRequests  ApiRequest[]
+  integrations Integration[]
 }
 
 model ProjectUser {
@@ -310,3 +311,21 @@ model ApiRequest {
   @@index([createdAt])
   @@index([type])
 }
+
+enum IntegrationType {
+  GITHUB
+}
+
+model Integration {
+  id        String   @id @default(cuid())
+  createdAt DateTime @default(now())
+  updatedAt DateTime @updatedAt
+
+  type      IntegrationType
+  settings  Json
+  project   Project         @relation(fields: [projectId], references: [id], onDelete: Cascade)
+  projectId String
+
+  @@unique([projectId, type])
+  @@index([projectId])
+}
diff --git a/apps/web/src/api/helpers.ts b/apps/web/src/api/helpers.ts
new file mode 100644
index 00000000..9437a4e0
--- /dev/null
+++ b/apps/web/src/api/helpers.ts
@@ -0,0 +1,33 @@
+import type { Context, MiddlewareHandler } from "hono";
+import { getCookie } from "hono/cookie";
+import type { GetServerSidePropsContext } from "next";
+import type { DefaultSession } from "next-auth";
+import { getServerAuthSession } from "server/common/get-server-auth-session";
+import type { UserSession } from "types/next-auth";
+
+export async function getHonoSession(c: Context) {
+  return await getServerAuthSession({
+    req: {
+      ...c.req.raw.clone(),
+      cookies: getCookie(c),
+    } as unknown as GetServerSidePropsContext["req"],
+    res: {
+      ...c.res,
+      getHeader: (h: string) => c.req.header(h),
+      setHeader: (h: string, v: string) => c.header(h, v),
+    } as unknown as GetServerSidePropsContext["res"],
+  });
+}
+
+export const authMiddleware: MiddlewareHandler<{
+  Variables: {
+    user: UserSession & DefaultSession["user"];
+  };
+}> = async (c, next) => {
+  const session = await getHonoSession(c);
+  if (!session || !session.user) {
+    return c.json({ error: "Unauthorized" }, { status: 401 });
+  }
+  c.set("user", session.user);
+  return next();
+};
diff --git a/apps/web/src/api/index.ts b/apps/web/src/api/index.ts
index ac36ac09..5dec4ab5 100644
--- a/apps/web/src/api/index.ts
+++ b/apps/web/src/api/index.ts
@@ -4,6 +4,7 @@ import { Hono } from "hono";
 import { cors } from "hono/cors";
 import { logger } from "hono/logger";
 import { makeHealthRoute } from "./routes/health";
+import { makeIntegrationsRoute } from "./routes/integrations";
 import { makeLegacyProjectDataRoute } from "./routes/legacy_project_data";
 import { makeEventRoute } from "./routes/v1_event";
 
@@ -19,4 +20,5 @@ export const app = new Hono()
   // v1 routes
   .route("/v1/config", makeConfigRoute())
   .route("/v1/data", makeProjectDataRoute())
-  .route("/v1/track", makeEventRoute());
+  .route("/v1/track", makeEventRoute())
+  .route("/integrations", makeIntegrationsRoute());
diff --git a/apps/web/src/api/routes/integrations.ts b/apps/web/src/api/routes/integrations.ts
new file mode 100644
index 00000000..1c402bb6
--- /dev/null
+++ b/apps/web/src/api/routes/integrations.ts
@@ -0,0 +1,94 @@
+import { zValidator } from "@hono/zod-validator";
+import { authMiddleware } from "api/helpers";
+import { Hono } from "hono";
+import { githubApp } from "server/common/github-app";
+import type { GithubIntegrationSettings } from "server/common/integrations";
+import { prisma } from "server/db/client";
+import { z } from "zod";
+
+export function makeIntegrationsRoute() {
+  return new Hono()
+    .get(
+      "/github",
+      zValidator(
+        "query",
+        z.object({
+          projectId: z.string(),
+        })
+      ),
+      authMiddleware,
+      async (c) => {
+        const user = c.get("user");
+        const { projectId } = c.req.valid("query");
+        const project = await prisma.project.findFirst({
+          where: { id: projectId, users: { some: { userId: user.id } } },
+          include: { integrations: true },
+        });
+        const referrer = new URL(c.req.header("Referer") ?? "/");
+
+        if (!project) {
+          referrer.searchParams.set("error", "Unauthorized");
+          return c.redirect(referrer.toString());
+        }
+        if (project.integrations.some((i) => i.type === "GITHUB")) {
+          referrer.searchParams.set("error", "Integration already exists");
+          return c.redirect(referrer.toString());
+        }
+
+        const searchParams = new URLSearchParams();
+        searchParams.set("projectId", projectId);
+
+        return c.redirect(
+          await githubApp.getInstallationUrl({ state: searchParams.toString() })
+        );
+      }
+    )
+    .get(
+      "/github/setup",
+      zValidator(
+        "query",
+        z.object({
+          installation_id: z.string().transform(Number),
+          setup_action: z.enum(["install", "update"]),
+          state: z.string().transform((s) => {
+            const url = new URLSearchParams(s);
+            const projectId = url.get("projectId");
+            if (!projectId) {
+              throw new Error("projectId not found in state");
+            }
+            return { projectId };
+          }),
+        })
+      ),
+      async (c) => {
+        const { installation_id, setup_action, state } = c.req.valid("query");
+        if (setup_action === "update") {
+          return c.json({ message: "Update not implemented" }, { status: 501 });
+        }
+
+        const project = await prisma.project.findFirst({
+          where: { id: state.projectId },
+          include: { integrations: true },
+        });
+        if (!project) {
+          return c.json(
+            { message: "Project not found" },
+            {
+              status: 404,
+            }
+          );
+        }
+        await prisma.integration.create({
+          data: {
+            type: "GITHUB",
+            projectId: project.id,
+            settings: {
+              installationId: installation_id,
+              repositoryIds: [],
+            } satisfies GithubIntegrationSettings,
+          },
+        });
+        return c.redirect(`/projects/${project.id}/settings`);
+      }
+    );
+}
diff --git a/apps/web/src/components/AddFeatureFlagModal.tsx b/apps/web/src/components/AddFeatureFlagModal.tsx
index 8187e97a..8a6f964e 100644
--- a/apps/web/src/components/AddFeatureFlagModal.tsx
+++ b/apps/web/src/components/AddFeatureFlagModal.tsx
@@ -177,7 +177,9 @@ export const AddFeatureFlagModal = ({
 
   const { mutateAsync } = trpc.flags.addFlag.useMutation({
     onSuccess() {
-      ctx.flags.getFlags.invalidate({ projectId });
+      ctx.flags.getFlags.invalidate({
+        projectId,
+      });
     },
   });
 
diff --git a/apps/web/src/components/FlagPage.tsx b/apps/web/src/components/FlagPage.tsx
index 22291fe5..0d190af4 100644
--- a/apps/web/src/components/FlagPage.tsx
+++ b/apps/web/src/components/FlagPage.tsx
@@ -1,5 +1,5 @@
 import type { FeatureFlagType } from "@prisma/client";
-import type { InferQueryResult } from "@trpc/react-query/dist/utils/inferReactQueryProcedure";
+import type { inferRouterOutputs } from "@trpc/server";
 import { AddFeatureFlagModal } from "components/AddFeatureFlagModal";
 import { CreateEnvironmentModal } from "components/CreateEnvironmentModal";
 import {
@@ -15,8 +15,14 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "components/Tooltip";
 import { Input } from "components/ui/input";
 import Fuse from "fuse.js";
 import { useProjectId } from "lib/hooks/useProjectId";
-import { EditIcon, FileEditIcon, Search, TrashIcon } from "lucide-react";
-import { useMemo, useState } from "react";
+import {
+  EditIcon,
+  FileEditIcon,
+  Search,
+  Sparkle,
+  TrashIcon,
+} from "lucide-react";
+import { useEffect, useMemo, useState } from "react";
 import { toast } from "react-hot-toast";
 import { AiOutlinePlus } from "react-icons/ai";
 import { BiInfoCircle } from "react-icons/bi";
@@ -155,9 +161,7 @@ export const FeatureFlagPageContent = ({
   data,
   type,
 }: {
-  data: NonNullable<
-    InferQueryResult<(typeof appRouter)["flags"]["getFlags"]>["data"]
-  >;
+  data: NonNullable<inferRouterOutputs<typeof appRouter>["flags"]["getFlags"]>;
   type: "Flags" | "Remote Config";
 }) => {
   const [isCreateFlagModalOpen, setIsCreateFlagModalOpen] = useState(false);
@@ -170,6 +174,8 @@ export const FeatureFlagPageContent = ({
 
   const [isCreateEnvironmentModalOpen, setIsCreateEnvironmentModalOpen] =
     useState(false);
+  const createFlagRemovalPRMutation =
+    trpc.flags.createFlagRemovalPR.useMutation();
 
   const projectId = useProjectId();
 
@@ -178,6 +184,10 @@ export const FeatureFlagPageContent = ({
     [data.flags]
   );
 
+  useEffect(() => {
+    setFlags(data.flags);
+  }, [data.flags]);
+
   const onSearch = (query: string) => {
     if (!query) {
       setFlags(data.flags);
@@ -326,6 +336,26 @@ export const FeatureFlagPageContent = ({
                         <FileEditIcon className="mr-4 h-4 w-4" />
                         Edit Description
                       </DropdownMenuItem>
+                      <DropdownMenuItem
+                        disabled={!data.hasGithubIntegration}
+                        className="cursor-pointer bg-gradient-to-r from-blue-800 via-purple-600 to-pink-500 hover:from-purple-700 hover:via-pink-500 hover:to-red-400"
+                        onClick={async () => {
+                          const url = await toast.promise(
+                            createFlagRemovalPRMutation.mutateAsync({
+                              flagId: currentFlag.id,
+                            }),
+                            {
+                              loading: "Creating removal PR...",
+                              success: "Successfully created removal PR",
+                              error: "Failed to create removal PR",
+                            }
+                          );
+                          window.open(url, "_blank");
+                        }}
+                      >
+                        <Sparkle className="mr-4 h-4 w-4" />
+                        Create Removal PR
+                      </DropdownMenuItem>
                       <DropdownMenuItem
                         className="cursor-pointer focus:!bg-red-700 focus:!text-white"
                         onClick={() => {
diff --git a/apps/web/src/components/settings/Integrations.tsx b/apps/web/src/components/settings/Integrations.tsx
new file mode 100644
index 00000000..c0b50688
--- /dev/null
+++ b/apps/web/src/components/settings/Integrations.tsx
@@ -0,0 +1,174 @@
+import { Label } from "@radix-ui/react-label";
+import { LoadingSpinner } from "components/LoadingSpinner";
+import { Button } from "components/ui/button";
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from "components/ui/card";
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+} from "components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "components/ui/popover";
+import { cn } from "lib/utils";
+import { Check, ChevronsUpDown } from "lucide-react";
+import Link from "next/link";
+import { useState } from "react";
+import { match } from "ts-pattern";
+import { trpc } from "utils/trpc";
+
+export function Integrations({ projectId }: { projectId: string | undefined }) {
+  const [open, setOpen] = useState(false);
+  const [selectedRepositoryId, setSelectedRepositoryId] = useState<string>();
+  const integrationsQuery = trpc.project.getIntegrations.useQuery(
+    // biome-ignore lint/style/noNonNullAssertion: we check for enabled
+    { projectId: projectId! },
+    { enabled: !!projectId }
+  );
+
+  const updateGithubIntegration =
+    trpc.project.updateGithubIntegration.useMutation({
+      onSuccess: () => {
+        integrationsQuery.refetch();
+      },
+    });
+
+  if (integrationsQuery.isLoading) return <LoadingSpinner />;
+  if (integrationsQuery.error) return <div>Error</div>;
+  if (integrationsQuery.data.length === 0) {
+    return (
+      <div>
+        <h1>No integrations</h1>
+      </div>
+    );
+  }
+
+  return integrationsQuery.data.map((i) =>
+    match(i.type)
+      .with("GITHUB", () => {
+        const selectedRepository = i.potentialRepositories.find(
+          (r) => r.id.toString() === selectedRepositoryId
+        );
+
+        return (
+          <Card key={i.id} className="max-w-lg">
+            <CardHeader>
+              <CardTitle>Github</CardTitle>
+              <CardDescription>
+                The selected repository will be used to automagically create and
+                update pull requests for your feature flags.
+              </CardDescription>
+            </CardHeader>
+            <CardContent>
+              {i.installedRepos.length === 0 ? (
+                <div className="flex flex-col space-y-3">
+                  <div className="grid gap-x-1.5">
+                    <Label>Selected Repository</Label>
+                    <Popover open={open} onOpenChange={setOpen}>
+                      <PopoverTrigger asChild>
+                        <Button
+                          variant="outline"
+                          role="combobox"
+                          aria-expanded={open}
+                          className="w-[350px] justify-between"
+                        >
+                          {selectedRepository ? (
+                            <span>
+                              <span className="text-muted-foreground">
+                                {selectedRepository.owner}/
+                              </span>
+                              {selectedRepository.name}
+                            </span>
+                          ) : (
+                            "Select framework..."
+                          )}
+                          <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                        </Button>
+                      </PopoverTrigger>
+                      <PopoverContent className="w-[350px] p-0">
+                        <Command>
+                          <CommandInput placeholder="Search repository..." />
+                          <CommandList>
+                            <CommandEmpty>No framework found.</CommandEmpty>
+                            <CommandGroup>
+                              {i.potentialRepositories.map((repo) => (
+                                <CommandItem
+                                  key={repo.id}
+                                  value={`${repo.owner}/${repo.name}`}
+                                  onSelect={() => {
+                                    setSelectedRepositoryId(
+                                      repo.id.toString() ===
+                                        selectedRepositoryId
+                                        ? undefined
+                                        : repo.id.toString()
+                                    );
+                                    setOpen(false);
+                                  }}
+                                >
+                                  <Check
+                                    className={cn(
+                                      "mr-2 h-4 w-4",
+                                      selectedRepositoryId ===
+                                        repo.id.toString()
+                                        ? "opacity-100"
+                                        : "opacity-0"
+                                    )}
+                                  />
+                                  <span>
+                                    <span className="text-muted-foreground">
+                                      {repo.owner}/
+                                    </span>
+                                    {repo.name}
+                                  </span>
+                                </CommandItem>
+                              ))}
+                            </CommandGroup>
+                          </CommandList>
+                        </Command>
+                      </PopoverContent>
+                    </Popover>
+                  </div>
+                  <Button
+                    className="ml-auto"
+                    onClick={async () => {
+                      if (!selectedRepositoryId) return;
+                      await updateGithubIntegration.mutateAsync({
+                        integrationId: i.id,
+                        repositoryId: Number(selectedRepositoryId),
+                      });
+                    }}
+                  >
+                    Save
+                  </Button>
+                </div>
+              ) : (
+                <div className="flex flex-col space-y-3">
+                  <span>Your selected repository is</span>
+
+                  <Link
+                    href={`https://github.com/${i.installedRepos[0]?.owner}/${i.installedRepos[0]?.name}`}
+                    className="bg-primary-foreground mr-auto p-2 rounded-md font-mono"
+                  >
+                    <span className="text-muted-foreground">
+                      {i.installedRepos[0]?.owner}/
+                    </span>
+                    {i.installedRepos[0]?.name}
+                  </Link>
+
+                  <small>Need to change this? Contact us</small>
+                </div>
+              )}
+            </CardContent>
+          </Card>
+        );
+      })
+      .exhaustive()
+  );
+}
diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx
index 7d43cb6a..342b8b6c 100644
--- a/apps/web/src/components/ui/select.tsx
+++ b/apps/web/src/components/ui/select.tsx
@@ -1,5 +1,5 @@
 import * as SelectPrimitive from "@radix-ui/react-select";
-import { Check, ChevronDown } from "lucide-react";
+import { Check, ChevronDown, ChevronUp } from "lucide-react";
 import * as React from "react";
 
 import { cn } from "lib/utils";
@@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
   <SelectPrimitive.Trigger
     ref={ref}
     className={cn(
-      "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+      "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
       className
     )}
     {...props}
@@ -30,6 +30,41 @@ const SelectTrigger = React.forwardRef<
 ));
 SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
 
+const SelectScrollUpButton = React.forwardRef<
+  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
+  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
+>(({ className, ...props }, ref) => (
+  <SelectPrimitive.ScrollUpButton
+    ref={ref}
+    className={cn(
+      "flex cursor-default items-center justify-center py-1",
+      className
+    )}
+    {...props}
+  >
+    <ChevronUp className="h-4 w-4" />
+  </SelectPrimitive.ScrollUpButton>
+));
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
+
+const SelectScrollDownButton = React.forwardRef<
+  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
+  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
+>(({ className, ...props }, ref) => (
+  <SelectPrimitive.ScrollDownButton
+    ref={ref}
+    className={cn(
+      "flex cursor-default items-center justify-center py-1",
+      className
+    )}
+    {...props}
+  >
+    <ChevronDown className="h-4 w-4" />
+  </SelectPrimitive.ScrollDownButton>
+));
+SelectScrollDownButton.displayName =
+  SelectPrimitive.ScrollDownButton.displayName;
+
 const SelectContent = React.forwardRef<
   React.ElementRef<typeof SelectPrimitive.Content>,
   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
@@ -38,7 +73,7 @@ const SelectContent = React.forwardRef<
     <SelectPrimitive.Content
       ref={ref}
       className={cn(
-        "relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+        "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
         position === "popper" &&
           "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
         className
@@ -46,6 +81,7 @@ const SelectContent = React.forwardRef<
       position={position}
       {...props}
     >
+      <SelectScrollUpButton />
       <SelectPrimitive.Viewport
         className={cn(
           "p-1",
@@ -55,6 +91,7 @@ const SelectContent = React.forwardRef<
       >
         {children}
       </SelectPrimitive.Viewport>
+      <SelectScrollDownButton />
     </SelectPrimitive.Content>
   </SelectPrimitive.Portal>
 ));
@@ -79,12 +116,12 @@ const SelectItem = React.forwardRef<
   <SelectPrimitive.Item
     ref={ref}
     className={cn(
-      "relative flex w-full cursor-default select-none items-center rounded-sm px-4 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+      "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
       className
     )}
     {...props}
   >
-    <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
+    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
       <SelectPrimitive.ItemIndicator>
         <Check className="h-4 w-4" />
       </SelectPrimitive.ItemIndicator>
@@ -116,4 +153,6 @@ export {
   SelectLabel,
   SelectItem,
   SelectSeparator,
+  SelectScrollUpButton,
+  SelectScrollDownButton,
 };
diff --git a/apps/web/src/env/schema.mjs b/apps/web/src/env/schema.mjs
index 0f0c71bc..fb7ff6f3 100644
--- a/apps/web/src/env/schema.mjs
+++ b/apps/web/src/env/schema.mjs
@@ -31,6 +31,10 @@ export const serverSchema = z.object({
   GOOGLE_CLIENT_ID: z.string().optional(),
   GOOGLE_CLIENT_SECRET: z.string().optional(),
   HASHING_SECRET: z.string().min(1),
+  ENABLE_GITHUB_APP: z.boolean().optional(),
+  GITHUB_APP_ID: z.string().optional(),
+  GITHUB_APP_PRIVATE_KEY: z.string().optional(),
+  OPENAI_API_KEY: z.string().optional(),
 });
 
 /**
diff --git a/apps/web/src/pages/projects/[projectId]/settings.tsx b/apps/web/src/pages/projects/[projectId]/settings.tsx
index 0c526bae..6a1cf2fe 100644
--- a/apps/web/src/pages/projects/[projectId]/settings.tsx
+++ b/apps/web/src/pages/projects/[projectId]/settings.tsx
@@ -1,4 +1,5 @@
 import { ROLE, type User } from "@prisma/client";
+import { GitHubLogoIcon } from "@radix-ui/react-icons";
 import clsx from "clsx";
 import { DashboardButton } from "components/DashboardButton";
 import {
@@ -11,6 +12,7 @@ import { Layout } from "components/Layout";
 import { FullPageLoadingSpinner } from "components/LoadingSpinner";
 import { Progress } from "components/Progress";
 import { RemoveUserModal } from "components/RemoveUserModal";
+import { Integrations } from "components/settings/Integrations";
 import { Button } from "components/ui/button";
 import { Input } from "components/ui/input";
 import { Tabs, TabsContent, TabsList, TabsTrigger } from "components/ui/tabs";
@@ -33,6 +35,7 @@ const SETTINGS_TABS = {
   General: "general",
   Team: "team",
   Billing: "billing",
+  Integrations: "integrations",
   Danger: "danger",
 } as const;
 
@@ -47,6 +50,7 @@ const SettingsPage: NextPageWithLayout = () => {
       SETTINGS_TABS.Team,
       SETTINGS_TABS.Billing,
       SETTINGS_TABS.Danger,
+      SETTINGS_TABS.Integrations,
     ] as const).withDefault(SETTINGS_TABS.General)
   );
   const router = useRouter();
@@ -56,9 +60,14 @@ const SettingsPage: NextPageWithLayout = () => {
   const projectNameRef = useRef<HTMLInputElement>(null);
 
   const trpcContext = trpc.useContext();
-  const { data, isLoading, isError } = trpc.project.getProjectData.useQuery({
-    projectId,
-  });
+  const { data, isLoading, isError } = trpc.project.getProjectData.useQuery(
+    {
+      projectId,
+    },
+    {
+      enabled: !!projectId,
+    }
+  );
 
   const session = useSession();
 
@@ -225,6 +234,33 @@ const SettingsPage: NextPageWithLayout = () => {
               </div>
             </DashboardSection>
           </TabsContent>
+          <TabsContent value="integrations">
+            <DashboardSection>
+              <DashboardSectionTitle>Integrations</DashboardSectionTitle>
+              <DashboardSectionSubtitle className="mb-8">
+                Manage your integrations
+              </DashboardSectionSubtitle>
+              <div className="flex flex-col space-y-4">
+                {data.project.integrations.length === 0 ? (
+                  <div className="flex flex-col space-y-3">
+                    <Link
+                      href={`/api/integrations/github?projectId=${projectId}`}
+                    >
+                      <Button
+                        variant="outline"
+                        className="flex space-x-2 mr-auto"
+                      >
+                        <GitHubLogoIcon height={18} width={18} />{" "}
+                        <span>Connect with Github</span>
+                      </Button>
+                    </Link>
+                  </div>
+                ) : (
+                  <Integrations projectId={projectId} />
+                )}
+              </div>
+            </DashboardSection>
+          </TabsContent>
           <TabsContent value="billing">
             <DashboardSection>
               <DashboardSectionTitle>Usage</DashboardSectionTitle>
diff --git a/apps/web/src/server/common/auth.ts b/apps/web/src/server/common/auth.ts
new file mode 100644
index 00000000..6698b3d3
--- /dev/null
+++ b/apps/web/src/server/common/auth.ts
@@ -0,0 +1,22 @@
+import { TRPCError } from "@trpc/server";
+import { prisma } from "server/db/client";
+
+export async function assertUserHasAcessToProject(
+  projectId: string,
+  userId: string
+) {
+  const project = await prisma.project.findFirst({
+    where: {
+      id: projectId,
+      users: {
+        some: {
+          userId: userId,
+        },
+      },
+    },
+  });
+  if (!project) {
+    throw new TRPCError({ code: "UNAUTHORIZED" });
+  }
+  return project;
+}
diff --git a/apps/web/src/server/common/github-app.ts b/apps/web/src/server/common/github-app.ts
new file mode 100644
index 00000000..915dc0c2
--- /dev/null
+++ b/apps/web/src/server/common/github-app.ts
@@ -0,0 +1,50 @@
+import memoize from "memoize";
+import { App } from "octokit";
+import { env } from "../../env/server.mjs";
+
+if (
+  env.ENABLE_GITHUB_APP &&
+  (!env.GITHUB_APP_ID || !env.GITHUB_APP_PRIVATE_KEY)
+) {
+  throw new Error("Missing required environment variables for GitHub App");
+}
+
+export const githubApp = new App({
+  // biome-ignore lint/style/noNonNullAssertion: we check above
+  appId: env.GITHUB_APP_ID!,
+  // biome-ignore lint/style/noNonNullAssertion: we check above
+  privateKey: env.GITHUB_APP_PRIVATE_KEY!,
+});
+
+const PER_PAGE = 100;
+export const getAllRepositoriesForInstallation = memoize(
+  async (installationId: number) => {
+    const gh = await githubApp.getInstallationOctokit(installationId);
+    let count = 0;
+    const res = await gh.request("GET /installation/repositories", {
+      installation_id: installationId,
+      per_page: 100,
+    });
+    count += res.data.repositories.length;
+    const repositories = res.data.repositories;
+
+    let hasMore = res.data.total_count > count;
+    while (hasMore) {
+      const res = await gh.request("GET /installation/repositories", {
+        installation_id: installationId,
+        per_page: 100,
+        page: Math.ceil(count / PER_PAGE) + 1,
+      });
+      count += res.data.repositories.length;
+      hasMore = res.data.total_count > count;
+      repositories.push(...res.data.repositories);
+    }
+
+    return repositories.toSorted((a, b) => {
+      return a.full_name.localeCompare(b.full_name);
+    });
+  },
+  {
+    maxAge: 1000 * 60,
+  }
+);
diff --git a/apps/web/src/server/common/integrations.ts b/apps/web/src/server/common/integrations.ts
new file mode 100644
index 00000000..9eb38e77
--- /dev/null
+++ b/apps/web/src/server/common/integrations.ts
@@ -0,0 +1,12 @@
+import { z } from "zod";
+
+export const githubIntegrationSettingsSchema = z
+  .object({
+    installationId: z.number(),
+    repositoryIds: z.array(z.number()),
+  })
+  .strict();
+
+export type GithubIntegrationSettings = z.infer<
+  typeof githubIntegrationSettingsSchema
+>;
diff --git a/apps/web/src/server/services/AiFlagRemovalService.ts b/apps/web/src/server/services/AiFlagRemovalService.ts
new file mode 100644
index 00000000..134cc87d
--- /dev/null
+++ b/apps/web/src/server/services/AiFlagRemovalService.ts
@@ -0,0 +1,32 @@
+import type { OpenAI } from "openai";
+
+export class AIFlagRemovalService {
+  constructor(private openai: OpenAI) {}
+
+  async removeFlagFromCode(code: string, flagName: string) {
+    const response = await this.openai.chat.completions.create({
+      model: "gpt-4o-mini",
+      messages: [
+        {
+          role: "system",
+          content: `You are an expert in code refactoring and have been assigned to update a codebase to remove calls to the useFeatureFlag hook where the parameter is ${flagName}. Your task is to ensure that all code paths that depend on the truthiness of this hook are always executed. But don't just replace the result of the function with true but rather remove the variable (if used). and update the code as if there was never a flag that prevented certain paths. You should only include the code without any other information or formatting. You should never update any other code that isnt related to the flag with the name ${flagName}. You should keep the formatting and line ending of the original code.`,
+        },
+        {
+          role: "user",
+          content: [
+            {
+              type: "text",
+              text: code,
+            },
+          ],
+        },
+      ],
+      temperature: 1,
+      max_tokens: 16383,
+      top_p: 1,
+      frequency_penalty: 0,
+      presence_penalty: 0,
+    });
+    return response.choices[0]?.message.content;
+  }
+}
diff --git a/apps/web/src/server/services/IntegrationService.ts b/apps/web/src/server/services/IntegrationService.ts
new file mode 100644
index 00000000..a96d2085
--- /dev/null
+++ b/apps/web/src/server/services/IntegrationService.ts
@@ -0,0 +1,3 @@
+export namespace IntegrationService {
+  //     createIntegration(projectId: string, In)
+}
diff --git a/apps/web/src/server/trpc/router/flags.ts b/apps/web/src/server/trpc/router/flags.ts
index d0ffdb06..8b694c31 100644
--- a/apps/web/src/server/trpc/router/flags.ts
+++ b/apps/web/src/server/trpc/router/flags.ts
@@ -1,6 +1,13 @@
+import { randomUUID } from "node:crypto";
 import { FeatureFlagType } from "@prisma/client";
 import { TRPCError } from "@trpc/server";
+import { getUseFeatureFlagRegex } from "@tryabby/core";
+import { env } from "env/server.mjs";
+import OpenAI from "openai";
 import { ConfigCache } from "server/common/config-cache";
+import { githubApp } from "server/common/github-app";
+import { githubIntegrationSettingsSchema } from "server/common/integrations";
+import { AIFlagRemovalService } from "server/services/AiFlagRemovalService";
 import { FlagService } from "server/services/FlagService";
 import { validateFlag } from "utils/validateFlags";
 import { z } from "zod";
@@ -15,40 +22,57 @@ export const flagRouter = router({
       })
     )
     .query(async ({ ctx, input }) => {
-      const flags = await ctx.prisma.featureFlag.findMany({
-        where: {
-          type: {
-            in: input.types,
-          },
-          project: {
-            id: input.projectId,
-            users: {
-              some: {
-                userId: ctx.session.user.id,
-              },
-            },
-          },
-        },
-        orderBy: { createdAt: "asc" },
+      const project = await ctx.prisma.project.findUnique({
+        where: { id: input.projectId },
         include: {
-          values: { include: { environment: true } },
+          users: { select: { userId: true } },
+          integrations: { where: { type: "GITHUB" } },
         },
       });
-
-      const environments = await ctx.prisma.environment.findMany({
-        where: {
-          project: {
-            id: input.projectId,
-            users: {
-              some: {
-                userId: ctx.session.user.id,
+      if (!project) throw new TRPCError({ code: "NOT_FOUND" });
+      if (!project.users.some((u) => u.userId === ctx.session.user.id)) {
+        throw new TRPCError({ code: "UNAUTHORIZED" });
+      }
+      const [flags, environments] = await Promise.all([
+        ctx.prisma.featureFlag.findMany({
+          where: {
+            type: {
+              in: input.types,
+            },
+            project: {
+              id: input.projectId,
+            },
+          },
+          orderBy: { createdAt: "asc" },
+          include: {
+            values: {
+              include: {
+                environment: true,
               },
             },
           },
-        },
-        orderBy: { sortIndex: "asc" },
-      });
-      return { flags, environments };
+        }),
+        ctx.prisma.environment.findMany({
+          where: {
+            project: {
+              id: input.projectId,
+            },
+          },
+          orderBy: { sortIndex: "asc" },
+        }),
+      ]);
+      const githubIntegration = project.integrations[0];
+      const integrationSettings = githubIntegration
+        ? githubIntegrationSettingsSchema.parse(githubIntegration.settings)
+        : null;
+
+      return {
+        flags,
+        environments,
+        hasGithubIntegration:
+          !!integrationSettings?.repositoryIds[0] &&
+          integrationSettings?.installationId,
+      };
     }),
   addFlag: protectedProcedure
     .input(
@@ -300,4 +324,159 @@ export const flagRouter = router({
         })
       );
     }),
+  createFlagRemovalPR: protectedProcedure
+    .input(
+      z.object({
+        flagId: z.string(),
+      })
+    )
+    .mutation(async ({ ctx, input }) => {
+      const flag = await ctx.prisma.featureFlag.findUnique({
+        where: { id: input.flagId },
+        include: {
+          project: {
+            include: { users: true, integrations: true },
+          },
+        },
+      });
+      if (!flag) throw new TRPCError({ code: "NOT_FOUND" });
+      const integration = flag.project.integrations.find(
+        (i) => i.type === "GITHUB"
+      );
+      if (!integration) {
+        throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
+      }
+
+      if (!flag.project.users.some((u) => u.userId === ctx.session.user.id)) {
+        throw new TRPCError({ code: "UNAUTHORIZED" });
+      }
+
+      const parsedIntegration = githubIntegrationSettingsSchema.parse(
+        integration.settings
+      );
+      const gh = await githubApp.getInstallationOctokit(
+        parsedIntegration.installationId
+      );
+      const repositoryId = parsedIntegration.repositoryIds[0];
+      if (!repositoryId) {
+        throw new TRPCError({
+          code: "INTERNAL_SERVER_ERROR",
+        });
+      }
+
+      const repo = await gh.request("GET /repositories/:id", {
+        id: repositoryId,
+      });
+
+      if (!repo) {
+        throw new TRPCError({
+          code: "INTERNAL_SERVER_ERROR",
+        });
+      }
+
+      if (!env.OPENAI_API_KEY) {
+        throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
+      }
+      const ai = new AIFlagRemovalService(
+        new OpenAI({
+          apiKey: env.OPENAI_API_KEY,
+        })
+      );
+
+      const flagRegex = getUseFeatureFlagRegex(flag.name);
+      const owner = repo.data.owner.login;
+      const name = repo.data.name;
+      const defaultBranch = repo.data.default_branch;
+
+      const u = await gh.request(
+        "GET /repos/{owner}/{repo}/git/trees/{tree_sha}",
+        {
+          owner,
+          repo: name,
+          tree_sha: defaultBranch,
+          recursive: "1",
+        }
+      );
+
+      const files = u.data.tree
+        .filter((f) => f.type === "blob")
+        .filter((f) => f.path?.endsWith(".ts") || f.path?.endsWith(".tsx"));
+
+      if (files.length === 0) {
+        throw new TRPCError({
+          code: "INTERNAL_SERVER_ERROR",
+          message: "No files found in the repository",
+        });
+      }
+
+      const fileContents = (
+        await Promise.all(
+          files.flatMap(async (f) => {
+            if (!f.sha || !f.path) return [];
+            const res = await gh.request(
+              "GET /repos/{owner}/{repo}/git/blobs/{file_sha}",
+              {
+                owner,
+                repo: name,
+                file_sha: f.sha,
+              }
+            );
+            const fileContent = Buffer.from(
+              res.data.content,
+              "base64"
+            ).toString("utf-8");
+            if (!flagRegex.test(fileContent)) return [];
+            return {
+              fileSha: f.sha,
+              filePath: f.path,
+              fileContent,
+            };
+          })
+        )
+      ).flat();
+      const baseBranchResponse = await gh.rest.git.getRef({
+        owner,
+        repo: name,
+        ref: `heads/${defaultBranch}`,
+      });
+
+      const baseSha = baseBranchResponse.data.object.sha;
+
+      const b = await gh.rest.git.createRef({
+        owner: "cstrnt",
+        repo: name,
+        ref: `refs/heads/abby_${randomUUID()}_remove_client_flag`,
+        sha: baseSha,
+      });
+
+      await Promise.all(
+        fileContents.map(async (f) => {
+          const updatedCode = await ai.removeFlagFromCode(
+            f.fileContent,
+            flag.name
+          );
+          if (!updatedCode) return;
+          return gh.rest.repos.createOrUpdateFileContents({
+            owner,
+            repo: name,
+            path: f.filePath,
+            message: `Remove Feature Flag: ${flag.name}`,
+            content: Buffer.from(updatedCode).toString("base64"),
+            sha: f.fileSha,
+            branch: b.data.ref,
+          });
+        })
+      );
+
+      const response = await gh.rest.pulls.create({
+        owner,
+        repo: name,
+        title: `[ABBY] Remove Feature Flag ${flag.name}`,
+        head: b.data.ref,
+        base: defaultBranch,
+        body: `This PR was created by Abby to remove the flag ${flag.name} from the codebase. Please review the changes and merge when ready.`,
+      });
+
+      return response.data.html_url;
+    }),
 });
diff --git a/apps/web/src/server/trpc/router/project.ts b/apps/web/src/server/trpc/router/project.ts
index cdfee39a..0573d3ac 100644
--- a/apps/web/src/server/trpc/router/project.ts
+++ b/apps/web/src/server/trpc/router/project.ts
@@ -2,6 +2,7 @@ import { type Option, ROLE } from "@prisma/client";
 import { TRPCError } from "@trpc/server";
 import { PLANS, planNameSchema } from "server/common/plans";
 import { stripe } from "server/common/stripe";
+import { prisma } from "server/db/client";
 import { EventService } from "server/services/EventService";
 import { ProjectService } from "server/services/ProjectService";
 import { generateCodeSnippets } from "utils/snippets";
@@ -12,6 +13,13 @@ export type ClientOption = Omit<Option, "chance"> & {
 };
 import { AbbyEventType } from "@tryabby/core";
 import dayjs from "dayjs";
+import { assertUserHasAcessToProject } from "server/common/auth";
+import {
+  getAllRepositoriesForInstallation,
+  githubApp,
+} from "server/common/github-app";
+import { githubIntegrationSettingsSchema } from "server/common/integrations";
+import { match } from "ts-pattern";
 import { updateProjectsOnSession } from "utils/updateSession";
 import { protectedProcedure, router } from "../trpc";
 
@@ -37,6 +45,7 @@ export const projectRouter = router({
           environments: true,
           featureFlags: true,
           users: { include: { user: true } },
+          integrations: true,
         },
       });
 
@@ -314,4 +323,96 @@ export const projectRouter = router({
         nextCursor,
       };
     }),
+  getIntegrations: protectedProcedure
+    .input(
+      z.object({
+        projectId: z.string(),
+      })
+    )
+    .query(async ({ ctx, input }) => {
+      await assertUserHasAcessToProject(input.projectId, ctx.session.user.id);
+
+      const project = await prisma.project.findUniqueOrThrow({
+        where: { id: input.projectId },
+        include: { integrations: true },
+      });
+
+      return await Promise.all(
+        project.integrations.map((i) =>
+          match(i.type)
+            .with("GITHUB", async () => {
+              const integration =
+                await githubIntegrationSettingsSchema.parseAsync(i.settings);
+
+              const gh = await githubApp.getInstallationOctokit(
+                integration.installationId
+              );
+              const [potentialRepositories, ...installedRepos] =
+                await Promise.all([
+                  getAllRepositoriesForInstallation(integration.installationId),
+                  ...integration.repositoryIds.map((id) =>
+                    gh.request("GET /repositories/:id", {
+                      id,
+                    })
+                  ),
+                ]);
+
+              return {
+                ...i,
+                settings: integration,
+                potentialRepositories: potentialRepositories.map((r) => ({
+                  name: r.name,
+                  owner: r.owner.login,
+                  id: r.id,
+                })),
+                installedRepos: installedRepos.map((r) => ({
+                  name: r.data.name,
+                  owner: r.data.owner.login,
+                  id: r.data.id,
+                })),
+              };
+            })
+            .exhaustive()
+        )
+      );
+    }),
+  updateGithubIntegration: protectedProcedure
+    .input(
+      z.object({
+        integrationId: z.string(),
+        repositoryId: z.number(),
+      })
+    )
+    .mutation(async ({ ctx, input }) => {
+      const integration = await prisma.integration.findUnique({
+        where: {
+          id: input.integrationId,
+        },
+        include: { project: { include: { users: true } } },
+      });
+      if (!integration) throw new TRPCError({ code: "NOT_FOUND" });
+      if (integration.type !== "GITHUB") {
+        throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
+      }
+      if (
+        !integration.project.users.some((u) => u.userId === ctx.session.user.id)
+      ) {
+        throw new TRPCError({ code: "UNAUTHORIZED" });
+      }
+
+      const newSettings = await githubIntegrationSettingsSchema.parseAsync(
+        integration.settings
+      );
+      newSettings.repositoryIds = [input.repositoryId];
+
+      return await prisma.integration.update({
+        where: {
+          id: input.integrationId,
+        },
+        data: {
+          settings:
+            await githubIntegrationSettingsSchema.parseAsync(newSettings),
+        },
+      });
+    }),
 });
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 7210a3f3..3314f645 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -10,7 +10,7 @@
     "esModuleInterop": true,
     "allowSyntheticDefaultImports": true,
     "module": "esnext",
-    "moduleResolution": "node",
+    "moduleResolution": "Bundler",
     "resolveJsonModule": true,
     "isolatedModules": true,
     "jsx": "preserve",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8d3f486c..2bdbd367 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -117,13 +117,13 @@ importers:
         version: 4.5.1(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0)
       '@next-auth/prisma-adapter':
         specifier: 1.0.5
-        version: 1.0.5(@prisma/client@5.6.0)(next-auth@4.22.1)
+        version: 1.0.5(@prisma/client@5.18.0)(next-auth@4.22.1)
       '@next/mdx':
         specifier: 14.0.4
         version: 14.0.4(@mdx-js/loader@3.0.0)(@mdx-js/react@3.0.0)
       '@prisma/client':
-        specifier: 5.6.0
-        version: 5.6.0(prisma@5.6.0)
+        specifier: 5.18.0
+        version: 5.18.0(prisma@5.18.0)
       '@radix-ui/react-avatar':
         specifier: ^1.0.3
         version: 1.0.3(@types/react-dom@18.0.5)(@types/react@18.0.14)(react-dom@18.2.0)(react@18.2.0)
@@ -292,6 +292,9 @@ importers:
       lucide-react:
         specifier: 0.320.0
         version: 0.320.0(react@18.2.0)
+      memoize:
+        specifier: ^10.0.0
+        version: 10.0.0
       micro:
         specifier: ^10.0.1
         version: 10.0.1
@@ -326,8 +329,14 @@ importers:
         specifier: ^1.17.8
         version: 1.17.8(next@14.1.1)
       octokit:
-        specifier: ^2.0.18
-        version: 2.0.19
+        specifier: ^4.0.2
+        version: 4.0.2
+      openai:
+        specifier: ^4.56.0
+        version: 4.56.0(zod@3.21.4)
+      prettier:
+        specifier: ^2.8.7
+        version: 2.8.8
       rate-limiter-flexible:
         specifier: ^5.0.3
         version: 5.0.3
@@ -428,15 +437,12 @@ importers:
       postcss:
         specifier: ^8.4.21
         version: 8.4.24
-      prettier:
-        specifier: ^2.8.7
-        version: 2.8.8
       prettier-plugin-tailwindcss:
         specifier: ^0.1.13
         version: 0.1.13(prettier@2.8.8)
       prisma:
-        specifier: 5.6.0
-        version: 5.6.0
+        specifier: 5.18.0
+        version: 5.18.0
       tailwindcss:
         specifier: ^3.3.1
         version: 3.3.2(ts-node@10.9.1)
@@ -7872,13 +7878,13 @@ packages:
       tar-fs: 2.1.1
     dev: true
 
-  /@next-auth/prisma-adapter@1.0.5(@prisma/client@5.6.0)(next-auth@4.22.1):
+  /@next-auth/prisma-adapter@1.0.5(@prisma/client@5.18.0)(next-auth@4.22.1):
     resolution: {integrity: sha512-VqMS11IxPXrPGXw6Oul6jcyS/n8GLOWzRMrPr3EMdtD6eOalM6zz05j08PcNiis8QzkfuYnCv49OvufTuaEwYQ==}
     peerDependencies:
       '@prisma/client': '>=2.26.0 || >=3'
       next-auth: ^4
     dependencies:
-      '@prisma/client': 5.6.0(prisma@5.6.0)
+      '@prisma/client': 5.18.0(prisma@5.18.0)
       next-auth: 4.22.1(next@14.1.1)(nodemailer@6.9.3)(react-dom@18.2.0)(react@18.2.0)
     dev: false
 
@@ -8205,259 +8211,232 @@ packages:
       - supports-color
     dev: true
 
-  /@octokit/app@13.1.5:
-    resolution: {integrity: sha512-6qTa24S+gdQUU66SCVfqTkyt2jAr9/ZeyPqJhnNI9PZ8Wum4lQy3bPS+voGlxABNOlzRKnxbSdYKoraMr3MqBA==}
-    engines: {node: '>= 14'}
+  /@octokit/app@15.1.0:
+    resolution: {integrity: sha512-TkBr7QgOmE6ORxvIAhDbZsqPkF7RSqTY4pLTtUQCvr6dTXqvi2fFo46q3h1lxlk/sGMQjqyZ0kEahkD/NyzOHg==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/auth-app': 4.0.13
-      '@octokit/auth-unauthenticated': 3.0.5
-      '@octokit/core': 4.2.1
-      '@octokit/oauth-app': 4.2.2
-      '@octokit/plugin-paginate-rest': 6.1.2(@octokit/core@4.2.1)
-      '@octokit/types': 9.3.1
-      '@octokit/webhooks': 10.9.1
-    transitivePeerDependencies:
-      - encoding
+      '@octokit/auth-app': 7.1.0
+      '@octokit/auth-unauthenticated': 6.1.0
+      '@octokit/core': 6.1.2
+      '@octokit/oauth-app': 7.1.3
+      '@octokit/plugin-paginate-rest': 11.3.3(@octokit/core@6.1.2)
+      '@octokit/types': 13.5.0
+      '@octokit/webhooks': 13.3.0
     dev: false
 
-  /@octokit/auth-app@4.0.13:
-    resolution: {integrity: sha512-NBQkmR/Zsc+8fWcVIFrwDgNXS7f4XDrkd9LHdi9DPQw1NdGHLviLzRO2ZBwTtepnwHXW5VTrVU9eFGijMUqllg==}
-    engines: {node: '>= 14'}
+  /@octokit/auth-app@7.1.0:
+    resolution: {integrity: sha512-cazGaJPSgeZ8NkVYeM/C5l/6IQ5vZnsI8p1aMucadCkt/bndI+q+VqwrlnWbASRmenjOkf1t1RpCKrif53U8gw==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/auth-oauth-app': 5.0.6
-      '@octokit/auth-oauth-user': 2.1.2
-      '@octokit/request': 6.2.5
-      '@octokit/request-error': 3.0.3
-      '@octokit/types': 9.3.1
-      deprecation: 2.3.1
-      lru-cache: 9.1.2
-      universal-github-app-jwt: 1.1.1
-      universal-user-agent: 6.0.0
-    transitivePeerDependencies:
-      - encoding
+      '@octokit/auth-oauth-app': 8.1.1
+      '@octokit/auth-oauth-user': 5.1.1
+      '@octokit/request': 9.1.3
+      '@octokit/request-error': 6.1.4
+      '@octokit/types': 13.5.0
+      lru-cache: 10.4.3
+      universal-github-app-jwt: 2.2.0
+      universal-user-agent: 7.0.2
     dev: false
 
-  /@octokit/auth-oauth-app@5.0.6:
-    resolution: {integrity: sha512-SxyfIBfeFcWd9Z/m1xa4LENTQ3l1y6Nrg31k2Dcb1jS5ov7pmwMJZ6OGX8q3K9slRgVpeAjNA1ipOAMHkieqyw==}
-    engines: {node: '>= 14'}
+  /@octokit/auth-oauth-app@8.1.1:
+    resolution: {integrity: sha512-5UtmxXAvU2wfcHIPPDWzVSAWXVJzG3NWsxb7zCFplCWEmMCArSZV0UQu5jw5goLQXbFyOr5onzEH37UJB3zQQg==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/auth-oauth-device': 4.0.5
-      '@octokit/auth-oauth-user': 2.1.2
-      '@octokit/request': 6.2.5
-      '@octokit/types': 9.3.1
-      '@types/btoa-lite': 1.0.0
-      btoa-lite: 1.0.0
-      universal-user-agent: 6.0.0
-    transitivePeerDependencies:
-      - encoding
+      '@octokit/auth-oauth-device': 7.1.1
+      '@octokit/auth-oauth-user': 5.1.1
+      '@octokit/request': 9.1.3
+      '@octokit/types': 13.5.0
+      universal-user-agent: 7.0.2
     dev: false
 
-  /@octokit/auth-oauth-device@4.0.5:
-    resolution: {integrity: sha512-XyhoWRTzf2ZX0aZ52a6Ew5S5VBAfwwx1QnC2Np6Et3MWQpZjlREIcbcvVZtkNuXp6Z9EeiSLSDUqm3C+aMEHzQ==}
-    engines: {node: '>= 14'}
+  /@octokit/auth-oauth-device@7.1.1:
+    resolution: {integrity: sha512-HWl8lYueHonuyjrKKIup/1tiy0xcmQCdq5ikvMO1YwkNNkxb6DXfrPjrMYItNLyCP/o2H87WuijuE+SlBTT8eg==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/oauth-methods': 2.0.6
-      '@octokit/request': 6.2.5
-      '@octokit/types': 9.3.1
-      universal-user-agent: 6.0.0
-    transitivePeerDependencies:
-      - encoding
+      '@octokit/oauth-methods': 5.1.2
+      '@octokit/request': 9.1.3
+      '@octokit/types': 13.5.0
+      universal-user-agent: 7.0.2
     dev: false
 
-  /@octokit/auth-oauth-user@2.1.2:
-    resolution: {integrity: sha512-kkRqNmFe7s5GQcojE3nSlF+AzYPpPv7kvP/xYEnE57584pixaFBH8Vovt+w5Y3E4zWUEOxjdLItmBTFAWECPAg==}
-    engines: {node: '>= 14'}
+  /@octokit/auth-oauth-user@5.1.1:
+    resolution: {integrity: sha512-rRkMz0ErOppdvEfnemHJXgZ9vTPhBuC6yASeFaB7I2yLMd7QpjfrL1mnvRPlyKo+M6eeLxrKanXJ9Qte29SRsw==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/auth-oauth-device': 4.0.5
-      '@octokit/oauth-methods': 2.0.6
-      '@octokit/request': 6.2.5
-      '@octokit/types': 9.3.1
-      btoa-lite: 1.0.0
-      universal-user-agent: 6.0.0
-    transitivePeerDependencies:
-      - encoding
+      '@octokit/auth-oauth-device': 7.1.1
+      '@octokit/oauth-methods': 5.1.2
+      '@octokit/request': 9.1.3
+      '@octokit/types': 13.5.0
+      universal-user-agent: 7.0.2
     dev: false
 
-  /@octokit/auth-token@3.0.4:
-    resolution: {integrity: sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==}
-    engines: {node: '>= 14'}
+  /@octokit/auth-token@5.1.1:
+    resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==}
+    engines: {node: '>= 18'}
     dev: false
 
-  /@octokit/auth-unauthenticated@3.0.5:
-    resolution: {integrity: sha512-yH2GPFcjrTvDWPwJWWCh0tPPtTL5SMgivgKPA+6v/XmYN6hGQkAto8JtZibSKOpf8ipmeYhLNWQ2UgW0GYILCw==}
-    engines: {node: '>= 14'}
+  /@octokit/auth-unauthenticated@6.1.0:
+    resolution: {integrity: sha512-zPSmfrUAcspZH/lOFQnVnvjQZsIvmfApQH6GzJrkIunDooU1Su2qt2FfMTSVPRp7WLTQyC20Kd55lF+mIYaohQ==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/request-error': 3.0.3
-      '@octokit/types': 9.3.1
+      '@octokit/request-error': 6.1.4
+      '@octokit/types': 13.5.0
     dev: false
 
-  /@octokit/core@4.2.1:
-    resolution: {integrity: sha512-tEDxFx8E38zF3gT7sSMDrT1tGumDgsw5yPG6BBh/X+5ClIQfMH/Yqocxz1PnHx6CHyF6pxmovUTOfZAUvQ0Lvw==}
-    engines: {node: '>= 14'}
+  /@octokit/core@6.1.2:
+    resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/auth-token': 3.0.4
-      '@octokit/graphql': 5.0.6
-      '@octokit/request': 6.2.5
-      '@octokit/request-error': 3.0.3
-      '@octokit/types': 9.3.1
-      before-after-hook: 2.2.3
-      universal-user-agent: 6.0.0
-    transitivePeerDependencies:
-      - encoding
+      '@octokit/auth-token': 5.1.1
+      '@octokit/graphql': 8.1.1
+      '@octokit/request': 9.1.3
+      '@octokit/request-error': 6.1.4
+      '@octokit/types': 13.5.0
+      before-after-hook: 3.0.2
+      universal-user-agent: 7.0.2
     dev: false
 
-  /@octokit/endpoint@7.0.6:
-    resolution: {integrity: sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==}
-    engines: {node: '>= 14'}
+  /@octokit/endpoint@10.1.1:
+    resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/types': 9.3.1
-      is-plain-object: 5.0.0
-      universal-user-agent: 6.0.0
+      '@octokit/types': 13.5.0
+      universal-user-agent: 7.0.2
     dev: false
 
-  /@octokit/graphql@5.0.6:
-    resolution: {integrity: sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==}
-    engines: {node: '>= 14'}
+  /@octokit/graphql@8.1.1:
+    resolution: {integrity: sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/request': 6.2.5
-      '@octokit/types': 9.3.1
-      universal-user-agent: 6.0.0
-    transitivePeerDependencies:
-      - encoding
+      '@octokit/request': 9.1.3
+      '@octokit/types': 13.5.0
+      universal-user-agent: 7.0.2
     dev: false
 
-  /@octokit/oauth-app@4.2.2:
-    resolution: {integrity: sha512-/jsPd43Yu2UXJ4XGq9KyOjPj5kNWQ5pfVzeDEfIVE8ENchyIPS+/IY2a8b0+OQSAsBKBLTHVp9m51RfGHmPZlw==}
-    engines: {node: '>= 14'}
+  /@octokit/oauth-app@7.1.3:
+    resolution: {integrity: sha512-EHXbOpBkSGVVGF1W+NLMmsnSsJRkcrnVmDKt0TQYRBb6xWfWzoi9sBD4DIqZ8jGhOWO/V8t4fqFyJ4vDQDn9bg==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/auth-oauth-app': 5.0.6
-      '@octokit/auth-oauth-user': 2.1.2
-      '@octokit/auth-unauthenticated': 3.0.5
-      '@octokit/core': 4.2.1
-      '@octokit/oauth-authorization-url': 5.0.0
-      '@octokit/oauth-methods': 2.0.6
+      '@octokit/auth-oauth-app': 8.1.1
+      '@octokit/auth-oauth-user': 5.1.1
+      '@octokit/auth-unauthenticated': 6.1.0
+      '@octokit/core': 6.1.2
+      '@octokit/oauth-authorization-url': 7.1.1
+      '@octokit/oauth-methods': 5.1.2
       '@types/aws-lambda': 8.10.117
-      fromentries: 1.3.2
-      universal-user-agent: 6.0.0
-    transitivePeerDependencies:
-      - encoding
+      universal-user-agent: 7.0.2
     dev: false
 
-  /@octokit/oauth-authorization-url@5.0.0:
-    resolution: {integrity: sha512-y1WhN+ERDZTh0qZ4SR+zotgsQUE1ysKnvBt1hvDRB2WRzYtVKQjn97HEPzoehh66Fj9LwNdlZh+p6TJatT0zzg==}
-    engines: {node: '>= 14'}
+  /@octokit/oauth-authorization-url@7.1.1:
+    resolution: {integrity: sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==}
+    engines: {node: '>= 18'}
     dev: false
 
-  /@octokit/oauth-methods@2.0.6:
-    resolution: {integrity: sha512-l9Uml2iGN2aTWLZcm8hV+neBiFXAQ9+3sKiQe/sgumHlL6HDg0AQ8/l16xX/5jJvfxueqTW5CWbzd0MjnlfHZw==}
-    engines: {node: '>= 14'}
+  /@octokit/oauth-methods@5.1.2:
+    resolution: {integrity: sha512-C5lglRD+sBlbrhCUTxgJAFjWgJlmTx5bQ7Ch0+2uqRjYv7Cfb5xpX4WuSC9UgQna3sqRGBL9EImX9PvTpMaQ7g==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/oauth-authorization-url': 5.0.0
-      '@octokit/request': 6.2.5
-      '@octokit/request-error': 3.0.3
-      '@octokit/types': 9.3.1
-      btoa-lite: 1.0.0
-    transitivePeerDependencies:
-      - encoding
+      '@octokit/oauth-authorization-url': 7.1.1
+      '@octokit/request': 9.1.3
+      '@octokit/request-error': 6.1.4
+      '@octokit/types': 13.5.0
     dev: false
 
-  /@octokit/openapi-types@18.0.0:
-    resolution: {integrity: sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==}
+  /@octokit/openapi-types@22.2.0:
+    resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==}
     dev: false
 
-  /@octokit/plugin-paginate-rest@6.1.2(@octokit/core@4.2.1):
-    resolution: {integrity: sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==}
-    engines: {node: '>= 14'}
+  /@octokit/openapi-webhooks-types@8.3.0:
+    resolution: {integrity: sha512-vKLsoR4xQxg4Z+6rU/F65ItTUz/EXbD+j/d4mlq2GW8TsA4Tc8Kdma2JTAAJ5hrKWUQzkR/Esn2fjsqiVRYaQg==}
+    dev: false
+
+  /@octokit/plugin-paginate-graphql@5.2.2(@octokit/core@6.1.2):
+    resolution: {integrity: sha512-7znSVvlNAOJisCqAnjN1FtEziweOHSjPGAuc5W58NeGNAr/ZB57yCsjQbXDlWsVryA7hHQaEQPcBbJYFawlkyg==}
+    engines: {node: '>= 18'}
     peerDependencies:
-      '@octokit/core': '>=4'
+      '@octokit/core': '>=6'
     dependencies:
-      '@octokit/core': 4.2.1
-      '@octokit/tsconfig': 1.0.2
-      '@octokit/types': 9.3.1
+      '@octokit/core': 6.1.2
     dev: false
 
-  /@octokit/plugin-rest-endpoint-methods@7.2.1(@octokit/core@4.2.1):
-    resolution: {integrity: sha512-UmlNrrcF+AXxcxhZslTt1a/8aDxUKH0trrt/mJCxEPrWbW1ZEc+6xxcd5/n0iw3b+Xo8UBJQUKDr71+vNCBpRQ==}
-    engines: {node: '>= 14'}
+  /@octokit/plugin-paginate-rest@11.3.3(@octokit/core@6.1.2):
+    resolution: {integrity: sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==}
+    engines: {node: '>= 18'}
     peerDependencies:
-      '@octokit/core': '>=3'
+      '@octokit/core': '>=6'
     dependencies:
-      '@octokit/core': 4.2.1
-      '@octokit/types': 9.3.1
+      '@octokit/core': 6.1.2
+      '@octokit/types': 13.5.0
     dev: false
 
-  /@octokit/plugin-retry@4.1.6(@octokit/core@4.2.1):
-    resolution: {integrity: sha512-obkYzIgEC75r8+9Pnfiiqy3y/x1bc3QLE5B7qvv9wi9Kj0R5tGQFC6QMBg1154WQ9lAVypuQDGyp3hNpp15gQQ==}
-    engines: {node: '>= 14'}
+  /@octokit/plugin-rest-endpoint-methods@13.2.4(@octokit/core@6.1.2):
+    resolution: {integrity: sha512-gusyAVgTrPiuXOdfqOySMDztQHv6928PQ3E4dqVGEtOvRXAKRbJR4b1zQyniIT9waqaWk/UDaoJ2dyPr7Bk7Iw==}
+    engines: {node: '>= 18'}
     peerDependencies:
-      '@octokit/core': '>=3'
+      '@octokit/core': '>=6'
     dependencies:
-      '@octokit/core': 4.2.1
-      '@octokit/types': 9.3.1
-      bottleneck: 2.19.5
+      '@octokit/core': 6.1.2
+      '@octokit/types': 13.5.0
     dev: false
 
-  /@octokit/plugin-throttling@5.2.3(@octokit/core@4.2.1):
-    resolution: {integrity: sha512-C9CFg9mrf6cugneKiaI841iG8DOv6P5XXkjmiNNut+swePxQ7RWEdAZRp5rJoE1hjsIqiYcKa/ZkOQ+ujPI39Q==}
-    engines: {node: '>= 14'}
+  /@octokit/plugin-retry@7.1.1(@octokit/core@6.1.2):
+    resolution: {integrity: sha512-G9Ue+x2odcb8E1XIPhaFBnTTIrrUDfXN05iFXiqhR+SeeeDMMILcAnysOsxUpEWcQp2e5Ft397FCXTcPkiPkLw==}
+    engines: {node: '>= 18'}
     peerDependencies:
-      '@octokit/core': ^4.0.0
+      '@octokit/core': '>=6'
     dependencies:
-      '@octokit/core': 4.2.1
-      '@octokit/types': 9.3.1
+      '@octokit/core': 6.1.2
+      '@octokit/request-error': 6.1.4
+      '@octokit/types': 13.5.0
       bottleneck: 2.19.5
     dev: false
 
-  /@octokit/request-error@3.0.3:
-    resolution: {integrity: sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==}
-    engines: {node: '>= 14'}
+  /@octokit/plugin-throttling@9.3.1(@octokit/core@6.1.2):
+    resolution: {integrity: sha512-Qd91H4liUBhwLB2h6jZ99bsxoQdhgPk6TdwnClPyTBSDAdviGPceViEgUwj+pcQDmB/rfAXAXK7MTochpHM3yQ==}
+    engines: {node: '>= 18'}
+    peerDependencies:
+      '@octokit/core': ^6.0.0
     dependencies:
-      '@octokit/types': 9.3.1
-      deprecation: 2.3.1
-      once: 1.4.0
+      '@octokit/core': 6.1.2
+      '@octokit/types': 13.5.0
+      bottleneck: 2.19.5
     dev: false
 
-  /@octokit/request@6.2.5:
-    resolution: {integrity: sha512-z83E8UIlPNaJUsXpjD8E0V5o/5f+vJJNbNcBwVZsX3/vC650U41cOkTLjq4PKk9BYonQGOnx7N17gvLyNjgGcQ==}
-    engines: {node: '>= 14'}
+  /@octokit/request-error@6.1.4:
+    resolution: {integrity: sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/endpoint': 7.0.6
-      '@octokit/request-error': 3.0.3
-      '@octokit/types': 9.3.1
-      is-plain-object: 5.0.0
-      node-fetch: 2.6.11
-      universal-user-agent: 6.0.0
-    transitivePeerDependencies:
-      - encoding
-    dev: false
-
-  /@octokit/tsconfig@1.0.2:
-    resolution: {integrity: sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==}
+      '@octokit/types': 13.5.0
     dev: false
 
-  /@octokit/types@9.3.1:
-    resolution: {integrity: sha512-zfJzyXLHC42sWcn2kS+oZ/DRvFZBYCCbfInZtwp1Uopl1qh6pRg4NSP/wFX1xCOpXvEkctiG1sxlSlkZmzvxdw==}
+  /@octokit/request@9.1.3:
+    resolution: {integrity: sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/openapi-types': 18.0.0
+      '@octokit/endpoint': 10.1.1
+      '@octokit/request-error': 6.1.4
+      '@octokit/types': 13.5.0
+      universal-user-agent: 7.0.2
     dev: false
 
-  /@octokit/webhooks-methods@3.0.3:
-    resolution: {integrity: sha512-2vM+DCNTJ5vL62O5LagMru6XnYhV4fJslK+5YUkTa6rWlW2S+Tqs1lF9Wr9OGqHfVwpBj3TeztWfVON/eUoW1Q==}
-    engines: {node: '>= 14'}
+  /@octokit/types@13.5.0:
+    resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==}
+    dependencies:
+      '@octokit/openapi-types': 22.2.0
     dev: false
 
-  /@octokit/webhooks-types@6.11.0:
-    resolution: {integrity: sha512-AanzbulOHljrku1NGfafxdpTCfw2ENaWzH01N2vqQM+cUFbk868Cgh0xylz0JIM9BoKbfI++bdD6EYX0Q/UTEw==}
+  /@octokit/webhooks-methods@5.1.0:
+    resolution: {integrity: sha512-yFZa3UH11VIxYnnoOYCVoJ3q4ChuSOk2IVBBQ0O3xtKX4x9bmKb/1t+Mxixv2iUhzMdOl1qeWJqEhouXXzB3rQ==}
+    engines: {node: '>= 18'}
     dev: false
 
-  /@octokit/webhooks@10.9.1:
-    resolution: {integrity: sha512-5NXU4VfsNOo2VSU/SrLrpPH2Z1ZVDOWFcET4EpnEBX1uh/v8Uz65UVuHIRx5TZiXhnWyRE9AO1PXHa+M/iWwZA==}
-    engines: {node: '>= 14'}
+  /@octokit/webhooks@13.3.0:
+    resolution: {integrity: sha512-TUkJLtI163Bz5+JK0O+zDkQpn4gKwN+BovclUvCj6pI/6RXrFqQvUMRS2M+Rt8Rv0qR3wjoMoOPmpJKeOh0nBg==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/request-error': 3.0.3
-      '@octokit/webhooks-methods': 3.0.3
-      '@octokit/webhooks-types': 6.11.0
-      aggregate-error: 3.1.0
+      '@octokit/openapi-webhooks-types': 8.3.0
+      '@octokit/request-error': 6.1.4
+      '@octokit/webhooks-methods': 5.1.0
     dev: false
 
   /@open-draft/until@1.0.3:
@@ -8486,8 +8465,8 @@ packages:
     resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
     dev: false
 
-  /@prisma/client@5.6.0(prisma@5.6.0):
-    resolution: {integrity: sha512-mUDefQFa1wWqk4+JhKPYq8BdVoFk9NFMBXUI8jAkBfQTtgx8WPx02U2HB/XbAz3GSUJpeJOKJQtNvaAIDs6sug==}
+  /@prisma/client@5.18.0(prisma@5.18.0):
+    resolution: {integrity: sha512-BWivkLh+af1kqC89zCJYkHsRcyWsM8/JHpsDMM76DjP3ZdEquJhXa4IeX+HkWPnwJ5FanxEJFZZDTWiDs/Kvyw==}
     engines: {node: '>=16.13'}
     requiresBuild: true
     peerDependencies:
@@ -8496,17 +8475,35 @@ packages:
       prisma:
         optional: true
     dependencies:
-      '@prisma/engines-version': 5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee
-      prisma: 5.6.0
+      prisma: 5.18.0
     dev: false
 
-  /@prisma/engines-version@5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee:
-    resolution: {integrity: sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw==}
-    dev: false
+  /@prisma/debug@5.18.0:
+    resolution: {integrity: sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw==}
 
-  /@prisma/engines@5.6.0:
-    resolution: {integrity: sha512-Mt2q+GNJpU2vFn6kif24oRSBQv1KOkYaterQsi0k2/lA+dLvhRX6Lm26gon6PYHwUM8/h8KRgXIUMU0PCLB6bw==}
+  /@prisma/engines-version@5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169:
+    resolution: {integrity: sha512-a/+LpJj8vYU3nmtkg+N3X51ddbt35yYrRe8wqHTJtYQt7l1f8kjIBcCs6sHJvodW/EK5XGvboOiwm47fmNrbgg==}
+
+  /@prisma/engines@5.18.0:
+    resolution: {integrity: sha512-ofmpGLeJ2q2P0wa/XaEgTnX/IsLnvSp/gZts0zjgLNdBhfuj2lowOOPmDcfKljLQUXMvAek3lw5T01kHmCG8rg==}
     requiresBuild: true
+    dependencies:
+      '@prisma/debug': 5.18.0
+      '@prisma/engines-version': 5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169
+      '@prisma/fetch-engine': 5.18.0
+      '@prisma/get-platform': 5.18.0
+
+  /@prisma/fetch-engine@5.18.0:
+    resolution: {integrity: sha512-I/3u0x2n31rGaAuBRx2YK4eB7R/1zCuayo2DGwSpGyrJWsZesrV7QVw7ND0/Suxeo/vLkJ5OwuBqHoCxvTHpOg==}
+    dependencies:
+      '@prisma/debug': 5.18.0
+      '@prisma/engines-version': 5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169
+      '@prisma/get-platform': 5.18.0
+
+  /@prisma/get-platform@5.18.0:
+    resolution: {integrity: sha512-Tk+m7+uhqcKDgnMnFN0lRiH7Ewea0OEsZZs9pqXa7i3+7svS3FSCqDBCaM9x5fmhhkufiG0BtunJVDka+46DlA==}
+    dependencies:
+      '@prisma/debug': 5.18.0
 
   /@radix-ui/number@1.0.1:
     resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
@@ -11906,10 +11903,6 @@ packages:
       '@types/node': 20.3.1
     dev: true
 
-  /@types/btoa-lite@1.0.0:
-    resolution: {integrity: sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg==}
-    dev: false
-
   /@types/connect-history-api-fallback@1.5.4:
     resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==}
     dependencies:
@@ -12100,7 +12093,7 @@ packages:
   /@types/graceful-fs@4.1.6:
     resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==}
     dependencies:
-      '@types/node': 20.3.1
+      '@types/node': 20.14.14
     dev: true
 
   /@types/hast@2.3.8:
@@ -12180,12 +12173,6 @@ packages:
   /@types/json-schema@7.0.15:
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
-  /@types/jsonwebtoken@9.0.2:
-    resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==}
-    dependencies:
-      '@types/node': 18.16.17
-    dev: false
-
   /@types/katex@0.16.7:
     resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
     dev: false
@@ -12282,9 +12269,8 @@ packages:
   /@types/node-fetch@2.6.4:
     resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==}
     dependencies:
-      '@types/node': 18.16.17
+      '@types/node': 20.14.14
       form-data: 3.0.1
-    dev: true
 
   /@types/node-forge@1.3.11:
     resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
@@ -12307,7 +12293,6 @@ packages:
     resolution: {integrity: sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==}
     dependencies:
       undici-types: 5.26.5
-    dev: true
 
   /@types/node@20.3.1:
     resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==}
@@ -12940,12 +12925,20 @@ packages:
       - supports-color
     dev: true
 
+  /agentkeepalive@4.5.0:
+    resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
+    engines: {node: '>= 8.0.0'}
+    dependencies:
+      humanize-ms: 1.2.1
+    dev: false
+
   /aggregate-error@3.1.0:
     resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
     engines: {node: '>=8'}
     dependencies:
       clean-stack: 2.2.0
       indent-string: 4.0.0
+    dev: true
 
   /ajv-formats@2.1.1(ajv@8.12.0):
     resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
@@ -13229,7 +13222,6 @@ packages:
 
   /asynckit@0.4.0:
     resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
-    dev: true
 
   /autoprefixer@10.4.14(postcss@8.4.24):
     resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
@@ -13428,8 +13420,8 @@ packages:
     resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==}
     dev: true
 
-  /before-after-hook@2.2.3:
-    resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
+  /before-after-hook@3.0.2:
+    resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==}
     dev: false
 
   /better-opn@2.1.1:
@@ -13608,18 +13600,10 @@ packages:
       node-int64: 0.4.0
     dev: true
 
-  /btoa-lite@1.0.0:
-    resolution: {integrity: sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==}
-    dev: false
-
   /buffer-crc32@0.2.13:
     resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
     dev: true
 
-  /buffer-equal-constant-time@1.0.1:
-    resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
-    dev: false
-
   /buffer-from@1.1.2:
     resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
 
@@ -13964,6 +13948,7 @@ packages:
   /clean-stack@2.2.0:
     resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
     engines: {node: '>=6'}
+    dev: true
 
   /clear@0.1.0:
     resolution: {integrity: sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==}
@@ -14156,7 +14141,6 @@ packages:
     engines: {node: '>= 0.8'}
     dependencies:
       delayed-stream: 1.0.0
-    dev: true
 
   /comma-separated-tokens@2.0.3:
     resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@@ -15111,7 +15095,6 @@ packages:
   /delayed-stream@1.0.0:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
-    dev: true
 
   /delegates@1.0.0:
     resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
@@ -15136,10 +15119,6 @@ packages:
     engines: {node: '>=4'}
     dev: true
 
-  /deprecation@2.3.1:
-    resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
-    dev: false
-
   /dequal@2.0.3:
     resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
     engines: {node: '>=6'}
@@ -15361,12 +15340,6 @@ packages:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
     dev: true
 
-  /ecdsa-sig-formatter@1.0.11:
-    resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
-    dependencies:
-      safe-buffer: 5.2.1
-    dev: false
-
   /editorconfig@0.15.3:
     resolution: {integrity: sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==}
     hasBin: true
@@ -16467,6 +16440,10 @@ packages:
       signal-exit: 4.1.0
     dev: true
 
+  /form-data-encoder@1.7.2:
+    resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
+    dev: false
+
   /form-data@3.0.1:
     resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
     engines: {node: '>= 6'}
@@ -16474,7 +16451,6 @@ packages:
       asynckit: 0.4.0
       combined-stream: 1.0.8
       mime-types: 2.1.35
-    dev: true
 
   /form-data@4.0.0:
     resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
@@ -16485,6 +16461,14 @@ packages:
       mime-types: 2.1.35
     dev: true
 
+  /formdata-node@4.4.1:
+    resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
+    engines: {node: '>= 12.20'}
+    dependencies:
+      node-domexception: 1.0.0
+      web-streams-polyfill: 4.0.0-beta.3
+    dev: false
+
   /formdata-polyfill@4.0.10:
     resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
     engines: {node: '>=12.20.0'}
@@ -16536,10 +16520,6 @@ packages:
     engines: {node: '>= 0.6'}
     dev: true
 
-  /fromentries@1.3.2:
-    resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==}
-    dev: false
-
   /fs-constants@1.0.0:
     resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
     dev: true
@@ -17466,6 +17446,12 @@ packages:
     engines: {node: '>=16.17.0'}
     dev: true
 
+  /humanize-ms@1.2.1:
+    resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+    dependencies:
+      ms: 2.1.3
+    dev: false
+
   /hyperdyperid@1.2.0:
     resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==}
     engines: {node: '>=10.18'}
@@ -17563,6 +17549,7 @@ packages:
   /indent-string@4.0.0:
     resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
     engines: {node: '>=8'}
+    dev: true
 
   /inflight@1.0.6:
     resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
@@ -17959,11 +17946,6 @@ packages:
       isobject: 3.0.1
     dev: true
 
-  /is-plain-object@5.0.0:
-    resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
-    engines: {node: '>=0.10.0'}
-    dev: false
-
   /is-potential-custom-element-name@1.0.1:
     resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
     dev: true
@@ -18357,7 +18339,7 @@ packages:
     resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
     engines: {node: '>= 10.13.0'}
     dependencies:
-      '@types/node': 20.3.1
+      '@types/node': 18.16.17
       merge-stream: 2.0.0
       supports-color: 8.1.1
 
@@ -18365,7 +18347,7 @@ packages:
     resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
-      '@types/node': 20.3.1
+      '@types/node': 20.14.14
       jest-util: 29.5.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
@@ -18598,31 +18580,6 @@ packages:
     engines: {'0': node >= 0.2.0}
     dev: true
 
-  /jsonwebtoken@9.0.0:
-    resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==}
-    engines: {node: '>=12', npm: '>=6'}
-    dependencies:
-      jws: 3.2.2
-      lodash: 4.17.21
-      ms: 2.1.3
-      semver: 7.6.0
-    dev: false
-
-  /jwa@1.4.1:
-    resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
-    dependencies:
-      buffer-equal-constant-time: 1.0.1
-      ecdsa-sig-formatter: 1.0.11
-      safe-buffer: 5.2.1
-    dev: false
-
-  /jws@3.2.2:
-    resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
-    dependencies:
-      jwa: 1.4.1
-      safe-buffer: 5.2.1
-    dev: false
-
   /karma-chrome-launcher@3.1.0:
     resolution: {integrity: sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==}
     dependencies:
@@ -19119,7 +19076,6 @@ packages:
 
   /lru-cache@10.4.3:
     resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
-    dev: true
 
   /lru-cache@4.1.5:
     resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
@@ -19141,6 +19097,7 @@ packages:
   /lru-cache@9.1.2:
     resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==}
     engines: {node: 14 || >=16.14}
+    dev: true
 
   /lucide-react@0.320.0(react@18.2.0):
     resolution: {integrity: sha512-HuMmfmFiWwctDkN27wklKVZr8UpwP2TTekLZ3xiLCEjx/STG1k0KLWMbBfIJ/lnNJQDfSjztDZSVU316xA+AQg==}
@@ -19647,6 +19604,13 @@ packages:
       tslib: 2.6.3
     dev: true
 
+  /memoize@10.0.0:
+    resolution: {integrity: sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==}
+    engines: {node: '>=18'}
+    dependencies:
+      mimic-function: 5.0.1
+    dev: false
+
   /memoizerific@1.11.3:
     resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
     dependencies:
@@ -20378,7 +20342,6 @@ packages:
   /mimic-function@5.0.1:
     resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
     engines: {node: '>=18'}
-    dev: true
 
   /min-indent@1.0.1:
     resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
@@ -21479,20 +21442,20 @@ packages:
     resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
     dev: true
 
-  /octokit@2.0.19:
-    resolution: {integrity: sha512-hSloK4MK78QGbAuBrtIir0bsxMoRVZE5CkwKSbSRH9lqv2hx9EwhCxtPqEF+BtHqLXkXdfUaGkJMyMBotYno+A==}
-    engines: {node: '>= 14'}
+  /octokit@4.0.2:
+    resolution: {integrity: sha512-wbqF4uc1YbcldtiBFfkSnquHtECEIpYD78YUXI6ri1Im5OO2NLo6ZVpRdbJpdnpZ05zMrVPssNiEo6JQtea+Qg==}
+    engines: {node: '>= 18'}
     dependencies:
-      '@octokit/app': 13.1.5
-      '@octokit/core': 4.2.1
-      '@octokit/oauth-app': 4.2.2
-      '@octokit/plugin-paginate-rest': 6.1.2(@octokit/core@4.2.1)
-      '@octokit/plugin-rest-endpoint-methods': 7.2.1(@octokit/core@4.2.1)
-      '@octokit/plugin-retry': 4.1.6(@octokit/core@4.2.1)
-      '@octokit/plugin-throttling': 5.2.3(@octokit/core@4.2.1)
-      '@octokit/types': 9.3.1
-    transitivePeerDependencies:
-      - encoding
+      '@octokit/app': 15.1.0
+      '@octokit/core': 6.1.2
+      '@octokit/oauth-app': 7.1.3
+      '@octokit/plugin-paginate-graphql': 5.2.2(@octokit/core@6.1.2)
+      '@octokit/plugin-paginate-rest': 11.3.3(@octokit/core@6.1.2)
+      '@octokit/plugin-rest-endpoint-methods': 13.2.4(@octokit/core@6.1.2)
+      '@octokit/plugin-retry': 7.1.1(@octokit/core@6.1.2)
+      '@octokit/plugin-throttling': 9.3.1(@octokit/core@6.1.2)
+      '@octokit/request-error': 6.1.4
+      '@octokit/types': 13.5.0
     dev: false
 
   /oidc-token-hash@5.0.3:
@@ -21575,6 +21538,27 @@ packages:
       is-wsl: 2.2.0
     dev: true
 
+  /openai@4.56.0(zod@3.21.4):
+    resolution: {integrity: sha512-zcag97+3bG890MNNa0DQD9dGmmTWL8unJdNkulZzWRXrl+QeD+YkBI4H58rJcwErxqGK6a0jVPZ4ReJjhDGcmw==}
+    hasBin: true
+    peerDependencies:
+      zod: ^3.23.8
+    peerDependenciesMeta:
+      zod:
+        optional: true
+    dependencies:
+      '@types/node': 18.16.17
+      '@types/node-fetch': 2.6.4
+      abort-controller: 3.0.0
+      agentkeepalive: 4.5.0
+      form-data-encoder: 1.7.2
+      formdata-node: 4.4.1
+      node-fetch: 2.6.11
+      zod: 3.21.4
+    transitivePeerDependencies:
+      - encoding
+    dev: false
+
   /opener@1.5.2:
     resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
     hasBin: true
@@ -22319,7 +22303,6 @@ packages:
     resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
     engines: {node: '>=10.13.0'}
     hasBin: true
-    dev: true
 
   /prettier@3.0.0:
     resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==}
@@ -22382,13 +22365,13 @@ packages:
     resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
     dev: true
 
-  /prisma@5.6.0:
-    resolution: {integrity: sha512-EEaccku4ZGshdr2cthYHhf7iyvCcXqwJDvnoQRAJg5ge2Tzpv0e2BaMCp+CbbDUwoVTzwgOap9Zp+d4jFa2O9A==}
+  /prisma@5.18.0:
+    resolution: {integrity: sha512-+TrSIxZsh64OPOmaSgVPH7ALL9dfU0jceYaMJXsNrTkFHO7/3RANi5K2ZiPB1De9+KDxCWn7jvRq8y8pvk+o9g==}
     engines: {node: '>=16.13'}
     hasBin: true
     requiresBuild: true
     dependencies:
-      '@prisma/engines': 5.6.0
+      '@prisma/engines': 5.18.0
 
   /proc-log@4.2.0:
     resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==}
@@ -25261,6 +25244,7 @@ packages:
 
   /tslib@2.6.3:
     resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
+    requiresBuild: true
 
   /tsscmp@1.0.6:
     resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
@@ -25525,7 +25509,6 @@ packages:
 
   /undici-types@5.26.5:
     resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
-    dev: true
 
   /undici-types@6.13.0:
     resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==}
@@ -25768,15 +25751,12 @@ packages:
       unist-util-visit-parents: 6.0.1
     dev: false
 
-  /universal-github-app-jwt@1.1.1:
-    resolution: {integrity: sha512-G33RTLrIBMFmlDV4u4CBF7dh71eWwykck4XgaxaIVeZKOYZRAAxvcGMRFTUclVY6xoUPQvO4Ne5wKGxYm/Yy9w==}
-    dependencies:
-      '@types/jsonwebtoken': 9.0.2
-      jsonwebtoken: 9.0.0
+  /universal-github-app-jwt@2.2.0:
+    resolution: {integrity: sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==}
     dev: false
 
-  /universal-user-agent@6.0.0:
-    resolution: {integrity: sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==}
+  /universal-user-agent@7.0.2:
+    resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==}
     dev: false
 
   /universalify@0.1.2:
@@ -26542,6 +26522,11 @@ packages:
     resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
     engines: {node: '>= 8'}
 
+  /web-streams-polyfill@4.0.0-beta.3:
+    resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
+    engines: {node: '>= 14'}
+    dev: false
+
   /web-worker@1.2.0:
     resolution: {integrity: sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==}
     dev: false