Feat: logs ui

This commit is contained in:
Harshith Mullapudi 2025-07-15 11:46:41 +05:30
parent 2c3466d378
commit 5dca80b4de
19 changed files with 1726 additions and 186 deletions

View File

@ -0,0 +1,228 @@
import { useState } from "react";
import { Check, ChevronsUpDown, Filter, X } from "lucide-react";
import { Button } from "~/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "~/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { Badge } from "~/components/ui/badge";
import { cn } from "~/lib/utils";
interface LogsFiltersProps {
availableSources: Array<{ name: string; slug: string }>;
selectedSource?: string;
selectedStatus?: string;
onSourceChange: (source?: string) => void;
onStatusChange: (status?: string) => void;
}
const statusOptions = [
{ value: "PENDING", label: "Pending" },
{ value: "PROCESSING", label: "Processing" },
{ value: "COMPLETED", label: "Completed" },
{ value: "FAILED", label: "Failed" },
{ value: "CANCELLED", label: "Cancelled" },
];
export function LogsFilters({
availableSources,
selectedSource,
selectedStatus,
onSourceChange,
onStatusChange,
}: LogsFiltersProps) {
const [sourceOpen, setSourceOpen] = useState(false);
const [statusOpen, setStatusOpen] = useState(false);
const selectedSourceName = availableSources.find(
(s) => s.slug === selectedSource,
)?.name;
const selectedStatusLabel = statusOptions.find(
(s) => s.value === selectedStatus,
)?.label;
const clearFilters = () => {
onSourceChange(undefined);
onStatusChange(undefined);
};
const hasFilters = selectedSource || selectedStatus;
return (
<div className="mb-4 flex items-center gap-2">
<div className="flex items-center gap-2">
<Filter className="text-muted-foreground h-4 w-4" />
<span className="text-sm font-medium">Filters:</span>
</div>
{/* Source Filter */}
<Popover open={sourceOpen} onOpenChange={setSourceOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={sourceOpen}
className="w-[200px] justify-between"
>
{selectedSourceName || "Select source..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search sources..." />
<CommandList>
<CommandEmpty>No sources found.</CommandEmpty>
<CommandGroup>
<CommandItem
value=""
onSelect={() => {
onSourceChange(undefined);
setSourceOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
!selectedSource ? "opacity-100" : "opacity-0",
)}
/>
All sources
</CommandItem>
{availableSources.map((source) => (
<CommandItem
key={source.slug}
value={source.slug}
onSelect={() => {
onSourceChange(
source.slug === selectedSource
? undefined
: source.slug,
);
setSourceOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedSource === source.slug
? "opacity-100"
: "opacity-0",
)}
/>
{source.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* Status Filter */}
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={statusOpen}
className="w-[200px] justify-between"
>
{selectedStatusLabel || "Select status..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search status..." />
<CommandList>
<CommandEmpty>No status found.</CommandEmpty>
<CommandGroup>
<CommandItem
value=""
onSelect={() => {
onStatusChange(undefined);
setStatusOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
!selectedStatus ? "opacity-100" : "opacity-0",
)}
/>
All statuses
</CommandItem>
{statusOptions.map((status) => (
<CommandItem
key={status.value}
value={status.value}
onSelect={() => {
onStatusChange(
status.value === selectedStatus
? undefined
: status.value,
);
setStatusOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedStatus === status.value
? "opacity-100"
: "opacity-0",
)}
/>
{status.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* Active Filters */}
{hasFilters && (
<div className="flex items-center gap-2">
{selectedSource && (
<Badge variant="secondary" className="gap-1">
{selectedSourceName}
<X
className="hover:text-destructive h-3 w-3 cursor-pointer"
onClick={() => onSourceChange(undefined)}
/>
</Badge>
)}
{selectedStatus && (
<Badge variant="secondary" className="gap-1">
{selectedStatusLabel}
<X
className="hover:text-destructive h-3 w-3 cursor-pointer"
onClick={() => onStatusChange(undefined)}
/>
</Badge>
)}
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="h-6 px-2 text-xs"
>
Clear all
</Button>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,198 @@
import { useEffect, useRef, useState } from "react";
import { List, InfiniteLoader, WindowScroller } from "react-virtualized";
import { LogItem } from "~/hooks/use-logs";
import { Badge } from "~/components/ui/badge";
import { Card, CardContent } from "~/components/ui/card";
import { AlertCircle, CheckCircle, Clock, XCircle } from "lucide-react";
import { cn } from "~/lib/utils";
interface VirtualLogsListProps {
logs: LogItem[];
hasMore: boolean;
loadMore: () => void;
isLoading: boolean;
height?: number;
}
const ITEM_HEIGHT = 120;
interface LogItemRendererProps {
index: number;
key: string;
style: React.CSSProperties;
}
function LogItemRenderer(props: LogItemRendererProps, logs: LogItem[]) {
const { index, key, style } = props;
const log = logs[index];
if (!log) {
return (
<div key={key} style={style} className="p-4">
<div className="h-24 animate-pulse rounded bg-gray-200" />
</div>
);
}
const getStatusIcon = (status: string) => {
switch (status) {
case "PROCESSING":
return <Clock className="h-4 w-4 text-blue-500" />;
case "PENDING":
return <Clock className="h-4 w-4 text-yellow-500" />;
case "COMPLETED":
return <CheckCircle className="h-4 w-4 text-green-500" />;
case "FAILED":
return <XCircle className="h-4 w-4 text-red-500" />;
case "CANCELLED":
return <XCircle className="h-4 w-4 text-gray-500" />;
default:
return <AlertCircle className="h-4 w-4 text-gray-500" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case "PROCESSING":
return "bg-blue-100 text-blue-800";
case "PENDING":
return "bg-yellow-100 text-yellow-800";
case "COMPLETED":
return "bg-green-100 text-green-800";
case "FAILED":
return "bg-red-100 text-red-800";
case "CANCELLED":
return "bg-gray-100 text-gray-800";
default:
return "bg-gray-100 text-gray-800";
}
};
return (
<div key={key} style={style} className="p-2">
<Card className="h-full">
<CardContent className="p-4">
<div className="mb-2 flex items-start justify-between">
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
{log.source}
</Badge>
<div className="flex items-center gap-1">
{getStatusIcon(log.status)}
<Badge className={cn("text-xs", getStatusColor(log.status))}>
{log.status.toLowerCase()}
</Badge>
</div>
</div>
<div className="text-muted-foreground text-xs">
{new Date(log.time).toLocaleString()}
</div>
</div>
<div className="mb-2">
<p className="line-clamp-2 text-sm text-gray-700">
{log.ingestText}
</p>
</div>
<div className="text-muted-foreground flex items-center justify-between text-xs">
<div className="flex items-center gap-4">
{log.sourceURL && (
<a
href={log.sourceURL}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline hover:text-blue-800"
>
Source URL
</a>
)}
{log.processedAt && (
<span>
Processed: {new Date(log.processedAt).toLocaleString()}
</span>
)}
</div>
{log.error && (
<div className="flex items-center gap-1 text-red-600">
<AlertCircle className="h-3 w-3" />
<span className="max-w-[200px] truncate" title={log.error}>
{log.error}
</span>
</div>
)}
</div>
</CardContent>
</Card>
</div>
);
}
export function VirtualLogsList({
logs,
hasMore,
loadMore,
isLoading,
height = 600,
}: VirtualLogsListProps) {
const [containerHeight, setContainerHeight] = useState(height);
useEffect(() => {
const updateHeight = () => {
const availableHeight = window.innerHeight - 300; // Account for header, filters, etc.
setContainerHeight(Math.min(availableHeight, height));
};
updateHeight();
window.addEventListener("resize", updateHeight);
return () => window.removeEventListener("resize", updateHeight);
}, [height]);
const isRowLoaded = ({ index }: { index: number }) => {
return !!logs[index];
};
const loadMoreRows = async () => {
if (hasMore) {
return loadMore();
}
return false;
};
const rowRenderer = (props: LogItemRendererProps) => {
return LogItemRenderer(props, logs);
};
const itemCount = hasMore ? logs.length + 1 : logs.length;
return (
<div className="overflow-hidden rounded-lg border">
<InfiniteLoader
isRowLoaded={isRowLoaded}
loadMoreRows={loadMoreRows}
rowCount={itemCount}
threshold={5}
>
{({ onRowsRendered, registerChild }) => (
<List
ref={registerChild}
height={containerHeight}
rowCount={itemCount}
rowHeight={ITEM_HEIGHT}
onRowsRendered={onRowsRendered}
rowRenderer={rowRenderer}
className="scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100"
/>
)}
</InfiniteLoader>
{isLoading && (
<div className="text-muted-foreground p-4 text-center text-sm">
Loading more logs...
</div>
)}
</div>
);
}

View File

@ -28,8 +28,8 @@ const data = {
icon: Network,
},
{
title: "Activity",
url: "/home/activity",
title: "Logs",
url: "/home/logs/all",
icon: Activity,
},
{

View File

@ -0,0 +1,179 @@
import { type DialogProps } from "@radix-ui/react-dialog";
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { Command as CommandPrimitive } from "cmdk";
import React from "react";
import { Dialog, DialogContent } from "./dialog";
import { cn } from "../../lib/utils";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md font-sans",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {
commandProps?: React.ComponentPropsWithoutRef<typeof CommandPrimitive>;
}
interface CommandInputProps
extends React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> {
icon?: boolean;
containerClassName?: string;
}
const CommandDialog = ({
children,
commandProps,
...props
}: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className={cn("overflow-hidden p-0 font-sans")}>
<Command
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
{...commandProps}
>
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
CommandInputProps
>(({ className, icon, containerClassName, ...props }, ref) => (
<div
className={cn(
"border-border flex items-center border-b px-3",
containerClassName,
)}
cmdk-input-wrapper=""
>
{icon && (
<MagnifyingGlassIcon className="mr-2 h-5 w-5 shrink-0 opacity-50" />
)}
<CommandPrimitive.Input
ref={ref}
className={cn(
"placeholder:text-muted-foreground flex h-8 w-full rounded-md bg-transparent py-3 outline-none disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn(
"command-list max-h-[500px] overflow-x-hidden overflow-y-auto",
className,
)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"command-item aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default items-center rounded-sm px-2 py-1 outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@ -7,7 +7,7 @@ const PAGE_TITLES: Record<string, string> = {
"/home/dashboard": "Memory graph",
"/home/conversation": "Conversation",
"/home/integrations": "Integrations",
"/home/activity": "Activity",
"/home/logs": "Logs",
};
function getHeaderTitle(pathname: string): string {
@ -26,12 +26,17 @@ function isConversationDetail(pathname: string): boolean {
return /^\/home\/conversation\/[^/]+$/.test(pathname);
}
function isIntegrationsPage(pathname: string): boolean {
return pathname === "/home/integrations";
}
export function SiteHeader() {
const location = useLocation();
const navigate = useNavigate();
const title = getHeaderTitle(location.pathname);
const showNewConversationButton = isConversationDetail(location.pathname);
const showRequestIntegrationButton = isIntegrationsPage(location.pathname);
return (
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b border-gray-300 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
@ -51,6 +56,16 @@ export function SiteHeader() {
New conversation
</Button>
)}
{showRequestIntegrationButton && (
<Button
onClick={() => window.open("https://github.com/redplanethq/core/issues/new", "_blank")}
variant="secondary"
className="gap-2"
>
<Plus size={14} />
Request New Integration
</Button>
)}
</div>
</div>
</header>

View File

@ -263,7 +263,8 @@ function SidebarTrigger({
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
className={cn("size-8", className)}
size="xs"
className={cn("size-6 rounded-md", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();

View File

@ -36,7 +36,7 @@ export function useIngestionStatus() {
clearInterval(interval);
setIsPolling(false);
};
}, [fetcher]);
}, []); // Remove fetcher from dependencies to prevent infinite loop
return {
data: fetcher.data,

View File

@ -0,0 +1,108 @@
import { useEffect, useState, useCallback } from "react";
import { useFetcher } from "@remix-run/react";
export interface LogItem {
id: string;
source: string;
ingestText: string;
time: string;
processedAt?: string;
status: "PENDING" | "PROCESSING" | "COMPLETED" | "FAILED" | "CANCELLED";
error?: string;
sourceURL?: string;
integrationSlug?: string;
activityId?: string;
}
export interface LogsResponse {
logs: LogItem[];
totalCount: number;
page: number;
limit: number;
hasMore: boolean;
availableSources: Array<{ name: string; slug: string }>;
}
export interface UseLogsOptions {
endpoint: string; // '/api/v1/logs/all' or '/api/v1/logs/activity'
source?: string;
status?: string;
}
export function useLogs({ endpoint, source, status }: UseLogsOptions) {
const fetcher = useFetcher<LogsResponse>();
const [logs, setLogs] = useState<LogItem[]>([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [availableSources, setAvailableSources] = useState<Array<{ name: string; slug: string }>>([]);
const [isInitialLoad, setIsInitialLoad] = useState(true);
const buildUrl = useCallback((pageNum: number) => {
const params = new URLSearchParams();
params.set('page', pageNum.toString());
params.set('limit', '20');
if (source) params.set('source', source);
if (status) params.set('status', status);
return `${endpoint}?${params.toString()}`;
}, [endpoint, source, status]);
const loadMore = useCallback(() => {
if (fetcher.state === 'idle' && hasMore) {
fetcher.load(buildUrl(page + 1));
}
}, [hasMore, page, buildUrl]);
const reset = useCallback(() => {
setLogs([]);
setPage(1);
setHasMore(true);
setIsInitialLoad(true);
fetcher.load(buildUrl(1));
}, [buildUrl]);
// Effect to handle fetcher data
useEffect(() => {
if (fetcher.data) {
const { logs: newLogs, hasMore: newHasMore, page: currentPage, availableSources: sources } = fetcher.data;
if (currentPage === 1) {
// First page or reset
setLogs(newLogs);
setIsInitialLoad(false);
} else {
// Append to existing logs
setLogs(prev => [...prev, ...newLogs]);
}
setHasMore(newHasMore);
setPage(currentPage);
setAvailableSources(sources);
}
}, [fetcher.data]);
// Effect to reset when filters change
useEffect(() => {
setLogs([]);
setPage(1);
setHasMore(true);
setIsInitialLoad(true);
fetcher.load(buildUrl(1));
}, [source, status, buildUrl]); // Inline reset logic to avoid dependency issues
// Initial load
useEffect(() => {
if (isInitialLoad) {
fetcher.load(buildUrl(1));
}
}, [isInitialLoad, buildUrl]);
return {
logs,
hasMore,
loadMore,
reset,
availableSources,
isLoading: fetcher.state === 'loading',
isInitialLoad,
};
}

View File

@ -16,6 +16,7 @@ export const IngestBodyRequest = z.object({
export const addToQueue = async (
body: z.infer<typeof IngestBodyRequest>,
userId: string,
activityId?: string,
) => {
const user = await prisma.user.findFirst({
where: {
@ -39,6 +40,7 @@ export const addToQueue = async (
status: IngestionStatus.PENDING,
priority: 1,
workspaceId: user.Workspace.id,
activityId,
},
});

View File

@ -40,7 +40,6 @@ const { action, loader } = createActionApiRoute(
throw new Error("User not found");
}
// Create the activity record
const activity = await prisma.activity.create({
data: {
@ -64,7 +63,11 @@ const { action, loader } = createActionApiRoute(
},
};
const queueResponse = await addToQueue(ingestData, authentication.userId);
const queueResponse = await addToQueue(
ingestData,
authentication.userId,
activity.id,
);
logger.log("Activity created and queued for ingestion", {
activityId: activity.id,
@ -90,4 +93,4 @@ const { action, loader } = createActionApiRoute(
},
);
export { action, loader };
export { action, loader };

View File

@ -0,0 +1,130 @@
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
export async function loader({ request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "20");
const source = url.searchParams.get("source");
const status = url.searchParams.get("status");
const skip = (page - 1) * limit;
const user = await prisma.user.findUnique({
where: { id: userId },
include: { Workspace: true },
});
if (!user?.Workspace) {
throw new Response("Workspace not found", { status: 404 });
}
// Build where clause for filtering - only items with activityId
const whereClause: any = {
workspaceId: user.Workspace.id,
activityId: {
not: null,
},
};
if (status) {
whereClause.status = status;
}
// If source filter is provided, we need to filter by integration source
if (source) {
whereClause.activity = {
integrationAccount: {
integrationDefinition: {
slug: source,
},
},
};
}
const [logs, totalCount] = await Promise.all([
prisma.ingestionQueue.findMany({
where: whereClause,
include: {
activity: {
include: {
integrationAccount: {
include: {
integrationDefinition: {
select: {
name: true,
slug: true,
},
},
},
},
},
},
},
orderBy: {
createdAt: "desc",
},
skip,
take: limit,
}),
prisma.ingestionQueue.count({
where: whereClause,
}),
]);
// Get available sources for filtering (only those with activities)
const availableSources = await prisma.integrationDefinitionV2.findMany({
where: {
IntegrationAccount: {
some: {
workspaceId: user.Workspace.id,
Activity: {
some: {
IngestionQueue: {
some: {
activityId: {
not: null,
},
},
},
},
},
},
},
},
select: {
name: true,
slug: true,
},
});
// Format the response
const formattedLogs = logs.map((log) => ({
id: log.id,
source: log.activity?.integrationAccount?.integrationDefinition?.name ||
(log.data as any)?.source ||
'Unknown',
ingestText: log.activity?.text ||
(log.data as any)?.episodeBody ||
(log.data as any)?.text ||
'No content',
time: log.createdAt,
processedAt: log.processedAt,
status: log.status,
error: log.error,
sourceURL: log.activity?.sourceURL,
integrationSlug: log.activity?.integrationAccount?.integrationDefinition?.slug,
activityId: log.activityId,
}));
return json({
logs: formattedLogs,
totalCount,
page,
limit,
hasMore: skip + logs.length < totalCount,
availableSources,
});
}

View File

@ -0,0 +1,137 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
/**
* Optimizations:
* - Use `findMany` with `select` instead of `include` to fetch only required fields.
* - Use `count` with the same where clause, but only after fetching logs (to avoid unnecessary count if no logs).
* - Use a single query for availableSources with minimal fields.
* - Avoid unnecessary object spreading and type casting.
* - Minimize nested object traversal in mapping.
*/
export async function loader({ request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "20");
const source = url.searchParams.get("source");
const status = url.searchParams.get("status");
const skip = (page - 1) * limit;
// Get user and workspace in one query
const user = await prisma.user.findUnique({
where: { id: userId },
select: { Workspace: { select: { id: true } } },
});
if (!user?.Workspace) {
throw new Response("Workspace not found", { status: 404 });
}
// Build where clause for filtering
const whereClause: any = {
workspaceId: user.Workspace.id,
};
if (status) {
whereClause.status = status;
}
// If source filter is provided, filter by integration source
if (source) {
whereClause.activity = {
integrationAccount: {
integrationDefinition: {
slug: source,
},
},
};
}
// Use select to fetch only required fields for logs
const [logs, totalCount, availableSources] = await Promise.all([
prisma.ingestionQueue.findMany({
where: whereClause,
select: {
id: true,
createdAt: true,
processedAt: true,
status: true,
error: true,
data: true,
activity: {
select: {
text: true,
sourceURL: true,
integrationAccount: {
select: {
integrationDefinition: {
select: {
name: true,
slug: true,
},
},
},
},
},
},
},
orderBy: {
createdAt: "desc",
},
skip,
take: limit,
}),
prisma.ingestionQueue.count({
where: whereClause,
}),
prisma.integrationDefinitionV2.findMany({
where: {
IntegrationAccount: {
some: {
workspaceId: user.Workspace.id,
},
},
},
select: {
name: true,
slug: true,
},
}),
]);
// Format the response
const formattedLogs = logs.map((log) => {
const integrationDef =
log.activity?.integrationAccount?.integrationDefinition;
const logData = log.data as any;
return {
id: log.id,
source: integrationDef?.name || logData?.source || "Unknown",
ingestText:
log.activity?.text ||
logData?.episodeBody ||
logData?.text ||
"No content",
time: log.createdAt,
processedAt: log.processedAt,
status: log.status,
error: log.error,
sourceURL: log.activity?.sourceURL,
integrationSlug: integrationDef?.slug,
};
});
return json({
logs: formattedLogs,
totalCount,
page,
limit,
hasMore: skip + logs.length < totalCount,
availableSources,
});
}

View File

@ -6,19 +6,33 @@ import { requireUserId, requireWorkpace } from "~/services/session.server";
import { getIntegrationDefinitions } from "~/services/integrationDefinition.server";
import { getIntegrationAccounts } from "~/services/integrationAccount.server";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { Input } from "~/components/ui/input";
import { FormButtons } from "~/components/ui/FormButtons";
import { Plus, Search } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
// Loader to fetch integration definitions and existing accounts
export async function loader({ request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const workspace = await requireWorkpace(request);
const [integrationDefinitions, integrationAccounts] = await Promise.all([
getIntegrationDefinitions(workspace.id),
getIntegrationAccounts(userId),
@ -32,46 +46,26 @@ export async function loader({ request }: LoaderFunctionArgs) {
}
export default function Integrations() {
const { integrationDefinitions, integrationAccounts, userId } = useLoaderData<typeof loader>();
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const { integrationDefinitions, integrationAccounts, userId } =
useLoaderData<typeof loader>();
const [selectedIntegration, setSelectedIntegration] = useState<any>(null);
const [apiKey, setApiKey] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
// Extract categories from integration definitions
const categories = Array.from(
new Set(integrationDefinitions.map((integration) => {
const specData = typeof integration.spec === 'string'
? JSON.parse(integration.spec)
: integration.spec;
return specData?.category || "Uncategorized";
}))
);
// Filter integrations by selected category
const filteredIntegrations = selectedCategory === "all"
? integrationDefinitions
: integrationDefinitions.filter(
(integration) => {
const specData = typeof integration.spec === 'string'
? JSON.parse(integration.spec)
: integration.spec;
return specData?.category === selectedCategory;
}
);
// Check if user has an active account for an integration
const hasActiveAccount = (integrationDefinitionId: string) => {
return integrationAccounts.some(
(account) => account.integrationDefinitionId === integrationDefinitionId && account.isActive
(account) =>
account.integrationDefinitionId === integrationDefinitionId &&
account.isActive,
);
};
// Handle connection with API key
const handleApiKeyConnect = async () => {
if (!selectedIntegration || !apiKey.trim()) return;
setIsLoading(true);
try {
const response = await fetch("/api/v1/integration_account", {
@ -84,12 +78,12 @@ export default function Integrations() {
apiKey,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to connect integration");
}
// Refresh the page to show the new integration account
window.location.reload();
} catch (error) {
@ -103,7 +97,7 @@ export default function Integrations() {
// Handle OAuth connection
const handleOAuthConnect = async () => {
if (!selectedIntegration) return;
setIsConnecting(true);
try {
const response = await fetch("/api/v1/oauth", {
@ -116,12 +110,12 @@ export default function Integrations() {
userId,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to start OAuth flow");
}
const { url } = await response.json();
// Redirect to OAuth authorization URL
window.location.href = url;
@ -134,117 +128,93 @@ export default function Integrations() {
};
return (
<div className="home flex h-full flex-col overflow-y-auto p-3">
<div className="flex items-center justify-between">
<div className="space-y-1 text-base">
<h2 className="text-lg font-semibold">Integrations</h2>
<p className="text-muted-foreground">Connect your tools and services</p>
</div>
<div className="flex items-center gap-2">
{/* Category filter */}
<Select
value={selectedCategory}
onValueChange={setSelectedCategory}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
{categories.map((category) => (
<SelectItem key={category} value={category}>{category}</SelectItem>
))}
</SelectContent>
</Select>
{/* Add integration button */}
<Button variant="default" size="sm">
<Plus className="mr-1 h-3.5 w-3.5" />
Add integration
</Button>
</div>
<div className="home flex h-full flex-col overflow-y-auto p-4 px-5">
<div className="space-y-1 text-base">
<p className="text-muted-foreground">Connect your tools and services</p>
</div>
{/* Integration cards grid */}
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{filteredIntegrations.map((integration) => {
{integrationDefinitions.map((integration) => {
const isConnected = hasActiveAccount(integration.id);
const authType = integration.spec?.auth?.type || "unknown";
return (
<Dialog key={integration.id} onOpenChange={(open) => {
if (open) {
setSelectedIntegration(integration);
setApiKey("");
} else {
setSelectedIntegration(null);
}
}}>
<Dialog
key={integration.id}
onOpenChange={(open) => {
if (open) {
setSelectedIntegration(integration);
setApiKey("");
} else {
setSelectedIntegration(null);
}
}}
>
<DialogTrigger asChild>
<Card className="cursor-pointer transition-all hover:shadow-md">
<CardHeader className="p-4">
<div className="mb-2 flex h-10 w-10 items-center justify-center rounded bg-background-2">
<div className="bg-background-2 mb-2 flex h-10 w-10 items-center justify-center rounded">
{integration.icon ? (
<img
src={integration.icon}
alt={integration.name}
className="h-6 w-6"
<img
src={integration.icon}
alt={integration.name}
className="h-6 w-6"
/>
) : (
<div className="h-6 w-6 rounded-full bg-gray-300" />
)}
</div>
<CardTitle className="text-base">{integration.name}</CardTitle>
<CardTitle className="text-base">
{integration.name}
</CardTitle>
<CardDescription className="line-clamp-2 text-xs">
{integration.description || "Connect to " + integration.name}
{integration.description ||
"Connect to " + integration.name}
</CardDescription>
</CardHeader>
<CardFooter className="border-t p-3">
<div className="flex w-full items-center justify-between">
<span className="text-xs text-muted-foreground">
{(() => {
const specData = typeof integration.spec === 'string'
? JSON.parse(integration.spec)
: integration.spec;
return specData?.category || "Uncategorized";
})()}
</span>
<div className="flex w-full items-center justify-end">
{isConnected ? (
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-800">
Connected
</span>
) : (
<span className="text-xs text-muted-foreground">Not connected</span>
<span className="text-muted-foreground text-xs">
Not connected
</span>
)}
</div>
</CardFooter>
</Card>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Connect to {integration.name}</DialogTitle>
<DialogDescription>
{integration.description || `Connect your ${integration.name} account to enable integration.`}
{integration.description ||
`Connect your ${integration.name} account to enable integration.`}
</DialogDescription>
</DialogHeader>
{/* API Key Authentication */}
{(() => {
const specData = typeof integration.spec === 'string'
? JSON.parse(integration.spec)
: integration.spec;
const specData =
typeof integration.spec === "string"
? JSON.parse(integration.spec)
: integration.spec;
return specData?.auth?.api_key;
})() && (
<div className="space-y-4 py-4">
<div className="space-y-2">
<label htmlFor="apiKey" className="text-sm font-medium">
{(() => {
const specData = typeof integration.spec === 'string'
? JSON.parse(integration.spec)
: integration.spec;
return specData?.auth?.api_key?.label || "API Key";
})()}
const specData =
typeof integration.spec === "string"
? JSON.parse(integration.spec)
: integration.spec;
return specData?.auth?.api_key?.label || "API Key";
})()}
</label>
<Input
id="apiKey"
@ -254,40 +224,45 @@ export default function Integrations() {
onChange={(e) => setApiKey(e.target.value)}
/>
{(() => {
const specData = typeof integration.spec === 'string'
? JSON.parse(integration.spec)
: integration.spec;
const specData =
typeof integration.spec === "string"
? JSON.parse(integration.spec)
: integration.spec;
return specData?.auth?.api_key?.description;
})() && (
<p className="text-xs text-muted-foreground">
<p className="text-muted-foreground text-xs">
{(() => {
const specData = typeof integration.spec === 'string'
? JSON.parse(integration.spec)
: integration.spec;
const specData =
typeof integration.spec === "string"
? JSON.parse(integration.spec)
: integration.spec;
return specData?.auth?.api_key?.description;
})()}
</p>
)}
</div>
<FormButtons>
<Button
type="button"
variant="default"
disabled={isLoading || !apiKey.trim()}
onClick={handleApiKeyConnect}
>
{isLoading ? "Connecting..." : "Connect"}
</Button>
</FormButtons>
<FormButtons
confirmButton={
<Button
type="button"
variant="default"
disabled={isLoading || !apiKey.trim()}
onClick={handleApiKeyConnect}
>
{isLoading ? "Connecting..." : "Connect"}
</Button>
}
></FormButtons>
</div>
)}
{/* OAuth Authentication */}
{(() => {
const specData = typeof integration.spec === 'string'
? JSON.parse(integration.spec)
: integration.spec;
const specData =
typeof integration.spec === "string"
? JSON.parse(integration.spec)
: integration.spec;
return specData?.auth?.oauth2;
})() && (
<div className="flex justify-center py-8">
@ -298,26 +273,30 @@ export default function Integrations() {
disabled={isConnecting}
onClick={handleOAuthConnect}
>
{isConnecting ? "Connecting..." : `Connect to ${integration.name}`}
{isConnecting
? "Connecting..."
: `Connect to ${integration.name}`}
</Button>
</div>
)}
{/* No authentication method found */}
{(() => {
const specData = typeof integration.spec === 'string'
? JSON.parse(integration.spec)
: integration.spec;
const specData =
typeof integration.spec === "string"
? JSON.parse(integration.spec)
: integration.spec;
return !specData?.auth?.api_key && !specData?.auth?.oauth2;
})() && (
<div className="py-4 text-center text-muted-foreground">
<div className="text-muted-foreground py-4 text-center">
This integration doesn't specify an authentication method.
</div>
)}
<DialogFooter className="sm:justify-start">
<div className="w-full text-xs text-muted-foreground">
By connecting, you agree to the {integration.name} terms of service.
<div className="text-muted-foreground w-full text-xs">
By connecting, you agree to the {integration.name} terms of
service.
</div>
</DialogFooter>
</DialogContent>
@ -325,19 +304,14 @@ export default function Integrations() {
);
})}
</div>
{/* Empty state */}
{filteredIntegrations.length === 0 && (
{integrationDefinitions.length === 0 && (
<div className="mt-20 flex flex-col items-center justify-center">
<Search className="mb-2 h-12 w-12 text-muted-foreground" />
<Search className="text-muted-foreground mb-2 h-12 w-12" />
<h3 className="text-lg font-medium">No integrations found</h3>
<p className="text-muted-foreground">
{selectedCategory === "all"
? "No integrations are available at this time."
: `No integrations found in the "${selectedCategory}" category.`}
</p>
</div>
)}
</div>
);
}
}

View File

@ -0,0 +1,116 @@
import { useState } from "react";
import { useLogs } from "~/hooks/use-logs";
import { LogsFilters } from "~/components/logs/logs-filters";
import { VirtualLogsList } from "~/components/logs/virtual-logs-list";
import { AppContainer, PageContainer, PageBody } from "~/components/layout/app-layout";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import { Activity } from "lucide-react";
export default function LogsActivity() {
const [selectedSource, setSelectedSource] = useState<string | undefined>();
const [selectedStatus, setSelectedStatus] = useState<string | undefined>();
const {
logs,
hasMore,
loadMore,
availableSources,
isLoading,
isInitialLoad
} = useLogs({
endpoint: '/api/v1/logs/activity',
source: selectedSource,
status: selectedStatus
});
if (isInitialLoad) {
return (
<AppContainer>
<PageContainer>
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
</PageContainer>
</AppContainer>
);
}
return (
<AppContainer>
<PageContainer>
<PageBody>
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Activity className="h-6 w-6 text-primary" />
<div>
<h1 className="text-2xl font-bold">Activity Ingestion Logs</h1>
<p className="text-muted-foreground">
View ingestion logs for activities from connected integrations
</p>
</div>
</div>
<Badge variant="outline" className="text-sm">
{logs.length} activity logs loaded
</Badge>
</div>
{/* Filters */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Filters</CardTitle>
</CardHeader>
<CardContent>
<LogsFilters
availableSources={availableSources}
selectedSource={selectedSource}
selectedStatus={selectedStatus}
onSourceChange={setSelectedSource}
onStatusChange={setSelectedStatus}
/>
</CardContent>
</Card>
{/* Logs List */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Activity Ingestion Queue</h2>
{hasMore && (
<span className="text-sm text-muted-foreground">
Scroll to load more...
</span>
)}
</div>
{logs.length === 0 ? (
<Card>
<CardContent className="flex items-center justify-center py-16">
<div className="text-center">
<Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">No activity logs found</h3>
<p className="text-muted-foreground">
{selectedSource || selectedStatus
? 'Try adjusting your filters to see more results.'
: 'No activity ingestion logs are available yet.'}
</p>
</div>
</CardContent>
</Card>
) : (
<VirtualLogsList
logs={logs}
hasMore={hasMore}
loadMore={loadMore}
isLoading={isLoading}
height={600}
/>
)}
</div>
</div>
</PageBody>
</PageContainer>
</AppContainer>
);
}

View File

@ -0,0 +1,102 @@
import { useState } from "react";
import { useLogs } from "~/hooks/use-logs";
import { LogsFilters } from "~/components/logs/logs-filters";
import { VirtualLogsList } from "~/components/logs/virtual-logs-list";
import {
AppContainer,
PageContainer,
PageBody,
} from "~/components/layout/app-layout";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import { Database } from "lucide-react";
export default function LogsAll() {
const [selectedSource, setSelectedSource] = useState<string | undefined>();
const [selectedStatus, setSelectedStatus] = useState<string | undefined>();
const {
logs,
hasMore,
loadMore,
availableSources,
isLoading,
isInitialLoad,
} = useLogs({
endpoint: "/api/v1/logs/all",
source: selectedSource,
status: selectedStatus,
});
if (isInitialLoad) {
return (
<AppContainer>
<PageContainer>
<div className="flex h-64 items-center justify-center">
<div className="border-primary h-8 w-8 animate-spin rounded-full border-b-2"></div>
</div>
</PageContainer>
</AppContainer>
);
}
return (
<div className="space-y-6 p-4 px-5">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div>
<p className="text-muted-foreground">
View all ingestion queue items and their processing status
</p>
</div>
</div>
</div>
{/* Filters */}
<LogsFilters
availableSources={availableSources}
selectedSource={selectedSource}
selectedStatus={selectedStatus}
onSourceChange={setSelectedSource}
onStatusChange={setSelectedStatus}
/>
{/* Logs List */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Ingestion Queue</h2>
{hasMore && (
<span className="text-muted-foreground text-sm">
Scroll to load more...
</span>
)}
</div>
{logs.length === 0 ? (
<Card>
<CardContent className="flex items-center justify-center py-16">
<div className="text-center">
<Database className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-semibold">No logs found</h3>
<p className="text-muted-foreground">
{selectedSource || selectedStatus
? "Try adjusting your filters to see more results."
: "No ingestion logs are available yet."}
</p>
</div>
</CardContent>
</Card>
) : (
<VirtualLogsList
logs={logs}
hasMore={hasMore}
loadMore={loadMore}
isLoading={isLoading}
height={600}
/>
)}
</div>
</div>
);
}

View File

@ -80,6 +80,7 @@
"class-validator": "0.14.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^0.2.1",
"compression": "^1.7.4",
"cross-env": "^7.0.3",
"d3": "^7.9.0",

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "IngestionQueue" ADD COLUMN "activityId" TEXT;
-- AddForeignKey
ALTER TABLE "IngestionQueue" ADD CONSTRAINT "IngestionQueue_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "Activity"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -31,6 +31,7 @@ model Activity {
WebhookDeliveryLog WebhookDeliveryLog[]
ConversationHistory ConversationHistory[]
IngestionQueue IngestionQueue[]
}
model AuthorizationCode {
@ -136,6 +137,9 @@ model IngestionQueue {
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
activity Activity? @relation(fields: [activityId], references: [id])
activityId String?
// Error handling
error String?
retryCount Int @default(0)

405
pnpm-lock.yaml generated
View File

@ -234,6 +234,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
cmdk:
specifier: ^0.2.1
version: 0.2.1(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
compression:
specifier: ^1.7.4
version: 1.8.0
@ -378,7 +381,7 @@ importers:
devDependencies:
'@remix-run/dev':
specifier: 2.16.7
version: 2.16.7(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/serve@2.16.7(typescript@5.8.3))(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))(yaml@2.8.0)
version: 2.16.7(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/serve@2.16.7(typescript@5.8.3))(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))(yaml@2.8.0)
'@remix-run/eslint-config':
specifier: 2.16.7
version: 2.16.7(eslint@8.57.1)(react@18.3.1)(typescript@5.8.3)
@ -393,7 +396,7 @@ importers:
version: 0.5.16(tailwindcss@4.1.7)
'@tailwindcss/vite':
specifier: ^4.1.7
version: 4.1.9(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))
version: 4.1.9(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))
'@trigger.dev/build':
specifier: ^4.0.0-v4-beta.22
version: 4.0.0-v4-beta.22(typescript@5.8.3)
@ -489,10 +492,10 @@ importers:
version: 5.8.3
vite:
specifier: ^6.0.0
version: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
version: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
vite-tsconfig-paths:
specifier: ^4.2.1
version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))
version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))
packages/database:
dependencies:
@ -547,6 +550,40 @@ importers:
specifier: 18.2.69
version: 18.2.69
packages/mcp:
dependencies:
'@modelcontextprotocol/sdk':
specifier: 1.0.1
version: 1.0.1
'@types/node':
specifier: ^22.14.1
version: 22.16.0
'@types/uuid':
specifier: ^10.0.0
version: 10.0.0
axios:
specifier: ^1.8.4
version: 1.10.0
dotenv:
specifier: ^16.5.0
version: 16.5.0
uuid:
specifier: ^11.1.0
version: 11.1.0
zod:
specifier: ^3.24.3
version: 3.25.76
zod-to-json-schema:
specifier: ^3.24.5
version: 3.24.5(zod@3.25.76)
devDependencies:
shx:
specifier: ^0.3.4
version: 0.3.4
typescript:
specifier: ^5.8.3
version: 5.8.3
packages/mcp-proxy:
dependencies:
'@modelcontextprotocol/sdk':
@ -1790,6 +1827,9 @@ packages:
'@mjackson/headers@0.9.0':
resolution: {integrity: sha512-1WFCu2iRaqbez9hcYYI611vcH1V25R+fDfOge/CyKc8sdbzniGfy/FRhNd3DgvFF4ZEEX2ayBrvFHLtOpfvadw==}
'@modelcontextprotocol/sdk@1.0.1':
resolution: {integrity: sha512-slLdFaxQJ9AlRg+hw28iiTtGvShAOgOKXcD0F91nUcRYiOMuS9ZBYjcdNZRXW9G5JQ511GRTdUy1zQVZDpJ+4w==}
'@modelcontextprotocol/sdk@1.13.2':
resolution: {integrity: sha512-Vx7qOcmoKkR3qhaQ9qf3GxiVKCEu+zfJddHv6x3dY/9P6+uIwJnmuAur5aB+4FDXf41rRrDnOEGkviX5oYZ67w==}
engines: {node: '>=18'}
@ -2139,6 +2179,9 @@ packages:
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
'@radix-ui/primitive@1.0.0':
resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==}
'@radix-ui/primitive@1.1.0':
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
@ -2275,6 +2318,11 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-compose-refs@1.0.0':
resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-compose-refs@1.0.1':
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
peerDependencies:
@ -2302,6 +2350,11 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-context@1.0.0':
resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-context@1.1.0':
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
peerDependencies:
@ -2320,6 +2373,12 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.0.0':
resolution: {integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-dialog@1.1.14':
resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==}
peerDependencies:
@ -2351,6 +2410,12 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.0.0':
resolution: {integrity: sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-dismissable-layer@1.1.0':
resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==}
peerDependencies:
@ -2390,6 +2455,11 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-focus-guards@1.0.0':
resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-focus-guards@1.1.0':
resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==}
peerDependencies:
@ -2408,6 +2478,12 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-focus-scope@1.0.0':
resolution: {integrity: sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-focus-scope@1.1.0':
resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==}
peerDependencies:
@ -2439,6 +2515,11 @@ packages:
peerDependencies:
react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc
'@radix-ui/react-id@1.0.0':
resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-id@1.1.0':
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
peerDependencies:
@ -2535,6 +2616,12 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-portal@1.0.0':
resolution: {integrity: sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-portal@1.1.1':
resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==}
peerDependencies:
@ -2561,6 +2648,12 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.0.0':
resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-presence@1.1.0':
resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
peerDependencies:
@ -2587,6 +2680,12 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@1.0.0':
resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-primitive@2.0.0':
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
peerDependencies:
@ -2678,6 +2777,11 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.0.0':
resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-slot@1.0.2':
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
@ -2796,6 +2900,11 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.0.0':
resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
@ -2814,6 +2923,11 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.0.0':
resolution: {integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-use-controllable-state@1.1.0':
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
peerDependencies:
@ -2841,6 +2955,11 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.0.0':
resolution: {integrity: sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-use-escape-keydown@1.1.0':
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
peerDependencies:
@ -2868,6 +2987,11 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.0.0':
resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-use-layout-effect@1.1.0':
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
peerDependencies:
@ -4310,6 +4434,9 @@ packages:
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@types/uuid@10.0.0':
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
'@types/uuid@9.0.8':
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
@ -5077,6 +5204,12 @@ packages:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
cmdk@0.2.1:
resolution: {integrity: sha512-U6//9lQ6JvT47+6OF6Gi8BvkxYQ8SCRRSKIJkthIMsFsLZRG0cKvTtuTaefyIKMQb8rvvXy0wGdpTNq/jPtm+g==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
cmdk@1.1.1:
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
peerDependencies:
@ -6576,6 +6709,10 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
interpret@1.4.0:
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
engines: {node: '>= 0.10'}
ioredis@5.6.1:
resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==}
engines: {node: '>=12.22.0'}
@ -8544,6 +8681,16 @@ packages:
'@types/react':
optional: true
react-remove-scroll@2.5.4:
resolution: {integrity: sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-remove-scroll@2.5.7:
resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==}
engines: {node: '>=10'}
@ -8650,6 +8797,10 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
rechoir@0.6.2:
resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
engines: {node: '>= 0.10'}
redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
@ -8968,9 +9119,19 @@ packages:
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
engines: {node: '>= 0.4'}
shelljs@0.8.5:
resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==}
engines: {node: '>=4'}
hasBin: true
shimmer@1.2.1:
resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==}
shx@0.3.4:
resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==}
engines: {node: '>=6'}
hasBin: true
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@ -9753,6 +9914,10 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
@ -10089,6 +10254,9 @@ packages:
zod@3.23.8:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zustand@4.5.7:
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
engines: {node: '>=12.7.0'}
@ -11470,6 +11638,12 @@ snapshots:
'@mjackson/headers@0.9.0': {}
'@modelcontextprotocol/sdk@1.0.1':
dependencies:
content-type: 1.0.5
raw-body: 3.0.0
zod: 3.25.76
'@modelcontextprotocol/sdk@1.13.2':
dependencies:
ajv: 6.12.6
@ -11832,6 +12006,10 @@ snapshots:
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.0.0':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/primitive@1.1.0': {}
'@radix-ui/primitive@1.1.2': {}
@ -11970,6 +12148,11 @@ snapshots:
'@types/react': 18.2.69
'@types/react-dom': 18.3.7(@types/react@18.2.69)
'@radix-ui/react-compose-refs@1.0.0(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
react: 18.3.1
'@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.69)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
@ -11989,6 +12172,11 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.69
'@radix-ui/react-context@1.0.0(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
react: 18.3.1
'@radix-ui/react-context@1.1.0(@types/react@18.2.47)(react@18.3.1)':
dependencies:
react: 18.3.1
@ -12001,6 +12189,28 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.69
'@radix-ui/react-dialog@1.0.0(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.3.1)
'@radix-ui/react-context': 1.0.0(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-focus-guards': 1.0.0(react@18.3.1)
'@radix-ui/react-focus-scope': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.0.0(react@18.3.1)
'@radix-ui/react-portal': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-presence': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.0.0(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.3.1)
aria-hidden: 1.2.6
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.5.4(@types/react@18.2.69)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
'@radix-ui/react-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.2
@ -12035,6 +12245,17 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.69
'@radix-ui/react-dismissable-layer@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.3.1)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1)
'@radix-ui/react-use-escape-keydown': 1.0.0(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
@ -12076,6 +12297,11 @@ snapshots:
'@types/react': 18.2.69
'@types/react-dom': 18.3.7(@types/react@18.2.69)
'@radix-ui/react-focus-guards@1.0.0(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
react: 18.3.1
'@radix-ui/react-focus-guards@1.1.0(@types/react@18.2.47)(react@18.3.1)':
dependencies:
react: 18.3.1
@ -12088,6 +12314,15 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.69
'@radix-ui/react-focus-scope@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/react-compose-refs': 1.0.0(react@18.3.1)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
@ -12114,6 +12349,12 @@ snapshots:
dependencies:
react: 18.3.1
'@radix-ui/react-id@1.0.0(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1)
react: 18.3.1
'@radix-ui/react-id@1.1.0(@types/react@18.2.47)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1)
@ -12245,6 +12486,13 @@ snapshots:
'@types/react': 18.2.69
'@types/react-dom': 18.3.7(@types/react@18.2.69)
'@radix-ui/react-portal@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -12265,6 +12513,14 @@ snapshots:
'@types/react': 18.2.69
'@types/react-dom': 18.3.7(@types/react@18.2.69)
'@radix-ui/react-presence@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/react-compose-refs': 1.0.0(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
@ -12285,6 +12541,13 @@ snapshots:
'@types/react': 18.2.69
'@types/react-dom': 18.3.7(@types/react@18.2.69)
'@radix-ui/react-primitive@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/react-slot': 1.0.0(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.7(@types/react@18.2.47))(@types/react@18.2.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1)
@ -12392,6 +12655,12 @@ snapshots:
'@types/react': 18.2.69
'@types/react-dom': 18.3.7(@types/react@18.2.69)
'@radix-ui/react-slot@1.0.0(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/react-compose-refs': 1.0.0(react@18.3.1)
react: 18.3.1
'@radix-ui/react-slot@1.0.2(@types/react@18.2.69)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
@ -12531,6 +12800,11 @@ snapshots:
'@types/react': 18.2.69
'@types/react-dom': 18.3.7(@types/react@18.2.69)
'@radix-ui/react-use-callback-ref@1.0.0(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
react: 18.3.1
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.47)(react@18.3.1)':
dependencies:
react: 18.3.1
@ -12543,6 +12817,12 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.69
'@radix-ui/react-use-controllable-state@1.0.0(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1)
react: 18.3.1
'@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.47)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1)
@ -12565,6 +12845,12 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.69
'@radix-ui/react-use-escape-keydown@1.0.0(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1)
react: 18.3.1
'@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.2.47)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1)
@ -12586,6 +12872,11 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.69
'@radix-ui/react-use-layout-effect@1.0.0(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
react: 18.3.1
'@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.47)(react@18.3.1)':
dependencies:
react: 18.3.1
@ -12791,7 +13082,7 @@ snapshots:
transitivePeerDependencies:
- encoding
'@remix-run/dev@2.16.7(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/serve@2.16.7(typescript@5.8.3))(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))(yaml@2.8.0)':
'@remix-run/dev@2.16.7(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/serve@2.16.7(typescript@5.8.3))(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))(yaml@2.8.0)':
dependencies:
'@babel/core': 7.27.4
'@babel/generator': 7.27.5
@ -12808,7 +13099,7 @@ snapshots:
'@remix-run/router': 1.23.0
'@remix-run/server-runtime': 2.16.7(typescript@5.8.3)
'@types/mdx': 2.0.13
'@vanilla-extract/integration': 6.5.0(@types/node@20.19.7)(lightningcss@1.30.1)(terser@5.42.0)
'@vanilla-extract/integration': 6.5.0(@types/node@22.16.0)(lightningcss@1.30.1)(terser@5.42.0)
arg: 5.0.2
cacache: 17.1.4
chalk: 4.1.2
@ -12848,12 +13139,12 @@ snapshots:
tar-fs: 2.1.3
tsconfig-paths: 4.2.0
valibot: 0.41.0(typescript@5.8.3)
vite-node: 3.2.3(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
vite-node: 3.2.3(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
ws: 7.5.10
optionalDependencies:
'@remix-run/serve': 2.16.7(typescript@5.8.3)
typescript: 5.8.3
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -13592,12 +13883,12 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 4.1.7
'@tailwindcss/vite@4.1.9(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))':
'@tailwindcss/vite@4.1.9(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0))':
dependencies:
'@tailwindcss/node': 4.1.9
'@tailwindcss/oxide': 4.1.9
tailwindcss: 4.1.9
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
'@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
@ -13947,7 +14238,7 @@ snapshots:
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
'@types/node': 18.19.115
'@types/node': 22.16.0
'@types/compression@1.8.1':
dependencies:
@ -13958,7 +14249,7 @@ snapshots:
'@types/connect@3.4.38':
dependencies:
'@types/node': 18.19.115
'@types/node': 22.16.0
'@types/cookie@0.4.1': {}
@ -13966,7 +14257,7 @@ snapshots:
'@types/cors@2.8.19':
dependencies:
'@types/node': 18.19.115
'@types/node': 22.16.0
'@types/d3-array@3.2.1': {}
@ -14113,7 +14404,7 @@ snapshots:
'@types/express-serve-static-core@4.19.6':
dependencies:
'@types/node': 18.19.115
'@types/node': 22.16.0
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 0.17.5
@ -14234,12 +14525,12 @@ snapshots:
'@types/send@0.17.5':
dependencies:
'@types/mime': 1.3.5
'@types/node': 18.19.115
'@types/node': 22.16.0
'@types/serve-static@1.15.8':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 18.19.115
'@types/node': 22.16.0
'@types/send': 0.17.5
'@types/shimmer@1.2.0': {}
@ -14254,13 +14545,15 @@ snapshots:
'@types/use-sync-external-store@0.0.6': {}
'@types/uuid@10.0.0': {}
'@types/uuid@9.0.8': {}
'@types/validator@13.15.2': {}
'@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.17))(esbuild@0.19.11)':
dependencies:
'@types/node': 18.19.115
'@types/node': 22.16.0
tapable: 2.2.2
webpack: 5.99.9(@swc/core@1.3.101(@swc/helpers@0.5.17))(esbuild@0.19.11)
transitivePeerDependencies:
@ -14523,7 +14816,7 @@ snapshots:
transitivePeerDependencies:
- babel-plugin-macros
'@vanilla-extract/integration@6.5.0(@types/node@20.19.7)(lightningcss@1.30.1)(terser@5.42.0)':
'@vanilla-extract/integration@6.5.0(@types/node@22.16.0)(lightningcss@1.30.1)(terser@5.42.0)':
dependencies:
'@babel/core': 7.27.4
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4)
@ -14536,8 +14829,8 @@ snapshots:
lodash: 4.17.21
mlly: 1.7.4
outdent: 0.8.0
vite: 5.4.19(@types/node@20.19.7)(lightningcss@1.30.1)(terser@5.42.0)
vite-node: 1.6.1(@types/node@20.19.7)(lightningcss@1.30.1)(terser@5.42.0)
vite: 5.4.19(@types/node@22.16.0)(lightningcss@1.30.1)(terser@5.42.0)
vite-node: 1.6.1(@types/node@22.16.0)(lightningcss@1.30.1)(terser@5.42.0)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -15174,6 +15467,14 @@ snapshots:
cluster-key-slot@1.1.2: {}
cmdk@0.2.1(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@radix-ui/react-dialog': 1.0.0(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.69)(react@18.3.1)
@ -15794,7 +16095,7 @@ snapshots:
dependencies:
'@types/cookie': 0.4.1
'@types/cors': 2.8.19
'@types/node': 18.19.115
'@types/node': 22.16.0
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.4.2
@ -16407,7 +16708,7 @@ snapshots:
eval@0.1.8:
dependencies:
'@types/node': 18.19.115
'@types/node': 22.16.0
require-like: 0.1.2
event-target-shim@5.0.1: {}
@ -17092,6 +17393,8 @@ snapshots:
internmap@2.0.3: {}
interpret@1.4.0: {}
ioredis@5.6.1:
dependencies:
'@ioredis/commands': 1.2.0
@ -17326,7 +17629,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
'@types/node': 18.19.115
'@types/node': 22.16.0
merge-stream: 2.0.0
supports-color: 8.1.1
@ -19209,7 +19512,7 @@ snapshots:
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
'@types/node': 18.19.115
'@types/node': 22.16.0
long: 5.3.2
proxy-addr@2.0.7:
@ -19416,6 +19719,17 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.69
react-remove-scroll@2.5.4(@types/react@18.2.69)(react@18.3.1):
dependencies:
react: 18.3.1
react-remove-scroll-bar: 2.3.8(@types/react@18.2.69)(react@18.3.1)
react-style-singleton: 2.2.3(@types/react@18.2.69)(react@18.3.1)
tslib: 2.8.1
use-callback-ref: 1.3.3(@types/react@18.2.69)(react@18.3.1)
use-sidecar: 1.1.3(@types/react@18.2.69)(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.69
react-remove-scroll@2.5.7(@types/react@18.2.47)(react@18.3.1):
dependencies:
react: 18.3.1
@ -19554,6 +19868,10 @@ snapshots:
readdirp@4.1.2: {}
rechoir@0.6.2:
dependencies:
resolve: 1.22.10
redent@3.0.0:
dependencies:
indent-string: 4.0.0
@ -19956,8 +20274,19 @@ snapshots:
shell-quote@1.8.3: {}
shelljs@0.8.5:
dependencies:
glob: 7.2.3
interpret: 1.4.0
rechoir: 0.6.2
shimmer@1.2.1: {}
shx@0.3.4:
dependencies:
minimist: 1.2.8
shelljs: 0.8.5
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
@ -20920,6 +21249,8 @@ snapshots:
utils-merge@1.0.1: {}
uuid@11.1.0: {}
uuid@9.0.1: {}
uvu@0.5.6:
@ -20966,13 +21297,13 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
vite-node@1.6.1(@types/node@20.19.7)(lightningcss@1.30.1)(terser@5.42.0):
vite-node@1.6.1(@types/node@22.16.0)(lightningcss@1.30.1)(terser@5.42.0):
dependencies:
cac: 6.7.14
debug: 4.4.1
pathe: 1.1.2
picocolors: 1.1.1
vite: 5.4.19(@types/node@20.19.7)(lightningcss@1.30.1)(terser@5.42.0)
vite: 5.4.19(@types/node@22.16.0)(lightningcss@1.30.1)(terser@5.42.0)
transitivePeerDependencies:
- '@types/node'
- less
@ -20984,13 +21315,13 @@ snapshots:
- supports-color
- terser
vite-node@3.2.3(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0):
vite-node@3.2.3(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0):
dependencies:
cac: 6.7.14
debug: 4.4.1
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
transitivePeerDependencies:
- '@types/node'
- jiti
@ -21005,29 +21336,29 @@ snapshots:
- tsx
- yaml
vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)):
vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)):
dependencies:
debug: 4.4.1
globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.8.3)
optionalDependencies:
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
- typescript
vite@5.4.19(@types/node@20.19.7)(lightningcss@1.30.1)(terser@5.42.0):
vite@5.4.19(@types/node@22.16.0)(lightningcss@1.30.1)(terser@5.42.0):
dependencies:
esbuild: 0.21.5
postcss: 8.5.5
rollup: 4.43.0
optionalDependencies:
'@types/node': 20.19.7
'@types/node': 22.16.0
fsevents: 2.3.3
lightningcss: 1.30.1
terser: 5.42.0
vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0):
vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(yaml@2.8.0):
dependencies:
esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.2)
@ -21036,7 +21367,7 @@ snapshots:
rollup: 4.43.0
tinyglobby: 0.2.14
optionalDependencies:
'@types/node': 20.19.7
'@types/node': 22.16.0
fsevents: 2.3.3
jiti: 2.4.2
lightningcss: 1.30.1
@ -21293,12 +21624,18 @@ snapshots:
dependencies:
zod: 3.23.8
zod-to-json-schema@3.24.5(zod@3.25.76):
dependencies:
zod: 3.25.76
zod-validation-error@1.5.0(zod@3.23.8):
dependencies:
zod: 3.23.8
zod@3.23.8: {}
zod@3.25.76: {}
zustand@4.5.7(@types/react@18.2.69)(react@18.3.1):
dependencies:
use-sync-external-store: 1.5.0(react@18.3.1)