mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-24 09:08:30 +00:00
Feat: single mcp server for all integrations
This commit is contained in:
parent
9d34e5d926
commit
256cdb8bdc
@ -1,4 +1,4 @@
|
|||||||
VERSION=0.1.17
|
VERSION=0.1.18
|
||||||
|
|
||||||
# Nest run in docker, change host to database container name
|
# Nest run in docker, change host to database container name
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
|
|||||||
@ -30,41 +30,6 @@ export function SpaceSearch({
|
|||||||
}
|
}
|
||||||
}, [debouncedSearchQuery, searchQuery, onSearchChange]);
|
}, [debouncedSearchQuery, searchQuery, onSearchChange]);
|
||||||
|
|
||||||
// Count statement nodes that match the search
|
|
||||||
const matchingStatements = useMemo(() => {
|
|
||||||
if (!debouncedSearchQuery.trim()) return 0;
|
|
||||||
|
|
||||||
const query = debouncedSearchQuery.toLowerCase();
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
const isStatementNode = (node: any) => {
|
|
||||||
return (
|
|
||||||
node.attributes?.fact ||
|
|
||||||
(node.labels && node.labels.includes("Statement"))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
triplets.forEach((triplet) => {
|
|
||||||
// Check if source node is a statement and matches
|
|
||||||
if (
|
|
||||||
isStatementNode(triplet.sourceNode) &&
|
|
||||||
triplet.sourceNode.attributes?.fact?.toLowerCase().includes(query)
|
|
||||||
) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if target node is a statement and matches
|
|
||||||
if (
|
|
||||||
isStatementNode(triplet.targetNode) &&
|
|
||||||
triplet.targetNode.attributes?.fact?.toLowerCase().includes(query)
|
|
||||||
) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [triplets, debouncedSearchQuery]);
|
|
||||||
|
|
||||||
// Helper to determine if a node is a statement
|
// Helper to determine if a node is a statement
|
||||||
const isStatementNode = useCallback((node: any) => {
|
const isStatementNode = useCallback((node: any) => {
|
||||||
// Check if node has a fact attribute (indicates it's a statement)
|
// Check if node has a fact attribute (indicates it's a statement)
|
||||||
@ -74,6 +39,34 @@ export function SpaceSearch({
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Count statement nodes that match the search
|
||||||
|
const matchingStatements = useMemo(() => {
|
||||||
|
if (!debouncedSearchQuery.trim()) return 0;
|
||||||
|
|
||||||
|
const query = debouncedSearchQuery.toLowerCase();
|
||||||
|
const statements: Record<string, number> = {};
|
||||||
|
|
||||||
|
triplets.forEach((triplet) => {
|
||||||
|
// Check if source node is a statement and matches
|
||||||
|
if (
|
||||||
|
isStatementNode(triplet.sourceNode) &&
|
||||||
|
triplet.sourceNode.attributes?.fact?.toLowerCase().includes(query)
|
||||||
|
) {
|
||||||
|
statements[triplet.sourceNode.uuid] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if target node is a statement and matches
|
||||||
|
if (
|
||||||
|
isStatementNode(triplet.targetNode) &&
|
||||||
|
triplet.targetNode.attributes?.fact?.toLowerCase().includes(query)
|
||||||
|
) {
|
||||||
|
statements[triplet.targetNode.uuid] = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.keys(statements).length;
|
||||||
|
}, [triplets, debouncedSearchQuery]);
|
||||||
|
|
||||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setInputValue(event.target.value);
|
setInputValue(event.target.value);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { useFetcher } from "@remix-run/react";
|
import { useFetcher } from "@remix-run/react";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Check, Copy } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import { Input } from "../ui/input";
|
|
||||||
|
|
||||||
interface MCPAuthSectionProps {
|
interface MCPAuthSectionProps {
|
||||||
integration: {
|
integration: {
|
||||||
@ -19,49 +18,6 @@ interface MCPAuthSectionProps {
|
|||||||
hasMCPAuth: boolean;
|
hasMCPAuth: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MCPUrlBoxProps {
|
|
||||||
mcpUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MCPUrlBox({ mcpUrl }: MCPUrlBoxProps) {
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
|
|
||||||
const handleCopy = useCallback(() => {
|
|
||||||
navigator.clipboard.writeText(mcpUrl).then(() => {
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
});
|
|
||||||
}, [mcpUrl]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-3 flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={mcpUrl}
|
|
||||||
readOnly
|
|
||||||
className="w-full rounded px-2 py-1 font-mono text-sm"
|
|
||||||
style={{ maxWidth: 400 }}
|
|
||||||
onFocus={(e) => e.target.select()}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={copied ? "secondary" : "ghost"}
|
|
||||||
onClick={handleCopy}
|
|
||||||
aria-label={copied ? "Copied" : "Copy MCP URL"}
|
|
||||||
disabled={copied}
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Check size={16} /> Copied
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<Copy size={16} />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MCPAuthSection({
|
export function MCPAuthSection({
|
||||||
integration,
|
integration,
|
||||||
activeAccount,
|
activeAccount,
|
||||||
@ -74,8 +30,6 @@ export function MCPAuthSection({
|
|||||||
const isMCPConnected = !!activeAccount?.integrationConfiguration?.mcp;
|
const isMCPConnected = !!activeAccount?.integrationConfiguration?.mcp;
|
||||||
const isConnected = !!activeAccount;
|
const isConnected = !!activeAccount;
|
||||||
|
|
||||||
const mcpUrl = `https://core.heysol.ai/api/v1/mcp/${integration.slug}`;
|
|
||||||
|
|
||||||
const handleMCPConnect = useCallback(() => {
|
const handleMCPConnect = useCallback(() => {
|
||||||
setIsMCPConnecting(true);
|
setIsMCPConnecting(true);
|
||||||
mcpFetcher.submit(
|
mcpFetcher.submit(
|
||||||
@ -135,8 +89,8 @@ export function MCPAuthSection({
|
|||||||
<div className="mt-6 space-y-2">
|
<div className="mt-6 space-y-2">
|
||||||
<h3 className="text-lg font-medium">MCP Authentication</h3>
|
<h3 className="text-lg font-medium">MCP Authentication</h3>
|
||||||
|
|
||||||
{hasMCPAuth ? (
|
{hasMCPAuth &&
|
||||||
isMCPConnected ? (
|
(isMCPConnected ? (
|
||||||
<div className="bg-background-3 rounded-lg p-4">
|
<div className="bg-background-3 rounded-lg p-4">
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<p className="inline-flex items-center gap-2 font-medium">
|
<p className="inline-flex items-center gap-2 font-medium">
|
||||||
@ -145,7 +99,6 @@ export function MCPAuthSection({
|
|||||||
<p className="text-muted-foreground mb-3">
|
<p className="text-muted-foreground mb-3">
|
||||||
MCP (Model Context Protocol) authentication is active
|
MCP (Model Context Protocol) authentication is active
|
||||||
</p>
|
</p>
|
||||||
<MCPUrlBox mcpUrl={mcpUrl} />
|
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@ -182,21 +135,7 @@ export function MCPAuthSection({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
) : (
|
|
||||||
// hasMCPAuth is false, but integration is connected: show just the MCPUrlBox
|
|
||||||
<div className="bg-background-3 rounded-lg p-4">
|
|
||||||
<div className="text-sm">
|
|
||||||
<p className="inline-flex items-center gap-2 font-medium">
|
|
||||||
<Check size={16} /> Integration Connected
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground mb-3">
|
|
||||||
You can use the MCP endpoint for this integration:
|
|
||||||
</p>
|
|
||||||
<MCPUrlBox mcpUrl={mcpUrl} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export function SpaceFactCard({ fact }: SpaceFactCardProps) {
|
|||||||
<div className="inline-flex min-h-[24px] min-w-[0px] shrink cursor-pointer items-center justify-start">
|
<div className="inline-flex min-h-[24px] min-w-[0px] shrink cursor-pointer items-center justify-start">
|
||||||
<div className={cn("truncate text-left")}>{displayText}</div>
|
<div className={cn("truncate text-left")}>{displayText}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground flex shrink-0 items-center justify-end text-xs">
|
<div className="text-muted-foreground flex shrink-0 items-center justify-end gap-2 text-xs">
|
||||||
{!!recallCount && <span>Recalled: {recallCount} times</span>}
|
{!!recallCount && <span>Recalled: {recallCount} times</span>}
|
||||||
<Badge variant="secondary" className="rounded text-xs">
|
<Badge variant="secondary" className="rounded text-xs">
|
||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
|
|||||||
@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Prisma, PrismaClient } from "@core/database";
|
||||||
Prisma,
|
|
||||||
PrismaClient,
|
|
||||||
type PrismaClientOrTransaction,
|
|
||||||
type PrismaReplicaClient,
|
|
||||||
type PrismaTransactionClient,
|
|
||||||
type PrismaTransactionOptions,
|
|
||||||
$transaction as transac,
|
|
||||||
} from "@core/database";
|
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { env } from "./env.server";
|
import { env } from "./env.server";
|
||||||
@ -16,72 +8,11 @@ import { singleton } from "./utils/singleton";
|
|||||||
|
|
||||||
import { type Span } from "@opentelemetry/api";
|
import { type Span } from "@opentelemetry/api";
|
||||||
|
|
||||||
export type {
|
|
||||||
PrismaTransactionClient,
|
|
||||||
PrismaClientOrTransaction,
|
|
||||||
PrismaTransactionOptions,
|
|
||||||
PrismaReplicaClient,
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function $transaction<R>(
|
|
||||||
prisma: PrismaClientOrTransaction,
|
|
||||||
name: string,
|
|
||||||
fn: (prisma: PrismaTransactionClient, span?: Span) => Promise<R>,
|
|
||||||
options?: PrismaTransactionOptions,
|
|
||||||
): Promise<R | undefined>;
|
|
||||||
export async function $transaction<R>(
|
|
||||||
prisma: PrismaClientOrTransaction,
|
|
||||||
fn: (prisma: PrismaTransactionClient) => Promise<R>,
|
|
||||||
options?: PrismaTransactionOptions,
|
|
||||||
): Promise<R | undefined>;
|
|
||||||
export async function $transaction<R>(
|
|
||||||
prisma: PrismaClientOrTransaction,
|
|
||||||
fnOrName: ((prisma: PrismaTransactionClient) => Promise<R>) | string,
|
|
||||||
fnOrOptions?:
|
|
||||||
| ((prisma: PrismaTransactionClient) => Promise<R>)
|
|
||||||
| PrismaTransactionOptions,
|
|
||||||
options?: PrismaTransactionOptions,
|
|
||||||
): Promise<R | undefined> {
|
|
||||||
if (typeof fnOrName === "string") {
|
|
||||||
const fn = fnOrOptions as (prisma: PrismaTransactionClient) => Promise<R>;
|
|
||||||
|
|
||||||
return await transac(
|
|
||||||
prisma,
|
|
||||||
(client) => fn(client),
|
|
||||||
(error) => {
|
|
||||||
logger.error("prisma.$transaction error", {
|
|
||||||
code: error.code,
|
|
||||||
meta: error.meta,
|
|
||||||
stack: error.stack,
|
|
||||||
message: error.message,
|
|
||||||
name: error.name,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return transac(
|
|
||||||
prisma,
|
|
||||||
fnOrName,
|
|
||||||
(error) => {
|
|
||||||
logger.error("prisma.$transaction error", {
|
|
||||||
code: error.code,
|
|
||||||
meta: error.meta,
|
|
||||||
stack: error.stack,
|
|
||||||
message: error.message,
|
|
||||||
name: error.name,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
typeof fnOrOptions === "function" ? undefined : fnOrOptions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Prisma };
|
export { Prisma };
|
||||||
|
|
||||||
export const prisma = singleton("prisma", getClient);
|
export const prisma = singleton("prisma", getClient);
|
||||||
|
|
||||||
export const $replica: PrismaReplicaClient = singleton(
|
export const $replica = singleton(
|
||||||
"replica",
|
"replica",
|
||||||
() => getReplicaClient() ?? prisma,
|
() => getReplicaClient() ?? prisma,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import { RemixServer } from "@remix-run/react";
|
|||||||
import { isbot } from "isbot";
|
import { isbot } from "isbot";
|
||||||
import { renderToPipeableStream } from "react-dom/server";
|
import { renderToPipeableStream } from "react-dom/server";
|
||||||
import { initializeStartupServices } from "./utils/startup";
|
import { initializeStartupServices } from "./utils/startup";
|
||||||
|
import { handleMCPRequest, handleSessionRequest } from "~/services/mcp.server";
|
||||||
|
import { authenticateHybridRequest } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
|
|
||||||
const ABORT_DELAY = 5_000;
|
const ABORT_DELAY = 5_000;
|
||||||
|
|
||||||
@ -149,3 +151,5 @@ function handleBrowserRequest(
|
|||||||
setTimeout(abort, ABORT_DELAY);
|
setTimeout(abort, ABORT_DELAY);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { handleMCPRequest, handleSessionRequest, authenticateHybridRequest };
|
||||||
|
|||||||
@ -1,33 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
import { json } from "@remix-run/node";
|
import { json } from "@remix-run/node";
|
||||||
import { searchMemoryAgent } from "~/agents/searchMemoryAgent.server";
|
import { extensionSearch } from "~/trigger/extension/search";
|
||||||
|
|
||||||
export const ExtensionSearchBodyRequest = z.object({
|
export const ExtensionSearchBodyRequest = z.object({
|
||||||
input: z.string().min(1, "Input text is required"),
|
input: z.string().min(1, "Input text is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate context summary from user input using SearchMemoryAgent
|
|
||||||
*/
|
|
||||||
async function generateContextSummary(
|
|
||||||
userInput: string,
|
|
||||||
userId: string,
|
|
||||||
): Promise<string> {
|
|
||||||
try {
|
|
||||||
const summary = await searchMemoryAgent.generateContextSummary({
|
|
||||||
userInput,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return summary;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error generating context with agent:", error);
|
|
||||||
// Fallback: use simple context description
|
|
||||||
return `Context related to: ${userInput}. Looking for relevant background information, previous discussions, and related concepts that would help provide a comprehensive answer.`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { action, loader } = createActionApiRoute(
|
const { action, loader } = createActionApiRoute(
|
||||||
{
|
{
|
||||||
body: ExtensionSearchBodyRequest,
|
body: ExtensionSearchBodyRequest,
|
||||||
@ -39,18 +18,12 @@ const { action, loader } = createActionApiRoute(
|
|||||||
corsStrategy: "all",
|
corsStrategy: "all",
|
||||||
},
|
},
|
||||||
async ({ body, authentication }) => {
|
async ({ body, authentication }) => {
|
||||||
// Generate context summary using SearchMemoryAgent
|
const trigger = await extensionSearch.trigger({
|
||||||
const contextSummary = await generateContextSummary(
|
userInput: body.input,
|
||||||
body.input,
|
userId: authentication.userId,
|
||||||
authentication.userId,
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Return results with agent-generated context summary
|
return json(trigger);
|
||||||
const finalResults = {
|
|
||||||
context_summary: contextSummary, // Agent's context summary
|
|
||||||
};
|
|
||||||
|
|
||||||
return json(finalResults);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
import { makeModelCall } from "~/lib/model.server";
|
|
||||||
import { json } from "@remix-run/node";
|
import { json } from "@remix-run/node";
|
||||||
import type { CoreMessage } from "ai";
|
import { extensionSummary } from "~/trigger/extension/summary";
|
||||||
import * as cheerio from "cheerio";
|
|
||||||
|
|
||||||
export const ExtensionSummaryBodyRequest = z.object({
|
export const ExtensionSummaryBodyRequest = z.object({
|
||||||
html: z.string().min(1, "HTML content is required"),
|
html: z.string().min(1, "HTML content is required"),
|
||||||
@ -11,163 +9,6 @@ export const ExtensionSummaryBodyRequest = z.object({
|
|||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type PageType = "text" | "video";
|
|
||||||
|
|
||||||
interface ContentExtractionResult {
|
|
||||||
pageType: PageType;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
metadata: {
|
|
||||||
url: string;
|
|
||||||
wordCount: number;
|
|
||||||
};
|
|
||||||
supported: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if page contains video content
|
|
||||||
*/
|
|
||||||
function isVideoPage(url: string, $: cheerio.CheerioAPI): boolean {
|
|
||||||
const hostname = new URL(url).hostname.toLowerCase();
|
|
||||||
|
|
||||||
// Known video platforms
|
|
||||||
if (
|
|
||||||
hostname.includes("youtube.com") ||
|
|
||||||
hostname.includes("youtu.be") ||
|
|
||||||
hostname.includes("vimeo.com") ||
|
|
||||||
hostname.includes("twitch.tv") ||
|
|
||||||
hostname.includes("tiktok.com")
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic video content detection
|
|
||||||
const videoElements = $("video").length;
|
|
||||||
const videoPlayers = $(
|
|
||||||
'.video-player, [class*="video-player"], [data-testid*="video"]',
|
|
||||||
).length;
|
|
||||||
|
|
||||||
// If there are multiple video indicators, likely a video-focused page
|
|
||||||
return videoElements > 0 || videoPlayers > 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract all text content from any webpage
|
|
||||||
*/
|
|
||||||
function extractTextContent(
|
|
||||||
$: cheerio.CheerioAPI,
|
|
||||||
url: string,
|
|
||||||
): ContentExtractionResult {
|
|
||||||
// Extract title from multiple possible locations
|
|
||||||
const title =
|
|
||||||
$("title").text() ||
|
|
||||||
$('meta[property="og:title"]').attr("content") ||
|
|
||||||
$('meta[name="title"]').attr("content") ||
|
|
||||||
$("h1").first().text() ||
|
|
||||||
"Untitled Page";
|
|
||||||
|
|
||||||
// Check if this is primarily a video page
|
|
||||||
const isVideo = isVideoPage(url, $);
|
|
||||||
const pageType: PageType = isVideo ? "video" : "text";
|
|
||||||
|
|
||||||
let content = "";
|
|
||||||
|
|
||||||
if (isVideo) {
|
|
||||||
// For video pages, try to get description/transcript text
|
|
||||||
content =
|
|
||||||
$("#description, .video-description, .description").text() ||
|
|
||||||
$('meta[name="description"]').attr("content") ||
|
|
||||||
$('[class*="transcript"], [class*="caption"]').text() ||
|
|
||||||
"Video content detected - text summarization not available";
|
|
||||||
} else {
|
|
||||||
// Simple universal text extraction
|
|
||||||
// Remove non-content elements
|
|
||||||
$("script, style, noscript, nav, header, footer").remove();
|
|
||||||
|
|
||||||
// Get all text content
|
|
||||||
const allText = $("body").text();
|
|
||||||
|
|
||||||
// Split into sentences and filter for meaningful content
|
|
||||||
const sentences = allText
|
|
||||||
.split(/[.!?]+/)
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter((s) => s.length > 20) // Keep sentences with substance
|
|
||||||
.filter(
|
|
||||||
(s) =>
|
|
||||||
!/^(click|menu|button|nav|home|search|login|signup|subscribe)$/i.test(
|
|
||||||
s.toLowerCase(),
|
|
||||||
),
|
|
||||||
) // Remove UI text
|
|
||||||
.filter((s) => s.split(" ").length > 3); // Keep sentences with multiple words
|
|
||||||
|
|
||||||
content = sentences.join(". ").slice(0, 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up whitespace and normalize text
|
|
||||||
content = content.replace(/\s+/g, " ").trim();
|
|
||||||
|
|
||||||
const wordCount = content
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter((word) => word.length > 0).length;
|
|
||||||
const supported = !isVideo && content.length > 50;
|
|
||||||
|
|
||||||
return {
|
|
||||||
pageType,
|
|
||||||
title: title.trim(),
|
|
||||||
content: content.slice(0, 10000), // Limit content size for processing
|
|
||||||
metadata: {
|
|
||||||
url,
|
|
||||||
wordCount,
|
|
||||||
},
|
|
||||||
supported,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate summary using LLM
|
|
||||||
*/
|
|
||||||
async function generateSummary(
|
|
||||||
title: string,
|
|
||||||
content: string,
|
|
||||||
): Promise<string> {
|
|
||||||
const messages: CoreMessage[] = [
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content: `You are a helpful assistant that creates concise summaries of web content in HTML format.
|
|
||||||
|
|
||||||
Create a clear, informative summary that captures the key points and main ideas from the provided content. The summary should:
|
|
||||||
- Focus on the most important information and key takeaways
|
|
||||||
- Be concise but comprehensive
|
|
||||||
- Maintain the original context and meaning
|
|
||||||
- Be useful for someone who wants to quickly understand the content
|
|
||||||
- Format the summary in HTML, using appropriate tags like <h1>, <p>, <ul>, <li> to structure the information
|
|
||||||
|
|
||||||
Extract the essential information while preserving important details, facts, or insights.`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: `Title: ${title}
|
|
||||||
Content: ${content}
|
|
||||||
|
|
||||||
Please provide a concise summary of this content in HTML format.`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await makeModelCall(
|
|
||||||
false,
|
|
||||||
messages,
|
|
||||||
() => {}, // onFinish callback
|
|
||||||
{ temperature: 0.3 },
|
|
||||||
);
|
|
||||||
|
|
||||||
return response as string;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error generating summary:", error);
|
|
||||||
return "<p>Unable to generate summary at this time.</p>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { action, loader } = createActionApiRoute(
|
const { action, loader } = createActionApiRoute(
|
||||||
{
|
{
|
||||||
body: ExtensionSummaryBodyRequest,
|
body: ExtensionSummaryBodyRequest,
|
||||||
@ -178,64 +19,9 @@ const { action, loader } = createActionApiRoute(
|
|||||||
corsStrategy: "all",
|
corsStrategy: "all",
|
||||||
},
|
},
|
||||||
async ({ body }) => {
|
async ({ body }) => {
|
||||||
try {
|
const response = await extensionSummary.trigger(body);
|
||||||
const $ = cheerio.load(body.html);
|
|
||||||
|
|
||||||
// Extract content from any webpage
|
return json(response);
|
||||||
const extraction = extractTextContent($, body.url);
|
|
||||||
|
|
||||||
// Override title if provided
|
|
||||||
if (body.title) {
|
|
||||||
extraction.title = body.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
let summary = "";
|
|
||||||
|
|
||||||
if (extraction.supported && extraction.content.length > 0) {
|
|
||||||
// Generate summary for text content
|
|
||||||
summary = await generateSummary(extraction.title, extraction.content);
|
|
||||||
} else {
|
|
||||||
// Handle unsupported content types
|
|
||||||
if (extraction.pageType === "video") {
|
|
||||||
summary =
|
|
||||||
"Video content detected. Text summarization not available for video-focused pages.";
|
|
||||||
} else {
|
|
||||||
summary =
|
|
||||||
"Unable to extract sufficient text content for summarization.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
success: true,
|
|
||||||
pageType: extraction.pageType,
|
|
||||||
title: extraction.title,
|
|
||||||
summary,
|
|
||||||
content: extraction.content.slice(0, 1000), // Return first 1000 chars of content
|
|
||||||
supported: extraction.supported,
|
|
||||||
metadata: extraction.metadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
return json(response);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing extension summary request:", error);
|
|
||||||
|
|
||||||
return json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: "Failed to process page content",
|
|
||||||
pageType: "text" as PageType,
|
|
||||||
title: body.title || "Error",
|
|
||||||
summary: "Unable to process this page content.",
|
|
||||||
content: "",
|
|
||||||
supported: false,
|
|
||||||
metadata: {
|
|
||||||
url: body.url,
|
|
||||||
wordCount: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
0
apps/webapp/app/routes/ingest.tsx
Normal file
0
apps/webapp/app/routes/ingest.tsx
Normal file
@ -1,6 +1,6 @@
|
|||||||
import { findUserByToken } from "~/models/personal-token.server";
|
import { findUserByToken } from "~/models/personal-token.server";
|
||||||
import { oauth2Service } from "~/services/oauth2.server";
|
import { oauth2Service } from "~/services/oauth2.server";
|
||||||
|
import { type Request as ERequest } from "express";
|
||||||
// See this for more: https://twitter.com/mattpocockuk/status/1653403198885904387?s=20
|
// See this for more: https://twitter.com/mattpocockuk/status/1653403198885904387?s=20
|
||||||
export type Prettify<T> = {
|
export type Prettify<T> = {
|
||||||
[K in keyof T]: T[K];
|
[K in keyof T]: T[K];
|
||||||
@ -111,8 +111,13 @@ export function isSecretApiKey(key: string) {
|
|||||||
return key.startsWith("rc_");
|
return key.startsWith("rc_");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApiKeyFromRequest(request: Request) {
|
export function getApiKeyFromRequest(request: Request | ERequest) {
|
||||||
return getApiKeyFromHeader(request.headers.get("Authorization"));
|
const authorizationHeader =
|
||||||
|
request instanceof Request
|
||||||
|
? request.headers.get("Authorization")
|
||||||
|
: request.headers["authorization"];
|
||||||
|
|
||||||
|
return getApiKeyFromHeader(authorizationHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApiKeyFromHeader(authorization?: string | null) {
|
export function getApiKeyFromHeader(authorization?: string | null) {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { createCookieSessionStorage, type Session } from "@remix-run/node";
|
import { createCookieSessionStorage, type Session } from "@remix-run/node";
|
||||||
import { env } from "~/env.server";
|
import { env } from "~/env.server";
|
||||||
|
import { type Request as ERequest } from "express";
|
||||||
|
|
||||||
export const impersonationSessionStorage = createCookieSessionStorage({
|
export const impersonationSessionStorage = createCookieSessionStorage({
|
||||||
cookie: {
|
cookie: {
|
||||||
@ -13,8 +14,13 @@ export const impersonationSessionStorage = createCookieSessionStorage({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getImpersonationSession(request: Request) {
|
export function getImpersonationSession(request: Request | ERequest) {
|
||||||
return impersonationSessionStorage.getSession(request.headers.get("Cookie"));
|
const cookieHeader =
|
||||||
|
request instanceof Request
|
||||||
|
? request.headers.get("Cookie")
|
||||||
|
: request.headers["cookie"];
|
||||||
|
|
||||||
|
return impersonationSessionStorage.getSession(cookieHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function commitImpersonationSession(session: Session) {
|
export function commitImpersonationSession(session: Session) {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { json } from "@remix-run/node";
|
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
@ -8,87 +7,18 @@ import {
|
|||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
|
||||||
createHybridActionApiRoute,
|
|
||||||
createLoaderApiRoute,
|
|
||||||
} from "~/services/routeBuilders/apiBuilder.server";
|
|
||||||
import { handleTransport } from "~/utils/mcp";
|
|
||||||
import { MCPSessionManager } from "~/utils/mcp/session-manager";
|
import { MCPSessionManager } from "~/utils/mcp/session-manager";
|
||||||
import { TransportManager } from "~/utils/mcp/transport-manager";
|
import { TransportManager } from "~/utils/mcp/transport-manager";
|
||||||
import { IntegrationLoader } from "~/utils/mcp/integration-loader";
|
import { IntegrationLoader } from "~/utils/mcp/integration-loader";
|
||||||
import { callMemoryTool, memoryTools } from "~/utils/mcp/memory";
|
import { callMemoryTool, memoryTools } from "~/utils/mcp/memory";
|
||||||
import { logger } from "~/services/logger.service";
|
import { logger } from "~/services/logger.service";
|
||||||
|
import { type Response, type Request } from "express";
|
||||||
|
|
||||||
// Request schemas
|
|
||||||
const MCPRequestSchema = z.object({}).passthrough();
|
|
||||||
const QueryParams = z.object({
|
const QueryParams = z.object({
|
||||||
source: z.string().optional(),
|
source: z.string().optional(),
|
||||||
integrations: z.string().optional(), // comma-separated slugs
|
integrations: z.string().optional(), // comma-separated slugs
|
||||||
});
|
});
|
||||||
|
|
||||||
// Common function to create and setup transport
|
|
||||||
async function createTransport(
|
|
||||||
sessionId: string,
|
|
||||||
source: string,
|
|
||||||
integrations: string[],
|
|
||||||
userId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<StreamableHTTPServerTransport> {
|
|
||||||
const transport = new StreamableHTTPServerTransport({
|
|
||||||
sessionIdGenerator: () => sessionId,
|
|
||||||
onsessioninitialized: async (sessionId) => {
|
|
||||||
// Clean up old sessions (24+ hours) during new session initialization
|
|
||||||
try {
|
|
||||||
const [dbCleanupCount, memoryCleanupCount] = await Promise.all([
|
|
||||||
MCPSessionManager.cleanupOldSessions(),
|
|
||||||
TransportManager.cleanupOldSessions(),
|
|
||||||
]);
|
|
||||||
if (dbCleanupCount > 0 || memoryCleanupCount > 0) {
|
|
||||||
logger.log(`Cleaned up ${dbCleanupCount} DB sessions and ${memoryCleanupCount} memory sessions`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error during session cleanup: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store session in database
|
|
||||||
await MCPSessionManager.upsertSession(sessionId, source, integrations);
|
|
||||||
|
|
||||||
// Store main transport
|
|
||||||
TransportManager.setMainTransport(sessionId, transport);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup cleanup on close
|
|
||||||
transport.onclose = async () => {
|
|
||||||
await MCPSessionManager.deleteSession(sessionId);
|
|
||||||
await TransportManager.cleanupSession(sessionId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load integration transports
|
|
||||||
try {
|
|
||||||
const result = await IntegrationLoader.loadIntegrationTransports(
|
|
||||||
sessionId,
|
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
integrations.length > 0 ? integrations : undefined,
|
|
||||||
);
|
|
||||||
logger.log(
|
|
||||||
`Loaded ${result.loaded} integration transports for session ${sessionId}`,
|
|
||||||
);
|
|
||||||
if (result.failed.length > 0) {
|
|
||||||
logger.warn(`Failed to load some integrations: ${result.failed}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error loading integration transports: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and connect MCP server
|
|
||||||
const server = await createMcpServer(userId, sessionId);
|
|
||||||
await server.connect(transport);
|
|
||||||
|
|
||||||
return transport;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create MCP server with memory tools + dynamic integration tools
|
// Create MCP server with memory tools + dynamic integration tools
|
||||||
async function createMcpServer(userId: string, sessionId: string) {
|
async function createMcpServer(userId: string, sessionId: string) {
|
||||||
const server = new Server(
|
const server = new Server(
|
||||||
@ -152,49 +82,82 @@ async function createMcpServer(userId: string, sessionId: string) {
|
|||||||
throw new Error(`Unknown tool: ${name}`);
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
||||||
const { name, arguments: args } = request.params;
|
|
||||||
|
|
||||||
// Handle memory tools
|
|
||||||
if (name.startsWith("memory_")) {
|
|
||||||
return await callMemoryTool(name, args, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle integration tools (prefixed with integration slug)
|
|
||||||
if (name.includes("_") && !name.startsWith("memory_")) {
|
|
||||||
try {
|
|
||||||
return await IntegrationLoader.callIntegrationTool(
|
|
||||||
sessionId,
|
|
||||||
name,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Error calling integration tool: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unknown tool: ${name}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle MCP requests
|
// Common function to create and setup transport
|
||||||
const handleMCPRequest = async (
|
async function createTransport(
|
||||||
|
sessionId: string,
|
||||||
|
source: string,
|
||||||
|
integrations: string[],
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<StreamableHTTPServerTransport> {
|
||||||
|
const transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: () => sessionId,
|
||||||
|
onsessioninitialized: async (sessionId) => {
|
||||||
|
// Clean up old sessions (24+ hours) during new session initialization
|
||||||
|
try {
|
||||||
|
const [dbCleanupCount, memoryCleanupCount] = await Promise.all([
|
||||||
|
MCPSessionManager.cleanupOldSessions(),
|
||||||
|
TransportManager.cleanupOldSessions(),
|
||||||
|
]);
|
||||||
|
if (dbCleanupCount > 0 || memoryCleanupCount > 0) {
|
||||||
|
logger.log(
|
||||||
|
`Cleaned up ${dbCleanupCount} DB sessions and ${memoryCleanupCount} memory sessions`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error during session cleanup: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store session in database
|
||||||
|
await MCPSessionManager.upsertSession(sessionId, source, integrations);
|
||||||
|
|
||||||
|
// Store main transport
|
||||||
|
TransportManager.setMainTransport(sessionId, transport);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup cleanup on close
|
||||||
|
transport.onclose = async () => {
|
||||||
|
await MCPSessionManager.deleteSession(sessionId);
|
||||||
|
await TransportManager.cleanupSession(sessionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load integration transports
|
||||||
|
try {
|
||||||
|
const result = await IntegrationLoader.loadIntegrationTransports(
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
integrations.length > 0 ? integrations : undefined,
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
`Loaded ${result.loaded} integration transports for session ${sessionId}`,
|
||||||
|
);
|
||||||
|
if (result.failed.length > 0) {
|
||||||
|
logger.warn(`Failed to load some integrations: ${result.failed}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error loading integration transports: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and connect MCP server
|
||||||
|
const server = await createMcpServer(userId, sessionId);
|
||||||
|
await server.connect(transport);
|
||||||
|
|
||||||
|
return transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleMCPRequest = async (
|
||||||
request: Request,
|
request: Request,
|
||||||
|
res: Response,
|
||||||
body: any,
|
body: any,
|
||||||
authentication: any,
|
authentication: any,
|
||||||
queryParams: z.infer<typeof QueryParams>,
|
queryParams: z.infer<typeof QueryParams>,
|
||||||
) => {
|
) => {
|
||||||
const sessionId = request.headers.get("mcp-session-id") as string | undefined;
|
const sessionId = request.headers["mcp-session-id"] as string | undefined;
|
||||||
const source = queryParams.source || "api";
|
const source = queryParams.source || "api";
|
||||||
const integrations = queryParams.integrations
|
const integrations = queryParams.integrations
|
||||||
? queryParams.integrations.split(",").map((s) => s.trim())
|
? queryParams.integrations.split(",").map((s) => s.trim())
|
||||||
@ -240,134 +203,34 @@ const handleMCPRequest = async (
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Invalid request
|
// Invalid request
|
||||||
return json(
|
throw new Error("No session id");
|
||||||
{
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
error: {
|
|
||||||
code: -32000,
|
|
||||||
message:
|
|
||||||
"Bad Request: No valid session ID provided or session inactive",
|
|
||||||
},
|
|
||||||
id: body?.id || null,
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the request through existing transport utility
|
// Handle the request through existing transport utility
|
||||||
const response = await handleTransport(transport!, request, body);
|
return await transport.handleRequest(request, res, body);
|
||||||
return response;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("MCP SSE request error:", error);
|
console.error("MCP SSE request error:", error);
|
||||||
return json(
|
throw new Error("MCP SSE request error");
|
||||||
{
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
error: {
|
|
||||||
code: -32000,
|
|
||||||
message:
|
|
||||||
error instanceof Error ? error.message : "Internal server error",
|
|
||||||
},
|
|
||||||
id: body?.id || null,
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle DELETE requests for session cleanup
|
export const handleSessionRequest = async (req: Request, res: Response) => {
|
||||||
const handleDelete = async (request: Request) => {
|
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||||
const sessionId = request.headers.get("mcp-session-id") as string | undefined;
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
return new Response("Missing session ID", { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Mark session as deleted in database
|
|
||||||
await MCPSessionManager.deleteSession(sessionId);
|
|
||||||
|
|
||||||
// Clean up all transports
|
|
||||||
await TransportManager.cleanupSession(sessionId);
|
|
||||||
|
|
||||||
return new Response(null, { status: 204 });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting session:", error);
|
|
||||||
return new Response("Internal server error", { status: 500 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { action } = createHybridActionApiRoute(
|
|
||||||
{
|
|
||||||
body: MCPRequestSchema,
|
|
||||||
searchParams: QueryParams,
|
|
||||||
allowJWT: true,
|
|
||||||
authorization: {
|
|
||||||
action: "mcp",
|
|
||||||
},
|
|
||||||
corsStrategy: "all",
|
|
||||||
},
|
|
||||||
async ({ body, authentication, request, searchParams }) => {
|
|
||||||
const method = request.method;
|
|
||||||
|
|
||||||
if (method === "POST") {
|
|
||||||
return await handleMCPRequest(
|
|
||||||
request,
|
|
||||||
body,
|
|
||||||
authentication,
|
|
||||||
searchParams,
|
|
||||||
);
|
|
||||||
} else if (method === "DELETE") {
|
|
||||||
return await handleDelete(request);
|
|
||||||
} else {
|
|
||||||
return json(
|
|
||||||
{
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
error: {
|
|
||||||
code: -32601,
|
|
||||||
message: "Method not allowed",
|
|
||||||
},
|
|
||||||
id: null,
|
|
||||||
},
|
|
||||||
{ status: 405 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const loader = createLoaderApiRoute(
|
|
||||||
{
|
|
||||||
allowJWT: true,
|
|
||||||
corsStrategy: "all",
|
|
||||||
findResource: async () => 1,
|
|
||||||
},
|
|
||||||
async ({ request }) => {
|
|
||||||
// Handle SSE requests (for server-to-client notifications)
|
|
||||||
const sessionId = request.headers.get("mcp-session-id");
|
|
||||||
if (!sessionId) {
|
|
||||||
return new Response("Missing session ID for SSE", { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (sessionId && (await MCPSessionManager.isSessionActive(sessionId))) {
|
||||||
const sessionData = TransportManager.getSessionInfo(sessionId);
|
const sessionData = TransportManager.getSessionInfo(sessionId);
|
||||||
if (!sessionData.exists) {
|
|
||||||
// Check if session exists in database and recreate transport
|
|
||||||
const sessionDetails = await MCPSessionManager.getSession(sessionId);
|
|
||||||
if (!sessionDetails) {
|
|
||||||
return new Response("Session not found", { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session exists in DB but not in memory - need authentication to recreate
|
if (sessionData.exists) {
|
||||||
return new Response("Session not found", { status: 404 });
|
const transport =
|
||||||
|
sessionData.mainTransport as StreamableHTTPServerTransport;
|
||||||
|
|
||||||
|
await transport.handleRequest(req, res);
|
||||||
|
} else {
|
||||||
|
res.status(400).send("Invalid or missing session ID");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Return SSE stream (this would be handled by the transport's handleRequest method)
|
res.status(400).send("Invalid or missing session ID");
|
||||||
// For now, just return session info
|
return;
|
||||||
return json({
|
}
|
||||||
sessionId,
|
};
|
||||||
active: await MCPSessionManager.isSessionActive(sessionId),
|
|
||||||
integrationCount: sessionData.integrationCount,
|
|
||||||
createdAt: sessionData.createdAt,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export { action, loader };
|
|
||||||
@ -643,7 +643,7 @@ export type HybridAuthenticationResult =
|
|||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function authenticateHybridRequest(
|
export async function authenticateHybridRequest(
|
||||||
request: Request,
|
request: Request,
|
||||||
options: { allowJWT?: boolean } = {},
|
options: { allowJWT?: boolean } = {},
|
||||||
): Promise<HybridAuthenticationResult | null> {
|
): Promise<HybridAuthenticationResult | null> {
|
||||||
|
|||||||
@ -2,9 +2,7 @@ import type { EpisodicNode, StatementNode } from "@core/types";
|
|||||||
import { logger } from "./logger.service";
|
import { logger } from "./logger.service";
|
||||||
import {
|
import {
|
||||||
applyCrossEncoderReranking,
|
applyCrossEncoderReranking,
|
||||||
applyMultiFactorReranking,
|
|
||||||
applyMultiFactorMMRReranking,
|
applyMultiFactorMMRReranking,
|
||||||
applyWeightedRRF,
|
|
||||||
} from "./search/rerank";
|
} from "./search/rerank";
|
||||||
import {
|
import {
|
||||||
getEpisodesByStatements,
|
getEpisodesByStatements,
|
||||||
|
|||||||
@ -3,13 +3,21 @@ import { getUserById, getUserLeftCredits } from "~/models/user.server";
|
|||||||
import { sessionStorage } from "./sessionStorage.server";
|
import { sessionStorage } from "./sessionStorage.server";
|
||||||
import { getImpersonationId } from "./impersonation.server";
|
import { getImpersonationId } from "./impersonation.server";
|
||||||
import { getWorkspaceByUser } from "~/models/workspace.server";
|
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||||
|
import { type Request as ERequest } from "express";
|
||||||
|
|
||||||
export async function getUserId(request: Request): Promise<string | undefined> {
|
export async function getUserId(
|
||||||
const impersonatedUserId = await getImpersonationId(request);
|
request: Request | ERequest,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const impersonatedUserId = await getImpersonationId(request as Request);
|
||||||
|
|
||||||
if (impersonatedUserId) return impersonatedUserId;
|
if (impersonatedUserId) return impersonatedUserId;
|
||||||
|
|
||||||
let session = await sessionStorage.getSession(request.headers.get("cookie"));
|
const cookieHeader =
|
||||||
|
request instanceof Request
|
||||||
|
? request.headers.get("Cookie")
|
||||||
|
: request.headers["cookie"];
|
||||||
|
|
||||||
|
let session = await sessionStorage.getSession(cookieHeader);
|
||||||
let user = session.get("user");
|
let user = session.get("user");
|
||||||
|
|
||||||
return user?.userId;
|
return user?.userId;
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
|
import { metadata, task } from "@trigger.dev/sdk";
|
||||||
|
import { streamText, type CoreMessage, tool } from "ai";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { openai } from "@ai-sdk/openai";
|
|
||||||
import { type CoreMessage, generateText, tool } from "ai";
|
|
||||||
import { logger } from "~/services/logger.service";
|
|
||||||
import { SearchService } from "~/services/search.server";
|
|
||||||
|
|
||||||
// Input schema for the agent
|
import { openai } from "@ai-sdk/openai";
|
||||||
export const SearchMemoryAgentInput = z.object({
|
import { logger } from "~/services/logger.service";
|
||||||
|
import {
|
||||||
|
deletePersonalAccessToken,
|
||||||
|
getOrCreatePersonalAccessToken,
|
||||||
|
} from "../utils/utils";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const ExtensionSearchBodyRequest = z.object({
|
||||||
userInput: z.string().min(1, "User input is required"),
|
userInput: z.string().min(1, "User input is required"),
|
||||||
userId: z.string().min(1, "User ID is required"),
|
userId: z.string().min(1, "User ID is required"),
|
||||||
context: z
|
context: z
|
||||||
@ -14,20 +19,18 @@ export const SearchMemoryAgentInput = z.object({
|
|||||||
.describe("Additional context about the user's current work"),
|
.describe("Additional context about the user's current work"),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
// Export a singleton instance
|
||||||
* Search Memory Agent - Designed to find relevant context from user's memory
|
export const extensionSearch = task({
|
||||||
*
|
id: "extensionSearch",
|
||||||
* This agent searches the user's memory using a searchMemory tool, retrieves relevant
|
maxDuration: 3000,
|
||||||
* facts and episodes, then summarizes them into a concise, relevant context summary.
|
run: async (body: z.infer<typeof ExtensionSearchBodyRequest>) => {
|
||||||
*/
|
const { userInput, userId, context } =
|
||||||
export class SearchMemoryAgent {
|
ExtensionSearchBodyRequest.parse(body);
|
||||||
private model = openai("gpt-4o");
|
|
||||||
private searchService = new SearchService();
|
|
||||||
|
|
||||||
async generateContextSummary(
|
const pat = await getOrCreatePersonalAccessToken({
|
||||||
input: z.infer<typeof SearchMemoryAgentInput>,
|
name: "extensionSearch",
|
||||||
): Promise<string> {
|
userId: userId as string,
|
||||||
const { userInput, userId, context } = SearchMemoryAgentInput.parse(input);
|
});
|
||||||
|
|
||||||
// Define the searchMemory tool that actually calls the search service
|
// Define the searchMemory tool that actually calls the search service
|
||||||
const searchMemoryTool = tool({
|
const searchMemoryTool = tool({
|
||||||
@ -38,7 +41,16 @@ export class SearchMemoryAgent {
|
|||||||
}),
|
}),
|
||||||
execute: async ({ query }) => {
|
execute: async ({ query }) => {
|
||||||
try {
|
try {
|
||||||
const searchResult = await this.searchService.search(query, userId);
|
const response = await axios.post(
|
||||||
|
`${process.env.API_BASE_URL}/api/v1/search`,
|
||||||
|
{ query },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${pat.token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const searchResult = response.data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
facts: searchResult.facts || [],
|
facts: searchResult.facts || [],
|
||||||
@ -79,8 +91,8 @@ If no relevant information is found, provide a brief statement indicating that.`
|
|||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await generateText({
|
const result = streamText({
|
||||||
model: this.model,
|
model: openai(process.env.MODEL as string),
|
||||||
messages,
|
messages,
|
||||||
tools: {
|
tools: {
|
||||||
searchMemory: searchMemoryTool,
|
searchMemory: searchMemoryTool,
|
||||||
@ -90,14 +102,21 @@ If no relevant information is found, provide a brief statement indicating that.`
|
|||||||
maxTokens: 600,
|
maxTokens: 600,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.text.trim();
|
const stream = await metadata.stream("messages", result.textStream);
|
||||||
|
|
||||||
|
let finalText: string = "";
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
finalText = finalText + chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
await deletePersonalAccessToken(pat.id);
|
||||||
|
|
||||||
|
return finalText;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`SearchMemoryAgent error: ${error}`);
|
logger.error(`SearchMemoryAgent error: ${error}`);
|
||||||
|
await deletePersonalAccessToken(pat.id);
|
||||||
|
|
||||||
return `Context related to: ${userInput}. Looking for relevant background information, previous discussions, and related concepts that would help provide a comprehensive answer.`;
|
return `Context related to: ${userInput}. Looking for relevant background information, previous discussions, and related concepts that would help provide a comprehensive answer.`;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
// Export a singleton instance
|
|
||||||
export const searchMemoryAgent = new SearchMemoryAgent();
|
|
||||||
234
apps/webapp/app/trigger/extension/summary.ts
Normal file
234
apps/webapp/app/trigger/extension/summary.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import { metadata, task } from "@trigger.dev/sdk";
|
||||||
|
import { type CoreMessage } from "ai";
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { makeModelCall } from "~/lib/model.server";
|
||||||
|
|
||||||
|
export type PageType = "text" | "video";
|
||||||
|
|
||||||
|
export const ExtensionSummaryBodyRequest = z.object({
|
||||||
|
html: z.string().min(1, "HTML content is required"),
|
||||||
|
url: z.string().url("Valid URL is required"),
|
||||||
|
title: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ContentExtractionResult {
|
||||||
|
pageType: PageType;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
metadata: {
|
||||||
|
url: string;
|
||||||
|
wordCount: number;
|
||||||
|
};
|
||||||
|
supported: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if page contains video content
|
||||||
|
*/
|
||||||
|
function isVideoPage(url: string, $: cheerio.CheerioAPI): boolean {
|
||||||
|
const hostname = new URL(url).hostname.toLowerCase();
|
||||||
|
|
||||||
|
// Known video platforms
|
||||||
|
if (
|
||||||
|
hostname.includes("youtube.com") ||
|
||||||
|
hostname.includes("youtu.be") ||
|
||||||
|
hostname.includes("vimeo.com") ||
|
||||||
|
hostname.includes("twitch.tv") ||
|
||||||
|
hostname.includes("tiktok.com")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic video content detection
|
||||||
|
const videoElements = $("video").length;
|
||||||
|
const videoPlayers = $(
|
||||||
|
'.video-player, [class*="video-player"], [data-testid*="video"]',
|
||||||
|
).length;
|
||||||
|
|
||||||
|
// If there are multiple video indicators, likely a video-focused page
|
||||||
|
return videoElements > 0 || videoPlayers > 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract all text content from any webpage
|
||||||
|
*/
|
||||||
|
function extractTextContent(
|
||||||
|
$: cheerio.CheerioAPI,
|
||||||
|
url: string,
|
||||||
|
): ContentExtractionResult {
|
||||||
|
// Extract title from multiple possible locations
|
||||||
|
const title =
|
||||||
|
$("title").text() ||
|
||||||
|
$('meta[property="og:title"]').attr("content") ||
|
||||||
|
$('meta[name="title"]').attr("content") ||
|
||||||
|
$("h1").first().text() ||
|
||||||
|
"Untitled Page";
|
||||||
|
|
||||||
|
// Check if this is primarily a video page
|
||||||
|
const isVideo = isVideoPage(url, $);
|
||||||
|
const pageType: PageType = isVideo ? "video" : "text";
|
||||||
|
|
||||||
|
let content = "";
|
||||||
|
|
||||||
|
if (isVideo) {
|
||||||
|
// For video pages, try to get description/transcript text
|
||||||
|
content =
|
||||||
|
$("#description, .video-description, .description").text() ||
|
||||||
|
$('meta[name="description"]').attr("content") ||
|
||||||
|
$('[class*="transcript"], [class*="caption"]').text() ||
|
||||||
|
"Video content detected - text summarization not available";
|
||||||
|
} else {
|
||||||
|
// Simple universal text extraction
|
||||||
|
// Remove non-content elements
|
||||||
|
$("script, style, noscript, nav, header, footer").remove();
|
||||||
|
|
||||||
|
// Get all text content
|
||||||
|
const allText = $("body").text();
|
||||||
|
|
||||||
|
// Split into sentences and filter for meaningful content
|
||||||
|
const sentences = allText
|
||||||
|
.split(/[.!?]+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 20) // Keep sentences with substance
|
||||||
|
.filter(
|
||||||
|
(s) =>
|
||||||
|
!/^(click|menu|button|nav|home|search|login|signup|subscribe)$/i.test(
|
||||||
|
s.toLowerCase(),
|
||||||
|
),
|
||||||
|
) // Remove UI text
|
||||||
|
.filter((s) => s.split(" ").length > 3); // Keep sentences with multiple words
|
||||||
|
|
||||||
|
content = sentences.join(". ").slice(0, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up whitespace and normalize text
|
||||||
|
content = content.replace(/\s+/g, " ").trim();
|
||||||
|
|
||||||
|
const wordCount = content
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((word) => word.length > 0).length;
|
||||||
|
const supported = !isVideo && content.length > 50;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageType,
|
||||||
|
title: title.trim(),
|
||||||
|
content: content.slice(0, 10000), // Limit content size for processing
|
||||||
|
metadata: {
|
||||||
|
url,
|
||||||
|
wordCount,
|
||||||
|
},
|
||||||
|
supported,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate summary using LLM
|
||||||
|
*/
|
||||||
|
async function generateSummary(title: string, content: string) {
|
||||||
|
const messages: CoreMessage[] = [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: `You are a helpful assistant that creates concise summaries of web content in HTML format.
|
||||||
|
|
||||||
|
Create a clear, informative summary that captures the key points and main ideas from the provided content. The summary should:
|
||||||
|
- Focus on the most important information and key takeaways
|
||||||
|
- Be concise but comprehensive
|
||||||
|
- Maintain the original context and meaning
|
||||||
|
- Be useful for someone who wants to quickly understand the content
|
||||||
|
- Format the summary in clean HTML using appropriate tags like <h1>, <h2>, <p>, <ul>, <li> to structure the information
|
||||||
|
|
||||||
|
IMPORTANT: Return ONLY the HTML content without any markdown code blocks or formatting. Do not wrap the response in \`\`\`html or any other markdown syntax. Return the raw HTML directly.
|
||||||
|
|
||||||
|
Extract the essential information while preserving important details, facts, or insights.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: `Title: ${title}
|
||||||
|
Content: ${content}
|
||||||
|
|
||||||
|
Please provide a concise summary of this content in HTML format.`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return await makeModelCall(
|
||||||
|
true,
|
||||||
|
messages,
|
||||||
|
() => {}, // onFinish callback
|
||||||
|
{ temperature: 0.3 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extensionSummary = task({
|
||||||
|
id: "extensionSummary",
|
||||||
|
maxDuration: 3000,
|
||||||
|
run: async (body: z.infer<typeof ExtensionSummaryBodyRequest>) => {
|
||||||
|
try {
|
||||||
|
const $ = cheerio.load(body.html);
|
||||||
|
|
||||||
|
// Extract content from any webpage
|
||||||
|
const extraction = extractTextContent($, body.url);
|
||||||
|
|
||||||
|
// Override title if provided
|
||||||
|
if (body.title) {
|
||||||
|
extraction.title = body.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
let summary = "";
|
||||||
|
|
||||||
|
if (extraction.supported && extraction.content.length > 0) {
|
||||||
|
// Generate summary for text content
|
||||||
|
const response = (await generateSummary(
|
||||||
|
extraction.title,
|
||||||
|
extraction.content,
|
||||||
|
)) as any;
|
||||||
|
|
||||||
|
const stream = await metadata.stream("messages", response.textStream);
|
||||||
|
|
||||||
|
let finalText: string = "";
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
finalText = finalText + chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary = finalText;
|
||||||
|
} else {
|
||||||
|
// Handle unsupported content types
|
||||||
|
if (extraction.pageType === "video") {
|
||||||
|
summary =
|
||||||
|
"Video content detected. Text summarization not available for video-focused pages.";
|
||||||
|
} else {
|
||||||
|
summary =
|
||||||
|
"Unable to extract sufficient text content for summarization.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
pageType: extraction.pageType,
|
||||||
|
title: extraction.title,
|
||||||
|
summary,
|
||||||
|
content: extraction.content.slice(0, 1000), // Return first 1000 chars of content
|
||||||
|
supported: extraction.supported,
|
||||||
|
metadata: extraction.metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing extension summary request:", error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to process page content",
|
||||||
|
pageType: "text" as PageType,
|
||||||
|
title: body.title || "Error",
|
||||||
|
summary: "Unable to process this page content.",
|
||||||
|
content: "",
|
||||||
|
supported: false,
|
||||||
|
metadata: {
|
||||||
|
url: body.url,
|
||||||
|
wordCount: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { prisma } from "~/db.server";
|
import { prisma } from "~/db.server";
|
||||||
import { TransportManager } from "./transport-manager";
|
import { TransportManager } from "./transport-manager";
|
||||||
|
import { configureStdioMCPEnvironment } from "~/trigger/utils/mcp";
|
||||||
|
import { getDefaultEnvironment } from "@core/mcp-proxy";
|
||||||
|
|
||||||
export interface IntegrationAccountWithDefinition {
|
export interface IntegrationAccountWithDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
@ -132,11 +134,23 @@ export class IntegrationLoader {
|
|||||||
|
|
||||||
loaded++;
|
loaded++;
|
||||||
} else {
|
} else {
|
||||||
// Skip non-HTTP transports for now
|
const { env, args } = configureStdioMCPEnvironment(spec, account);
|
||||||
failed.push({
|
const slug = account.integrationDefinition.slug;
|
||||||
slug: account.integrationDefinition.slug,
|
|
||||||
error: `Unsupported transport type: ${mcpConfig.type}`,
|
// Extract headers from the incoming request and convert to environment variables
|
||||||
});
|
const extractedEnv = { ...getDefaultEnvironment(), ...env };
|
||||||
|
|
||||||
|
// Use the saved local file instead of command
|
||||||
|
const executablePath = `./integrations/${slug}/main`;
|
||||||
|
|
||||||
|
await TransportManager.addStdioIntegrationTransport(
|
||||||
|
sessionId,
|
||||||
|
account.id,
|
||||||
|
account.integrationDefinition.slug,
|
||||||
|
executablePath,
|
||||||
|
args,
|
||||||
|
extractedEnv,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
failed.push({
|
failed.push({
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { type StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
import { type StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||||
import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
|
import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||||
|
|
||||||
export interface IntegrationTransport {
|
export interface IntegrationTransport {
|
||||||
client: McpClient;
|
client: McpClient;
|
||||||
transport: StreamableHTTPClientTransport;
|
transport: StreamableHTTPClientTransport | StdioClientTransport;
|
||||||
integrationAccountId: string;
|
integrationAccountId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
url: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionTransports {
|
export interface SessionTransports {
|
||||||
@ -102,6 +103,47 @@ export class TransportManager {
|
|||||||
return integrationTransport;
|
return integrationTransport;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async addStdioIntegrationTransport(
|
||||||
|
sessionId: string,
|
||||||
|
integrationAccountId: string,
|
||||||
|
slug: string,
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
env?: any,
|
||||||
|
): Promise<IntegrationTransport> {
|
||||||
|
const session = this.getOrCreateSession(sessionId);
|
||||||
|
|
||||||
|
const transport = new StdioClientTransport({
|
||||||
|
command,
|
||||||
|
args: args || [],
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create MCP client
|
||||||
|
const client = new McpClient({
|
||||||
|
name: `core-client-${slug}`,
|
||||||
|
version: "1.0.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect client to transport
|
||||||
|
await client.connect(transport);
|
||||||
|
|
||||||
|
const integrationTransport: IntegrationTransport = {
|
||||||
|
client,
|
||||||
|
transport,
|
||||||
|
integrationAccountId,
|
||||||
|
slug,
|
||||||
|
url: command,
|
||||||
|
};
|
||||||
|
|
||||||
|
session.integrationTransports.set(
|
||||||
|
integrationAccountId,
|
||||||
|
integrationTransport,
|
||||||
|
);
|
||||||
|
|
||||||
|
return integrationTransport;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get integration transport by account ID
|
* Get integration transport by account ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -4,11 +4,11 @@
|
|||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "remix vite:build",
|
"build": "remix vite:build && tsc server.ts --outDir ./ --module ESNext --moduleResolution bundler --target ES2022 --allowSyntheticDefaultImports --skipLibCheck",
|
||||||
"dev": "node ./server.mjs",
|
"dev": "tsx watch server.ts",
|
||||||
"lint": "eslint --fix --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
"lint": "eslint --fix --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||||
"lint:fix": "eslint 'app/**/*.{ts,tsx,js,jsx}' --rule 'turbo/no-undeclared-env-vars:error' -f table",
|
"lint:fix": "eslint 'app/**/*.{ts,tsx,js,jsx}' --rule 'turbo/no-undeclared-env-vars:error' -f table",
|
||||||
"start": "remix-serve ./build/server/index.js",
|
"start": "node server.js",
|
||||||
"typecheck": "tsc",
|
"typecheck": "tsc",
|
||||||
"trigger:dev": "pnpm dlx trigger.dev@4.0.0-v4-beta.22 dev",
|
"trigger:dev": "pnpm dlx trigger.dev@4.0.0-v4-beta.22 dev",
|
||||||
"trigger:deploy": "pnpm dlx trigger.dev@4.0.0-v4-beta.22 deploy"
|
"trigger:deploy": "pnpm dlx trigger.dev@4.0.0-v4-beta.22 deploy"
|
||||||
@ -175,7 +175,8 @@
|
|||||||
"tailwindcss": "4.1.7",
|
"tailwindcss": "4.1.7",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"vite": "^6.0.0",
|
"vite": "^6.0.0",
|
||||||
"vite-tsconfig-paths": "^4.2.1"
|
"vite-tsconfig-paths": "^4.2.1",
|
||||||
|
"tsx": "4.20.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|||||||
@ -3,7 +3,13 @@ import compression from "compression";
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
|
|
||||||
let viteDevServer;
|
// import {
|
||||||
|
// handleMCPRequest,
|
||||||
|
// handleSessionRequest,
|
||||||
|
// } from "~/services/mcp.server";
|
||||||
|
// import { authenticateHybridRequest } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
|
|
||||||
|
let viteDevServer: any;
|
||||||
let remixHandler;
|
let remixHandler;
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
@ -14,10 +20,13 @@ async function init() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const build = viteDevServer
|
const build: any = viteDevServer
|
||||||
? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
|
? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
|
||||||
: await import("./build/server/index.js");
|
: await import("./build/server/index.js");
|
||||||
|
|
||||||
|
const { authenticateHybridRequest, handleMCPRequest, handleSessionRequest } =
|
||||||
|
build.entry.module;
|
||||||
|
|
||||||
remixHandler = createRequestHandler({ build });
|
remixHandler = createRequestHandler({ build });
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -44,6 +53,68 @@ async function init() {
|
|||||||
|
|
||||||
app.use(morgan("tiny"));
|
app.use(morgan("tiny"));
|
||||||
|
|
||||||
|
app.get("/api/v1/mcp", async (req, res) => {
|
||||||
|
const authenticationResult = await authenticateHybridRequest(req as any, {
|
||||||
|
allowJWT: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authenticationResult) {
|
||||||
|
res.status(401).json({ error: "Authentication required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleSessionRequest(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/v1/mcp", async (req, res) => {
|
||||||
|
const authenticationResult = await authenticateHybridRequest(req as any, {
|
||||||
|
allowJWT: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authenticationResult) {
|
||||||
|
res.status(401).json({ error: "Authentication required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = "";
|
||||||
|
req.on("data", (chunk) => {
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on("end", async () => {
|
||||||
|
try {
|
||||||
|
const parsedBody = JSON.parse(body);
|
||||||
|
const queryParams = req.query; // Get query parameters from the request
|
||||||
|
await handleMCPRequest(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
parsedBody,
|
||||||
|
authenticationResult,
|
||||||
|
queryParams,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ error: "Invalid JSON" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/v1/mcp", async (req, res) => {
|
||||||
|
const authenticationResult = await authenticateHybridRequest(req as any, {
|
||||||
|
allowJWT: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authenticationResult) {
|
||||||
|
res.status(401).json({ error: "Authentication required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleSessionRequest(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.options("/api/v1/mcp", (_, res) => {
|
||||||
|
res.json({});
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/.well-known/oauth-authorization-server", (req, res) => {
|
app.get("/.well-known/oauth-authorization-server", (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
issuer: process.env.APP_ORIGIN,
|
issuer: process.env.APP_ORIGIN,
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"tailwind.config.js",
|
"tailwind.config.js",
|
||||||
"tailwind.config.js",
|
"tailwind.config.js",
|
||||||
"trigger.config.ts",
|
"trigger.config.ts",
|
||||||
"server.mjs"
|
"server.ts"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["@remix-run/node", "vite/client"],
|
"types": ["@remix-run/node", "vite/client"],
|
||||||
|
|||||||
@ -64,7 +64,7 @@ ENV NODE_ENV production
|
|||||||
COPY --from=base /usr/bin/dumb-init /usr/bin/dumb-init
|
COPY --from=base /usr/bin/dumb-init /usr/bin/dumb-init
|
||||||
COPY --from=pruner --chown=node:node /core/out/full/ .
|
COPY --from=pruner --chown=node:node /core/out/full/ .
|
||||||
COPY --from=production-deps --chown=node:node /core .
|
COPY --from=production-deps --chown=node:node /core .
|
||||||
COPY --from=builder --chown=node:node /core/apps/webapp/server.mjs ./apps/webapp/server.mjs
|
COPY --from=builder --chown=node:node /core/apps/webapp/server.js ./apps/webapp/server.js
|
||||||
COPY --from=builder --chown=node:node /core/apps/webapp/build ./apps/webapp/build
|
COPY --from=builder --chown=node:node /core/apps/webapp/build ./apps/webapp/build
|
||||||
COPY --from=builder --chown=node:node /core/apps/webapp/public ./apps/webapp/public
|
COPY --from=builder --chown=node:node /core/apps/webapp/public ./apps/webapp/public
|
||||||
COPY --from=builder --chown=node:node /core/scripts ./scripts
|
COPY --from=builder --chown=node:node /core/scripts ./scripts
|
||||||
|
|||||||
@ -15,4 +15,4 @@ cp packages/database/prisma/schema.prisma apps/webapp/prisma/
|
|||||||
|
|
||||||
cd /core/apps/webapp
|
cd /core/apps/webapp
|
||||||
# exec dumb-init pnpm run start:local
|
# exec dumb-init pnpm run start:local
|
||||||
NODE_PATH='/core/node_modules/.pnpm/node_modules' exec dumb-init node --max-old-space-size=8192 ./server.mjs
|
NODE_PATH='/core/node_modules/.pnpm/node_modules' exec dumb-init node --max-old-space-size=8192 ./server.js
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
VERSION=0.1.17
|
VERSION=0.1.18
|
||||||
|
|
||||||
# Nest run in docker, change host to database container name
|
# Nest run in docker, change host to database container name
|
||||||
DB_HOST=postgres
|
DB_HOST=postgres
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "core",
|
"name": "core",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.17",
|
"version": "0.1.18",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
@ -1,26 +1,10 @@
|
|||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
import { MCPRemoteClientConfig, AuthenticationResult } from "../types/remote-client.js";
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
||||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
||||||
import {
|
|
||||||
MCPRemoteClientConfig,
|
|
||||||
AuthenticationResult,
|
|
||||||
ProxyConnectionConfig,
|
|
||||||
CredentialLoadCallback,
|
|
||||||
MCPProxyFunction,
|
|
||||||
StoredCredentials,
|
|
||||||
TransportStrategy,
|
|
||||||
} from "../types/remote-client.js";
|
|
||||||
import { MCPAuthProxyError } from "../utils/errors.js";
|
import { MCPAuthProxyError } from "../utils/errors.js";
|
||||||
import { NodeOAuthClientProvider } from "../lib/node-oauth-client-provider.js";
|
import { NodeOAuthClientProvider } from "../lib/node-oauth-client-provider.js";
|
||||||
import { globalAuthStorage } from "../lib/in-memory-auth-storage.js";
|
import { globalAuthStorage } from "../lib/in-memory-auth-storage.js";
|
||||||
import { getServerUrlHash } from "../lib/utils.js";
|
import { getServerUrlHash } from "../lib/utils.js";
|
||||||
import { RemixMCPTransport } from "../utils/mcp-transport.js";
|
import { createAuthProviderFromConfig } from "../utils/auth-provider-factory.js";
|
||||||
import { createMCPTransportBridge } from "../utils/mcp-transport-bridge.js";
|
|
||||||
import {
|
|
||||||
createAuthProviderFromConfig,
|
|
||||||
createAuthProviderForProxy,
|
|
||||||
} from "../utils/auth-provider-factory.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an MCP authentication client that handles OAuth flow
|
* Creates an MCP authentication client that handles OAuth flow
|
||||||
@ -32,424 +16,10 @@ export function createMCPAuthClient(config: MCPRemoteClientConfig): MCPAuthentic
|
|||||||
return new MCPAuthenticationClient(config);
|
return new MCPAuthenticationClient(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an MCP proxy that forwards requests to the remote MCP server
|
|
||||||
* Consolidates all proxy functionality into a single function
|
|
||||||
* @param config Configuration for the proxy connection
|
|
||||||
* @param onCredentialLoad Callback to load credentials from your database
|
|
||||||
* @returns Proxy function that can be used in your Remix API routes
|
|
||||||
*/
|
|
||||||
export function createMCPProxy(
|
|
||||||
config: ProxyConnectionConfig & {
|
|
||||||
/** Enable debug logging */
|
|
||||||
debug?: boolean;
|
|
||||||
},
|
|
||||||
onCredentialLoad: CredentialLoadCallback
|
|
||||||
): MCPProxyFunction {
|
|
||||||
return async (request: Request, userApiKey: string): Promise<Response> => {
|
|
||||||
return new Promise<Response>(async (resolve) => {
|
|
||||||
let bridge: any = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Load credentials for this user and server
|
|
||||||
const credentials = await onCredentialLoad(userApiKey, config.serverUrl);
|
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
return resolve(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
error: "No credentials found for this service",
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 401,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if tokens are expired
|
|
||||||
if (credentials.expiresAt && credentials.expiresAt < new Date()) {
|
|
||||||
return resolve(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
error: "Credentials expired - please re-authenticate",
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 401,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract session ID and last event ID from incoming request
|
|
||||||
const clientSessionId = request.headers.get("Mcp-Session-Id");
|
|
||||||
const lastEventId = request.headers.get("Last-Event-Id");
|
|
||||||
|
|
||||||
// Create remote transport (connects to the MCP server) FIRST
|
|
||||||
const serverTransport = await createRemoteTransport(
|
|
||||||
credentials.serverUrl,
|
|
||||||
credentials,
|
|
||||||
config.redirectUrl,
|
|
||||||
config.transportStrategy || "sse-first",
|
|
||||||
{ sessionId: clientSessionId, lastEventId } // Pass both session and event IDs
|
|
||||||
);
|
|
||||||
|
|
||||||
// Start server transport and wait for connection
|
|
||||||
await serverTransport.start();
|
|
||||||
|
|
||||||
// Create Remix transport (converts HTTP to MCP messages)
|
|
||||||
const clientTransport = new RemixMCPTransport(request, resolve);
|
|
||||||
|
|
||||||
// Bridge the transports
|
|
||||||
const bridgeOptions: any = {
|
|
||||||
debug: config.debug || false,
|
|
||||||
onError: (error: Error, source: string) => {
|
|
||||||
console.error(`[MCP Bridge] ${source} error:`, error);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.debug) {
|
|
||||||
bridgeOptions.onMessage = (direction: string, message: any) => {
|
|
||||||
console.log(`[MCP Bridge] ${direction}:`, message.method || message.id);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge = createMCPTransportBridge(
|
|
||||||
clientTransport as any,
|
|
||||||
serverTransport as any,
|
|
||||||
bridgeOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
// Start only the client transport (server is already started)
|
|
||||||
await clientTransport.start();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("MCP Transport Proxy Error:", error);
|
|
||||||
|
|
||||||
if (bridge) {
|
|
||||||
bridge.close().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
resolve(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
error: `Transport proxy error: ${errorMessage}`,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to create remote transport
|
|
||||||
async function createRemoteTransport(
|
|
||||||
serverUrl: string,
|
|
||||||
credentials: StoredCredentials,
|
|
||||||
redirectUrl: string,
|
|
||||||
transportStrategy: TransportStrategy = "sse-first",
|
|
||||||
clientHeaders?: { sessionId?: string | null; lastEventId?: string | null }
|
|
||||||
): Promise<SSEClientTransport | StreamableHTTPClientTransport | StdioClientTransport> {
|
|
||||||
// Create auth provider with stored credentials using common factory
|
|
||||||
const authProvider = await createAuthProviderForProxy(serverUrl, credentials, redirectUrl);
|
|
||||||
|
|
||||||
const url = new URL(serverUrl);
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
Authorization: `Bearer ${credentials.tokens.access_token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...config.headers,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add session and event headers if provided
|
|
||||||
if (clientHeaders?.sessionId) {
|
|
||||||
headers["Mcp-Session-Id"] = clientHeaders.sessionId;
|
|
||||||
}
|
|
||||||
if (clientHeaders?.lastEventId) {
|
|
||||||
headers["Last-Event-Id"] = clientHeaders.lastEventId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create transport based on strategy (don't start yet)
|
|
||||||
let transport: SSEClientTransport | StreamableHTTPClientTransport | StdioClientTransport;
|
|
||||||
|
|
||||||
switch (transportStrategy) {
|
|
||||||
case "stdio":
|
|
||||||
// For stdio transport, serverUrl should contain the command to execute
|
|
||||||
// This is mainly for completeness - prefer using createMCPStdioProxy directly
|
|
||||||
throw new Error(
|
|
||||||
"Stdio transport not supported in createRemoteTransport. Use createMCPStdioProxy instead."
|
|
||||||
);
|
|
||||||
|
|
||||||
case "sse-only":
|
|
||||||
transport = new SSEClientTransport(url, {
|
|
||||||
authProvider,
|
|
||||||
requestInit: { headers },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "http-only":
|
|
||||||
transport = new StreamableHTTPClientTransport(url, {
|
|
||||||
requestInit: { headers },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "sse-first":
|
|
||||||
// Try SSE first, fallback to HTTP on error
|
|
||||||
try {
|
|
||||||
transport = new SSEClientTransport(url, {
|
|
||||||
authProvider,
|
|
||||||
requestInit: { headers },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("SSE transport failed, falling back to HTTP:", error);
|
|
||||||
transport = new StreamableHTTPClientTransport(url, {
|
|
||||||
requestInit: { headers },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "http-first":
|
|
||||||
// Try HTTP first, fallback to SSE on error
|
|
||||||
try {
|
|
||||||
transport = new StreamableHTTPClientTransport(url, {
|
|
||||||
requestInit: { headers },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("HTTP transport failed, falling back to SSE:", error);
|
|
||||||
transport = new SSEClientTransport(url, {
|
|
||||||
authProvider,
|
|
||||||
requestInit: { headers },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown transport strategy: ${transportStrategy}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return transport;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an MCP proxy that forwards requests to a stdio process.
|
|
||||||
* Maintains a mapping of sessionId -> StdioClientTransport for reuse.
|
|
||||||
* If sessionId is provided, it is returned in the response header as mcp_session_id.
|
|
||||||
* @param request The incoming HTTP request
|
|
||||||
* @param command The command to execute for the stdio process
|
|
||||||
* @param args Arguments for the command
|
|
||||||
* @param options Optional configuration for the proxy
|
|
||||||
* @param sessionId Optional session id for transport reuse
|
|
||||||
* @returns Promise that resolves to the HTTP response
|
|
||||||
*/
|
|
||||||
// Track both the transport and its last used timestamp
|
|
||||||
type StdioTransportEntry = {
|
|
||||||
transport: StdioClientTransport;
|
|
||||||
lastUsed: number; // ms since epoch
|
|
||||||
};
|
|
||||||
|
|
||||||
const stdioTransports: Map<string, StdioTransportEntry> = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans up any stdio transports that have not been used in the last 5 minutes.
|
|
||||||
* Closes and removes them from the map.
|
|
||||||
*/
|
|
||||||
function cleanupOldStdioTransports() {
|
|
||||||
const now = Date.now();
|
|
||||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
|
||||||
for (const [sessionId, entry] of stdioTransports.entries()) {
|
|
||||||
if (now - entry.lastUsed > FIVE_MINUTES) {
|
|
||||||
try {
|
|
||||||
entry.transport.close?.();
|
|
||||||
} catch (err) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
stdioTransports.delete(sessionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMCPStdioProxy(
|
|
||||||
request: Request,
|
|
||||||
command: string,
|
|
||||||
args?: string[],
|
|
||||||
options?: {
|
|
||||||
/** Enable debug logging */
|
|
||||||
debug?: boolean;
|
|
||||||
/** Environment variables to pass to the process */
|
|
||||||
env?: Record<string, string>;
|
|
||||||
/** Custom header-to-environment variable mapping */
|
|
||||||
headerMapping?: Record<string, string>;
|
|
||||||
/** Optional session id for transport reuse */
|
|
||||||
sessionId?: string;
|
|
||||||
}
|
|
||||||
): Promise<Response> {
|
|
||||||
return new Promise<Response>(async (resolve) => {
|
|
||||||
let bridge: any = null;
|
|
||||||
let serverTransport: StdioClientTransport | undefined;
|
|
||||||
let sessionId: string | undefined =
|
|
||||||
options?.sessionId || request.headers.get("Mcp-Session-Id") || undefined;
|
|
||||||
|
|
||||||
// Clean up old transports before handling new connection
|
|
||||||
cleanupOldStdioTransports();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Extract headers from the incoming request and convert to environment variables
|
|
||||||
const env = createEnvironmentFromRequest(
|
|
||||||
request,
|
|
||||||
options?.env || {},
|
|
||||||
options?.headerMapping || {}
|
|
||||||
);
|
|
||||||
|
|
||||||
// If sessionId is provided, try to reuse the transport
|
|
||||||
let entry: StdioTransportEntry | undefined;
|
|
||||||
if (sessionId) {
|
|
||||||
entry = stdioTransports.get(sessionId);
|
|
||||||
if (entry) {
|
|
||||||
serverTransport = entry.transport;
|
|
||||||
entry.lastUsed = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no transport exists for this sessionId, create a new one and store it
|
|
||||||
if (!serverTransport) {
|
|
||||||
serverTransport = new StdioClientTransport({
|
|
||||||
command,
|
|
||||||
args: args || [],
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
await serverTransport.start();
|
|
||||||
if (sessionId) {
|
|
||||||
stdioTransports.set(sessionId, {
|
|
||||||
transport: serverTransport,
|
|
||||||
lastUsed: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Remix transport (converts HTTP to MCP messages)
|
|
||||||
// We need to wrap resolve to inject the sessionId header if present
|
|
||||||
const resolveWithSessionId = (response: Response) => {
|
|
||||||
if (sessionId) {
|
|
||||||
// Clone the response and add the mcp_session_id header
|
|
||||||
const headers = new Headers(response.headers);
|
|
||||||
headers.set("mcp-session-id", sessionId);
|
|
||||||
resolve(
|
|
||||||
new Response(response.body, {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
resolve(response);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clientTransport = new RemixMCPTransport(request, resolveWithSessionId);
|
|
||||||
|
|
||||||
// Bridge the transports
|
|
||||||
const bridgeOptions: any = {
|
|
||||||
debug: options?.debug || false,
|
|
||||||
onError: (error: Error, source: string) => {
|
|
||||||
console.error(`[MCP Stdio Bridge] ${source} error:`, error);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options?.debug) {
|
|
||||||
bridgeOptions.onMessage = (direction: string, message: any) => {
|
|
||||||
console.log(`[MCP Stdio Bridge] ${direction}:`, message.method || message.id);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge = createMCPTransportBridge(
|
|
||||||
clientTransport as any,
|
|
||||||
serverTransport as any,
|
|
||||||
bridgeOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
// Start only the client transport (server is already started)
|
|
||||||
await clientTransport.start();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("MCP Stdio Proxy Error:", error);
|
|
||||||
|
|
||||||
if (bridge) {
|
|
||||||
bridge.close().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
// Always include mcp_session_id header if sessionId is present
|
|
||||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
||||||
if (sessionId) {
|
|
||||||
headers["mcp-session-id"] = sessionId;
|
|
||||||
}
|
|
||||||
resolve(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
error: `Stdio proxy error: ${errorMessage}`,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates environment variables from request headers
|
|
||||||
*/
|
|
||||||
function createEnvironmentFromRequest(
|
|
||||||
request: Request,
|
|
||||||
baseEnv: Record<string, string>,
|
|
||||||
headerMapping: Record<string, string>
|
|
||||||
): Record<string, string> {
|
|
||||||
// Start with base environment (inherit safe environment variables)
|
|
||||||
const env: Record<string, string> = {
|
|
||||||
...getDefaultEnvironment(),
|
|
||||||
...baseEnv,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add standard MCP headers as environment variables
|
|
||||||
const sessionId = request.headers.get("Mcp-Session-Id");
|
|
||||||
const lastEventId = request.headers.get("Last-Event-Id");
|
|
||||||
const contentType = request.headers.get("Content-Type");
|
|
||||||
const userAgent = request.headers.get("User-Agent");
|
|
||||||
|
|
||||||
if (sessionId) {
|
|
||||||
env["MCP_SESSION_ID"] = sessionId;
|
|
||||||
}
|
|
||||||
if (lastEventId) {
|
|
||||||
env["MCP_LAST_EVENT_ID"] = lastEventId;
|
|
||||||
}
|
|
||||||
if (contentType) {
|
|
||||||
env["MCP_CONTENT_TYPE"] = contentType;
|
|
||||||
}
|
|
||||||
if (userAgent) {
|
|
||||||
env["MCP_USER_AGENT"] = userAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply custom header-to-environment variable mapping
|
|
||||||
for (const [headerName, envVarName] of Object.entries(headerMapping)) {
|
|
||||||
const headerValue = request.headers.get(headerName);
|
|
||||||
if (headerValue) {
|
|
||||||
env[envVarName] = headerValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a default environment object including only environment variables deemed safe to inherit.
|
* Returns a default environment object including only environment variables deemed safe to inherit.
|
||||||
*/
|
*/
|
||||||
function getDefaultEnvironment(): Record<string, string> {
|
export function getDefaultEnvironment(): Record<string, string> {
|
||||||
const DEFAULT_INHERITED_ENV_VARS =
|
const DEFAULT_INHERITED_ENV_VARS =
|
||||||
process.platform === "win32"
|
process.platform === "win32"
|
||||||
? [
|
? [
|
||||||
|
|||||||
@ -4,8 +4,7 @@ export * from "./types/index.js";
|
|||||||
// MCP Remote Client exports (new simplified interface)
|
// MCP Remote Client exports (new simplified interface)
|
||||||
export {
|
export {
|
||||||
createMCPAuthClient,
|
createMCPAuthClient,
|
||||||
createMCPProxy,
|
getDefaultEnvironment,
|
||||||
createMCPStdioProxy,
|
|
||||||
MCPAuthenticationClient,
|
MCPAuthenticationClient,
|
||||||
} from "./core/mcp-remote-client.js";
|
} from "./core/mcp-remote-client.js";
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export interface StatementNode {
|
|||||||
userId: string;
|
userId: string;
|
||||||
space?: string; // Legacy field - deprecated in favor of spaceIds
|
space?: string; // Legacy field - deprecated in favor of spaceIds
|
||||||
spaceIds?: string[]; // Array of space UUIDs this statement belongs to
|
spaceIds?: string[]; // Array of space UUIDs this statement belongs to
|
||||||
recallCount?: number;
|
recallCount?: { low: number; high: number };
|
||||||
provenanceCount?: number;
|
provenanceCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
123
pnpm-lock.yaml
generated
123
pnpm-lock.yaml
generated
@ -646,7 +646,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@remix-run/dev':
|
'@remix-run/dev':
|
||||||
specifier: 2.16.7
|
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@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.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@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))(yaml@2.8.0)
|
||||||
'@remix-run/eslint-config':
|
'@remix-run/eslint-config':
|
||||||
specifier: 2.16.7
|
specifier: 2.16.7
|
||||||
version: 2.16.7(eslint@8.57.1)(react@18.3.1)(typescript@5.8.3)
|
version: 2.16.7(eslint@8.57.1)(react@18.3.1)(typescript@5.8.3)
|
||||||
@ -661,7 +661,7 @@ importers:
|
|||||||
version: 0.5.16(tailwindcss@4.1.7)
|
version: 0.5.16(tailwindcss@4.1.7)
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.7
|
specifier: ^4.1.7
|
||||||
version: 4.1.9(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0))
|
version: 4.1.9(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))
|
||||||
'@trigger.dev/build':
|
'@trigger.dev/build':
|
||||||
specifier: 4.0.0-v4-beta.22
|
specifier: 4.0.0-v4-beta.22
|
||||||
version: 4.0.0-v4-beta.22(typescript@5.8.3)
|
version: 4.0.0-v4-beta.22(typescript@5.8.3)
|
||||||
@ -752,15 +752,18 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: 4.1.7
|
specifier: 4.1.7
|
||||||
version: 4.1.7
|
version: 4.1.7
|
||||||
|
tsx:
|
||||||
|
specifier: 4.20.4
|
||||||
|
version: 4.20.4
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.8.3
|
specifier: 5.8.3
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
|
version: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
|
||||||
vite-tsconfig-paths:
|
vite-tsconfig-paths:
|
||||||
specifier: ^4.2.1
|
specifier: ^4.2.1
|
||||||
version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0))
|
version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))
|
||||||
|
|
||||||
packages/database:
|
packages/database:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -829,7 +832,7 @@ importers:
|
|||||||
version: 20.19.7
|
version: 20.19.7
|
||||||
tsup:
|
tsup:
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.5.0(@swc/core@1.3.101)(jiti@2.4.2)(postcss@8.5.5)(tsx@4.17.0)(typescript@5.8.3)(yaml@2.8.0)
|
version: 8.5.0(@swc/core@1.3.101(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.5)(tsx@4.20.4)(typescript@5.8.3)(yaml@2.8.0)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
@ -866,7 +869,7 @@ importers:
|
|||||||
version: 6.0.1
|
version: 6.0.1
|
||||||
tsup:
|
tsup:
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.5.0(@swc/core@1.3.101)(jiti@2.4.2)(postcss@8.5.5)(tsx@4.17.0)(typescript@5.8.3)(yaml@2.8.0)
|
version: 8.5.0(@swc/core@1.3.101(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.5)(tsx@4.20.4)(typescript@5.8.3)(yaml@2.8.0)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.3.0
|
specifier: ^5.3.0
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
@ -10880,6 +10883,11 @@ packages:
|
|||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
tsx@4.20.4:
|
||||||
|
resolution: {integrity: sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
tty-table@4.2.3:
|
tty-table@4.2.3:
|
||||||
resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==}
|
resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@ -12911,7 +12919,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/dom': 1.7.1
|
'@floating-ui/dom': 1.7.1
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
|
|
||||||
'@floating-ui/react-dom@2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@floating-ui/react-dom@2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -13664,7 +13672,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -13718,7 +13726,7 @@ snapshots:
|
|||||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -13746,7 +13754,7 @@ snapshots:
|
|||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -13879,7 +13887,7 @@ snapshots:
|
|||||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -13944,7 +13952,7 @@ snapshots:
|
|||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -14036,7 +14044,7 @@ snapshots:
|
|||||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
aria-hidden: 1.2.6
|
aria-hidden: 1.2.6
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
react-remove-scroll: 2.5.7(@types/react@18.2.47)(react@18.2.0)
|
react-remove-scroll: 2.5.7(@types/react@18.2.47)(react@18.2.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
@ -14078,7 +14086,7 @@ snapshots:
|
|||||||
'@radix-ui/react-use-size': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-size': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
'@radix-ui/rect': 1.1.0
|
'@radix-ui/rect': 1.1.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -14113,7 +14121,7 @@ snapshots:
|
|||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -14141,7 +14149,7 @@ snapshots:
|
|||||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -14167,7 +14175,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -14193,7 +14201,7 @@ snapshots:
|
|||||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -14378,7 +14386,7 @@ snapshots:
|
|||||||
'@radix-ui/react-toggle': 1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-toggle': 1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -14389,7 +14397,7 @@ snapshots:
|
|||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -14409,7 +14417,7 @@ snapshots:
|
|||||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
|
||||||
'@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -14561,7 +14569,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.47
|
'@types/react': 18.2.47
|
||||||
'@types/react-dom': 18.2.18
|
'@types/react-dom': 18.2.18
|
||||||
@ -14682,7 +14690,7 @@ snapshots:
|
|||||||
html-to-text: 9.0.5
|
html-to-text: 9.0.5
|
||||||
js-beautify: 1.15.4
|
js-beautify: 1.15.4
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
react-promise-suspense: 0.3.4
|
react-promise-suspense: 0.3.4
|
||||||
|
|
||||||
'@react-email/row@0.0.7(react@18.3.1)':
|
'@react-email/row@0.0.7(react@18.3.1)':
|
||||||
@ -14716,7 +14724,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- 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@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.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@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))(yaml@2.8.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.27.4
|
'@babel/core': 7.27.4
|
||||||
'@babel/generator': 7.27.5
|
'@babel/generator': 7.27.5
|
||||||
@ -14733,7 +14741,7 @@ snapshots:
|
|||||||
'@remix-run/router': 1.23.0
|
'@remix-run/router': 1.23.0
|
||||||
'@remix-run/server-runtime': 2.16.7(typescript@5.8.3)
|
'@remix-run/server-runtime': 2.16.7(typescript@5.8.3)
|
||||||
'@types/mdx': 2.0.13
|
'@types/mdx': 2.0.13
|
||||||
'@vanilla-extract/integration': 6.5.0(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
|
'@vanilla-extract/integration': 6.5.0(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
|
||||||
arg: 5.0.2
|
arg: 5.0.2
|
||||||
cacache: 17.1.4
|
cacache: 17.1.4
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
@ -14773,12 +14781,12 @@ snapshots:
|
|||||||
tar-fs: 2.1.3
|
tar-fs: 2.1.3
|
||||||
tsconfig-paths: 4.2.0
|
tsconfig-paths: 4.2.0
|
||||||
valibot: 0.41.0(typescript@5.8.3)
|
valibot: 0.41.0(typescript@5.8.3)
|
||||||
vite-node: 3.2.3(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
|
vite-node: 3.2.3(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
|
||||||
ws: 7.5.10
|
ws: 7.5.10
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@remix-run/serve': 2.16.7(typescript@5.8.3)
|
'@remix-run/serve': 2.16.7(typescript@5.8.3)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
|
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
@ -15517,12 +15525,12 @@ snapshots:
|
|||||||
postcss-selector-parser: 6.0.10
|
postcss-selector-parser: 6.0.10
|
||||||
tailwindcss: 4.1.7
|
tailwindcss: 4.1.7
|
||||||
|
|
||||||
'@tailwindcss/vite@4.1.9(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0))':
|
'@tailwindcss/vite@4.1.9(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.1.9
|
'@tailwindcss/node': 4.1.9
|
||||||
'@tailwindcss/oxide': 4.1.9
|
'@tailwindcss/oxide': 4.1.9
|
||||||
tailwindcss: 4.1.9
|
tailwindcss: 4.1.9
|
||||||
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
|
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
|
||||||
|
|
||||||
'@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -16531,7 +16539,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
'@vanilla-extract/integration@6.5.0(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)':
|
'@vanilla-extract/integration@6.5.0(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.27.4
|
'@babel/core': 7.27.4
|
||||||
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4)
|
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4)
|
||||||
@ -16544,8 +16552,8 @@ snapshots:
|
|||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
mlly: 1.7.4
|
mlly: 1.7.4
|
||||||
outdent: 0.8.0
|
outdent: 0.8.0
|
||||||
vite: 5.4.19(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
|
vite: 5.4.19(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
|
||||||
vite-node: 1.6.1(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
|
vite-node: 1.6.1(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
@ -18904,7 +18912,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@emotion/is-prop-valid': 0.8.8
|
'@emotion/is-prop-valid': 0.8.8
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
|
|
||||||
framework-utils@1.1.0: {}
|
framework-utils@1.1.0: {}
|
||||||
|
|
||||||
@ -20754,7 +20762,7 @@ snapshots:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
styled-jsx: 5.1.1(@babel/core@7.24.5)(react@18.2.0)
|
styled-jsx: 5.1.1(@babel/core@7.24.5)(react@18.2.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 14.1.4
|
'@next/swc-darwin-arm64': 14.1.4
|
||||||
@ -21378,13 +21386,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
postcss: 8.5.5
|
postcss: 8.5.5
|
||||||
|
|
||||||
postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.5)(tsx@4.17.0)(yaml@2.8.0):
|
postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.5)(tsx@4.20.4)(yaml@2.8.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
jiti: 2.4.2
|
jiti: 2.4.2
|
||||||
postcss: 8.5.5
|
postcss: 8.5.5
|
||||||
tsx: 4.17.0
|
tsx: 4.20.4
|
||||||
yaml: 2.8.0
|
yaml: 2.8.0
|
||||||
|
|
||||||
postcss-loader@8.1.1(postcss@8.5.5)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5)):
|
postcss-loader@8.1.1(postcss@8.5.5)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5)):
|
||||||
@ -21808,6 +21816,12 @@ snapshots:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
scheduler: 0.23.2
|
scheduler: 0.23.2
|
||||||
|
|
||||||
|
react-dom@18.2.0(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
react: 18.3.1
|
||||||
|
scheduler: 0.23.2
|
||||||
|
|
||||||
react-dom@18.3.1(react@18.3.1):
|
react-dom@18.3.1(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
@ -21847,7 +21861,7 @@ snapshots:
|
|||||||
postcss: 8.4.38
|
postcss: 8.4.38
|
||||||
prism-react-renderer: 2.1.0(react@18.2.0)
|
prism-react-renderer: 2.1.0(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
socket.io: 4.7.3
|
socket.io: 4.7.3
|
||||||
socket.io-client: 4.7.3
|
socket.io-client: 4.7.3
|
||||||
sonner: 1.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
sonner: 1.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
@ -22650,7 +22664,7 @@ snapshots:
|
|||||||
sonner@1.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
sonner@1.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.3.1)
|
||||||
|
|
||||||
source-map-js@1.0.2: {}
|
source-map-js@1.0.2: {}
|
||||||
|
|
||||||
@ -23177,7 +23191,7 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
tsup@8.5.0(@swc/core@1.3.101)(jiti@2.4.2)(postcss@8.5.5)(tsx@4.17.0)(typescript@5.8.3)(yaml@2.8.0):
|
tsup@8.5.0(@swc/core@1.3.101(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.5)(tsx@4.20.4)(typescript@5.8.3)(yaml@2.8.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
bundle-require: 5.1.0(esbuild@0.25.5)
|
bundle-require: 5.1.0(esbuild@0.25.5)
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
@ -23188,7 +23202,7 @@ snapshots:
|
|||||||
fix-dts-default-cjs-exports: 1.0.1
|
fix-dts-default-cjs-exports: 1.0.1
|
||||||
joycon: 3.1.1
|
joycon: 3.1.1
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.5)(tsx@4.17.0)(yaml@2.8.0)
|
postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.5)(tsx@4.20.4)(yaml@2.8.0)
|
||||||
resolve-from: 5.0.0
|
resolve-from: 5.0.0
|
||||||
rollup: 4.43.0
|
rollup: 4.43.0
|
||||||
source-map: 0.8.0-beta.0
|
source-map: 0.8.0-beta.0
|
||||||
@ -23218,6 +23232,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
tsx@4.20.4:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.25.5
|
||||||
|
get-tsconfig: 4.10.1
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
tty-table@4.2.3:
|
tty-table@4.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
@ -23580,13 +23601,13 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.2
|
vfile-message: 4.0.2
|
||||||
|
|
||||||
vite-node@1.6.1(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0):
|
vite-node@1.6.1(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.1(supports-color@10.0.0)
|
debug: 4.4.1(supports-color@10.0.0)
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
vite: 5.4.19(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
|
vite: 5.4.19(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- less
|
- less
|
||||||
@ -23598,13 +23619,13 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
vite-node@3.2.3(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0):
|
vite-node@3.2.3(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.1(supports-color@10.0.0)
|
debug: 4.4.1(supports-color@10.0.0)
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
|
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- jiti
|
- jiti
|
||||||
@ -23619,31 +23640,31 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)):
|
vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.1(supports-color@10.0.0)
|
debug: 4.4.1(supports-color@10.0.0)
|
||||||
globrex: 0.1.2
|
globrex: 0.1.2
|
||||||
tsconfck: 3.1.6(typescript@5.8.3)
|
tsconfck: 3.1.6(typescript@5.8.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
|
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
vite@5.4.19(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0):
|
vite@5.4.19(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
postcss: 8.5.5
|
postcss: 8.5.5
|
||||||
rollup: 4.43.0
|
rollup: 4.43.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.16.0
|
'@types/node': 20.19.7
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
less: 4.4.0
|
less: 4.4.0
|
||||||
lightningcss: 1.30.1
|
lightningcss: 1.30.1
|
||||||
sass: 1.89.2
|
sass: 1.89.2
|
||||||
terser: 5.42.0
|
terser: 5.42.0
|
||||||
|
|
||||||
vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0):
|
vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.5
|
esbuild: 0.25.5
|
||||||
fdir: 6.4.6(picomatch@4.0.2)
|
fdir: 6.4.6(picomatch@4.0.2)
|
||||||
@ -23652,14 +23673,14 @@ snapshots:
|
|||||||
rollup: 4.43.0
|
rollup: 4.43.0
|
||||||
tinyglobby: 0.2.14
|
tinyglobby: 0.2.14
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.16.0
|
'@types/node': 20.19.7
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.4.2
|
jiti: 2.4.2
|
||||||
less: 4.4.0
|
less: 4.4.0
|
||||||
lightningcss: 1.30.1
|
lightningcss: 1.30.1
|
||||||
sass: 1.89.2
|
sass: 1.89.2
|
||||||
terser: 5.42.0
|
terser: 5.42.0
|
||||||
tsx: 4.17.0
|
tsx: 4.20.4
|
||||||
yaml: 2.8.0
|
yaml: 2.8.0
|
||||||
|
|
||||||
w3c-keyname@2.2.8: {}
|
w3c-keyname@2.2.8: {}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user