mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 18:18:27 +00:00
Feat: add conversation API
This commit is contained in:
parent
12453954b7
commit
158d26f7c2
@ -382,7 +382,6 @@ export const Graph = forwardRef<GraphRef, GraphProps>(
|
|||||||
|
|
||||||
// Node click handler
|
// Node click handler
|
||||||
sigma.on("clickNode", (event) => {
|
sigma.on("clickNode", (event) => {
|
||||||
console.log(event);
|
|
||||||
const { node } = event;
|
const { node } = event;
|
||||||
// resetHighlights();
|
// resetHighlights();
|
||||||
if (onNodeClick) {
|
if (onNodeClick) {
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
|
|
||||||
|
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||||
|
import {
|
||||||
|
createConversation,
|
||||||
|
CreateConversationSchema,
|
||||||
|
readConversation,
|
||||||
|
} from "~/services/conversation.server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ConversationIdSchema = z.object({
|
||||||
|
conversationId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { action, loader } = createActionApiRoute(
|
||||||
|
{
|
||||||
|
params: ConversationIdSchema,
|
||||||
|
allowJWT: true,
|
||||||
|
authorization: {
|
||||||
|
action: "oauth",
|
||||||
|
},
|
||||||
|
corsStrategy: "all",
|
||||||
|
},
|
||||||
|
async ({ authentication, params }) => {
|
||||||
|
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error("No workspace found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the service to get the redirect URL
|
||||||
|
const read = await readConversation(params.conversationId);
|
||||||
|
|
||||||
|
return json(read);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { action, loader };
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
|
|
||||||
|
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||||
|
import {
|
||||||
|
createConversation,
|
||||||
|
CreateConversationSchema,
|
||||||
|
getCurrentConversationRun,
|
||||||
|
readConversation,
|
||||||
|
stopConversation,
|
||||||
|
} from "~/services/conversation.server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ConversationIdSchema = z.object({
|
||||||
|
conversationId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { action, loader } = createActionApiRoute(
|
||||||
|
{
|
||||||
|
params: ConversationIdSchema,
|
||||||
|
allowJWT: true,
|
||||||
|
authorization: {
|
||||||
|
action: "oauth",
|
||||||
|
},
|
||||||
|
corsStrategy: "all",
|
||||||
|
},
|
||||||
|
async ({ authentication, params }) => {
|
||||||
|
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error("No workspace found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the service to get the redirect URL
|
||||||
|
const run = await getCurrentConversationRun(
|
||||||
|
params.conversationId,
|
||||||
|
workspace?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return json(run);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { action, loader };
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
|
|
||||||
|
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||||
|
import {
|
||||||
|
createConversation,
|
||||||
|
CreateConversationSchema,
|
||||||
|
readConversation,
|
||||||
|
stopConversation,
|
||||||
|
} from "~/services/conversation.server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ConversationIdSchema = z.object({
|
||||||
|
conversationId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { action, loader } = createActionApiRoute(
|
||||||
|
{
|
||||||
|
params: ConversationIdSchema,
|
||||||
|
allowJWT: true,
|
||||||
|
authorization: {
|
||||||
|
action: "oauth",
|
||||||
|
},
|
||||||
|
corsStrategy: "all",
|
||||||
|
},
|
||||||
|
async ({ authentication, params }) => {
|
||||||
|
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error("No workspace found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the service to get the redirect URL
|
||||||
|
const stop = await stopConversation(params.conversationId, workspace?.id);
|
||||||
|
|
||||||
|
return json(stop);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { action, loader };
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
|
|
||||||
|
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||||
|
import {
|
||||||
|
getConversation,
|
||||||
|
deleteConversation,
|
||||||
|
} from "~/services/conversation.server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ConversationIdSchema = z.object({
|
||||||
|
conversationId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { action, loader } = createActionApiRoute(
|
||||||
|
{
|
||||||
|
params: ConversationIdSchema,
|
||||||
|
allowJWT: true,
|
||||||
|
authorization: {
|
||||||
|
action: "oauth",
|
||||||
|
},
|
||||||
|
corsStrategy: "all",
|
||||||
|
},
|
||||||
|
async ({ params, authentication, request }) => {
|
||||||
|
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error("No workspace found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = request.method;
|
||||||
|
|
||||||
|
if (method === "GET") {
|
||||||
|
// Get a conversation by ID
|
||||||
|
const conversation = await getConversation(params.conversationId);
|
||||||
|
return json(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "DELETE") {
|
||||||
|
// Soft delete a conversation
|
||||||
|
const deleted = await deleteConversation(params.conversationId);
|
||||||
|
return json(deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method not allowed
|
||||||
|
return new Response("Method Not Allowed", { status: 405 });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { action, loader };
|
||||||
37
apps/webapp/app/routes/api.v1.conversation._index.tsx
Normal file
37
apps/webapp/app/routes/api.v1.conversation._index.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||||
|
|
||||||
|
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||||
|
import {
|
||||||
|
createConversation,
|
||||||
|
CreateConversationSchema,
|
||||||
|
} from "~/services/conversation.server";
|
||||||
|
|
||||||
|
const { action, loader } = createActionApiRoute(
|
||||||
|
{
|
||||||
|
body: CreateConversationSchema,
|
||||||
|
allowJWT: true,
|
||||||
|
authorization: {
|
||||||
|
action: "oauth",
|
||||||
|
},
|
||||||
|
corsStrategy: "all",
|
||||||
|
},
|
||||||
|
async ({ body, authentication }) => {
|
||||||
|
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error("No workspace found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the service to get the redirect URL
|
||||||
|
const conversation = await createConversation(
|
||||||
|
workspace?.id,
|
||||||
|
authentication.userId,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
|
||||||
|
return json(conversation);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { action, loader };
|
||||||
228
apps/webapp/app/services/conversation.server.ts
Normal file
228
apps/webapp/app/services/conversation.server.ts
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import { UserTypeEnum } from "@core/types";
|
||||||
|
|
||||||
|
import { auth, runs, tasks } from "@trigger.dev/sdk/v3";
|
||||||
|
import { prisma } from "~/db.server";
|
||||||
|
import { getOrCreatePersonalAccessToken } from "./personalAccessToken.server";
|
||||||
|
import { createConversationTitle } from "~/trigger/conversation/create-conversation-title";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const CreateConversationSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
conversationId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateConversationDto = z.infer<typeof CreateConversationSchema>;
|
||||||
|
|
||||||
|
// Create a new conversation
|
||||||
|
export async function createConversation(
|
||||||
|
workspaceId: string,
|
||||||
|
userId: string,
|
||||||
|
conversationData: CreateConversationDto,
|
||||||
|
) {
|
||||||
|
const { title, conversationId, ...otherData } = conversationData;
|
||||||
|
// Ensure PAT exists for the user
|
||||||
|
await getOrCreatePersonalAccessToken({ name: "trigger", userId });
|
||||||
|
|
||||||
|
if (conversationId) {
|
||||||
|
// Add a new message to an existing conversation
|
||||||
|
const conversationHistory = await prisma.conversationHistory.create({
|
||||||
|
data: {
|
||||||
|
...otherData,
|
||||||
|
userType: UserTypeEnum.User,
|
||||||
|
...(userId && {
|
||||||
|
user: {
|
||||||
|
connect: { id: userId },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
conversation: {
|
||||||
|
connect: { id: conversationId },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
conversation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// No context logic here
|
||||||
|
const handler = await tasks.trigger(
|
||||||
|
"chat",
|
||||||
|
{
|
||||||
|
conversationHistoryId: conversationHistory.id,
|
||||||
|
conversationId: conversationHistory.conversation.id,
|
||||||
|
},
|
||||||
|
{ tags: [conversationHistory.id, workspaceId, conversationId] },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: handler.id,
|
||||||
|
token: handler.publicAccessToken,
|
||||||
|
conversationId: conversationHistory.conversation.id,
|
||||||
|
conversationHistoryId: conversationHistory.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new conversation and its first message
|
||||||
|
const conversation = await prisma.conversation.create({
|
||||||
|
data: {
|
||||||
|
workspaceId,
|
||||||
|
userId,
|
||||||
|
title:
|
||||||
|
title?.substring(0, 100) ?? conversationData.message.substring(0, 100),
|
||||||
|
ConversationHistory: {
|
||||||
|
create: {
|
||||||
|
userId,
|
||||||
|
userType: UserTypeEnum.User,
|
||||||
|
...otherData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
ConversationHistory: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const conversationHistory = conversation.ConversationHistory[0];
|
||||||
|
|
||||||
|
// Trigger conversation title task
|
||||||
|
await tasks.trigger<typeof createConversationTitle>(
|
||||||
|
createConversationTitle.id,
|
||||||
|
{
|
||||||
|
conversationId: conversation.id,
|
||||||
|
message: conversationData.message,
|
||||||
|
},
|
||||||
|
{ tags: [conversation.id, workspaceId] },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = await tasks.trigger(
|
||||||
|
"chat",
|
||||||
|
{
|
||||||
|
conversationHistoryId: conversationHistory.id,
|
||||||
|
conversationId: conversation.id,
|
||||||
|
},
|
||||||
|
{ tags: [conversationHistory.id, workspaceId, conversation.id] },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: handler.id,
|
||||||
|
token: handler.publicAccessToken,
|
||||||
|
conversationId: conversation.id,
|
||||||
|
conversationHistoryId: conversationHistory.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a conversation by ID
|
||||||
|
export async function getConversation(conversationId: string) {
|
||||||
|
return prisma.conversation.findUnique({
|
||||||
|
where: { id: conversationId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a conversation (soft delete)
|
||||||
|
export async function deleteConversation(conversationId: string) {
|
||||||
|
return prisma.conversation.update({
|
||||||
|
where: { id: conversationId },
|
||||||
|
data: {
|
||||||
|
deleted: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark a conversation as read
|
||||||
|
export async function readConversation(conversationId: string) {
|
||||||
|
return prisma.conversation.update({
|
||||||
|
where: { id: conversationId },
|
||||||
|
data: { unread: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentConversationRun(
|
||||||
|
conversationId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
) {
|
||||||
|
const conversationHistory = await prisma.conversationHistory.findFirst({
|
||||||
|
where: {
|
||||||
|
conversationId,
|
||||||
|
conversation: {
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
updatedAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversationHistory) {
|
||||||
|
throw new Error("No run found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await runs.list({
|
||||||
|
tag: [conversationId, conversationHistory.id],
|
||||||
|
status: ["QUEUED", "EXECUTING"],
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = response.data[0];
|
||||||
|
if (!run) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicToken = await auth.createPublicToken({
|
||||||
|
scopes: {
|
||||||
|
read: {
|
||||||
|
runs: [run.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: run.id,
|
||||||
|
token: publicToken,
|
||||||
|
conversationId,
|
||||||
|
conversationHistoryId: conversationHistory.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopConversation(
|
||||||
|
conversationId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
) {
|
||||||
|
const conversationHistory = await prisma.conversationHistory.findFirst({
|
||||||
|
where: {
|
||||||
|
conversationId,
|
||||||
|
conversation: {
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
updatedAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversationHistory) {
|
||||||
|
throw new Error("No run found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await runs.list({
|
||||||
|
tag: [conversationId, conversationHistory.id],
|
||||||
|
status: ["QUEUED", "EXECUTING"],
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = response.data[0];
|
||||||
|
if (!run) {
|
||||||
|
await prisma.conversation.update({
|
||||||
|
where: {
|
||||||
|
id: conversationId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "failed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await runs.cancel(run.id);
|
||||||
|
}
|
||||||
@ -397,8 +397,6 @@ export function createActionApiRoute<
|
|||||||
maxContentLength,
|
maxContentLength,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
console.log(options);
|
|
||||||
|
|
||||||
async function loader({ request, params }: LoaderFunctionArgs) {
|
async function loader({ request, params }: LoaderFunctionArgs) {
|
||||||
if (corsStrategy !== "none" && request.method.toUpperCase() === "OPTIONS") {
|
if (corsStrategy !== "none" && request.method.toUpperCase() === "OPTIONS") {
|
||||||
return apiCors(request, json({}));
|
return apiCors(request, json({}));
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { ActionStatusEnum, LLMMappings } from "@core/types";
|
import { ActionStatusEnum } from "@core/types";
|
||||||
import { logger } from "@trigger.dev/sdk/v3";
|
import { logger } from "@trigger.dev/sdk/v3";
|
||||||
import {
|
import {
|
||||||
type CoreMessage,
|
type CoreMessage,
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { LLMMappings } from "@core/types";
|
||||||
|
import { logger, task } from "@trigger.dev/sdk/v3";
|
||||||
|
import { generate } from "../chat/stream-utils";
|
||||||
|
import { conversationTitlePrompt } from "./prompt";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
export const createConversationTitle = task({
|
||||||
|
id: "create-conversation-title",
|
||||||
|
run: async (payload: { conversationId: string; message: string }) => {
|
||||||
|
let conversationTitleResponse = "";
|
||||||
|
const gen = generate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: conversationTitlePrompt.replace(
|
||||||
|
"{{message}}",
|
||||||
|
payload.message,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
() => {},
|
||||||
|
undefined,
|
||||||
|
"",
|
||||||
|
LLMMappings.CLAUDESONNET,
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const chunk of gen) {
|
||||||
|
if (typeof chunk === "string") {
|
||||||
|
conversationTitleResponse += chunk;
|
||||||
|
} else if (chunk && typeof chunk === "object" && chunk.message) {
|
||||||
|
conversationTitleResponse += chunk.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputMatch = conversationTitleResponse.match(
|
||||||
|
/<output>(.*?)<\/output>/s,
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Conversation title data: ${JSON.stringify(outputMatch)}`);
|
||||||
|
|
||||||
|
if (!outputMatch) {
|
||||||
|
logger.error("No output found in recurrence response");
|
||||||
|
throw new Error("Invalid response format from AI");
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonStr = outputMatch[1].trim();
|
||||||
|
const conversationTitleData = JSON.parse(jsonStr);
|
||||||
|
|
||||||
|
if (conversationTitleData) {
|
||||||
|
await prisma.conversation.update({
|
||||||
|
where: {
|
||||||
|
id: payload.conversationId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: conversationTitleData.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
28
apps/webapp/app/trigger/conversation/prompt.ts
Normal file
28
apps/webapp/app/trigger/conversation/prompt.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export const conversationTitlePrompt = `You are an AI assistant specialized in generating concise and informative conversation titles. Your task is to analyze the given message and context to create an appropriate title.
|
||||||
|
|
||||||
|
Here is the message:
|
||||||
|
<message>
|
||||||
|
{{message}}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
Please follow these steps:
|
||||||
|
- Extract the core topic/intent from the message
|
||||||
|
- Create a clear, concise title
|
||||||
|
- Focus on the main subject or action
|
||||||
|
- Avoid unnecessary words
|
||||||
|
- Maximum length: 60 characters
|
||||||
|
|
||||||
|
Before providing output, analyze in <title_analysis> tags:
|
||||||
|
- Key elements from message
|
||||||
|
- Main topic/action
|
||||||
|
- Relevant actors/context
|
||||||
|
- Your title formation process
|
||||||
|
|
||||||
|
Provide final output in this format:
|
||||||
|
<output>
|
||||||
|
{
|
||||||
|
"title": "Your generated title"
|
||||||
|
}
|
||||||
|
</output>
|
||||||
|
|
||||||
|
If message is empty or contains no meaningful content, return {"title": "New Conversation"}`;
|
||||||
@ -1,38 +1,38 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
// import { PrismaClient } from "@prisma/client";
|
||||||
import { IntegrationPayloadEventType } from '@redplanethq/sol-sdk';
|
// import { IntegrationPayloadEventType } from "@core/types";
|
||||||
import { logger, schedules, tasks } from '@trigger.dev/sdk/v3';
|
// import { logger, schedules, tasks } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
import { integrationRun } from './integration-run';
|
// import { integrationRun } from "./integration-run";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
// const prisma = new PrismaClient();
|
||||||
|
|
||||||
export const integrationRunSchedule = schedules.task({
|
// export const integrationRunSchedule = schedules.task({
|
||||||
id: 'integration-run-schedule',
|
// id: "integration-run-schedule",
|
||||||
run: async (payload) => {
|
// run: async (payload) => {
|
||||||
const { externalId } = payload;
|
// const { externalId } = payload;
|
||||||
const integrationAccount = await prisma.integrationAccount.findUnique({
|
// const integrationAccount = await prisma.integrationAccount.findUnique({
|
||||||
where: { id: externalId },
|
// where: { id: externalId },
|
||||||
include: {
|
// include: {
|
||||||
integrationDefinition: true,
|
// integrationDefinition: true,
|
||||||
workspace: true,
|
// workspace: true,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (!integrationAccount) {
|
// if (!integrationAccount) {
|
||||||
const deletedSchedule = await schedules.del(externalId);
|
// const deletedSchedule = await schedules.del(externalId);
|
||||||
logger.info('Deleting schedule as integration account is not there');
|
// logger.info("Deleting schedule as integration account is not there");
|
||||||
return deletedSchedule;
|
// return deletedSchedule;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const pat = await prisma.personalAccessToken.findFirst({
|
// const pat = await prisma.personalAccessToken.findFirst({
|
||||||
where: { userId: integrationAccount.workspace.userId, name: 'default' },
|
// where: { userId: integrationAccount.workspace.userId, name: "default" },
|
||||||
});
|
// });
|
||||||
|
|
||||||
return await tasks.trigger<typeof integrationRun>('integration-run', {
|
// return await tasks.trigger<typeof integrationRun>("integration-run", {
|
||||||
event: IntegrationPayloadEventType.SCHEDULED_SYNC,
|
// event: IntegrationPayloadEventType.SCHEDULED_SYNC,
|
||||||
pat: pat.token,
|
// pat: pat.token,
|
||||||
integrationAccount,
|
// integrationAccount,
|
||||||
integrationDefinition: integrationAccount.integrationDefinition,
|
// integrationDefinition: integrationAccount.integrationDefinition,
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|||||||
@ -1,90 +1,87 @@
|
|||||||
import createLoadRemoteModule, {
|
// import createLoadRemoteModule, {
|
||||||
createRequires,
|
// createRequires,
|
||||||
} from '@paciolan/remote-module-loader';
|
// } from "@paciolan/remote-module-loader";
|
||||||
import {
|
|
||||||
IntegrationAccount,
|
|
||||||
IntegrationDefinition,
|
|
||||||
} from '@redplanethq/sol-sdk';
|
|
||||||
import { logger, task } from '@trigger.dev/sdk/v3';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const fetcher = async (url: string) => {
|
// import { logger, task } from "@trigger.dev/sdk/v3";
|
||||||
// Handle remote URLs with axios
|
// import axios from "axios";
|
||||||
const response = await axios.get(url);
|
|
||||||
|
|
||||||
return response.data;
|
// const fetcher = async (url: string) => {
|
||||||
};
|
// // Handle remote URLs with axios
|
||||||
|
// const response = await axios.get(url);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// return response.data;
|
||||||
const loadRemoteModule = async (requires: any) =>
|
// };
|
||||||
createLoadRemoteModule({ fetcher, requires });
|
|
||||||
|
|
||||||
function createAxiosInstance(token: string) {
|
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const instance = axios.create();
|
// const loadRemoteModule = async (requires: any) =>
|
||||||
|
// createLoadRemoteModule({ fetcher, requires });
|
||||||
|
|
||||||
instance.interceptors.request.use((config) => {
|
// function createAxiosInstance(token: string) {
|
||||||
// Check if URL starts with /api and doesn't have a full host
|
// const instance = axios.create();
|
||||||
if (config.url?.startsWith('/api')) {
|
|
||||||
config.url = `${process.env.BACKEND_HOST}${config.url.replace('/api/', '/')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
// instance.interceptors.request.use((config) => {
|
||||||
config.url.includes(process.env.FRONTEND_HOST) ||
|
// // Check if URL starts with /api and doesn't have a full host
|
||||||
config.url.includes(process.env.BACKEND_HOST)
|
// if (config.url?.startsWith("/api")) {
|
||||||
) {
|
// config.url = `${process.env.BACKEND_HOST}${config.url.replace("/api/", "/")}`;
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
// if (
|
||||||
});
|
// config.url.includes(process.env.FRONTEND_HOST) ||
|
||||||
|
// config.url.includes(process.env.BACKEND_HOST)
|
||||||
|
// ) {
|
||||||
|
// config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
// }
|
||||||
|
|
||||||
return instance;
|
// return config;
|
||||||
}
|
// });
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// return instance;
|
||||||
const getRequires = (axios: any) => createRequires({ axios });
|
// }
|
||||||
|
|
||||||
export const integrationRun = task({
|
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
id: 'integration-run',
|
// const getRequires = (axios: any) => createRequires({ axios });
|
||||||
run: async ({
|
|
||||||
pat,
|
|
||||||
eventBody,
|
|
||||||
integrationAccount,
|
|
||||||
integrationDefinition,
|
|
||||||
event,
|
|
||||||
}: {
|
|
||||||
pat: string;
|
|
||||||
// This is the event you want to pass to the integration
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
event: any;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
eventBody?: any;
|
|
||||||
integrationDefinition: IntegrationDefinition;
|
|
||||||
integrationAccount?: IntegrationAccount;
|
|
||||||
}) => {
|
|
||||||
const remoteModuleLoad = await loadRemoteModule(
|
|
||||||
getRequires(createAxiosInstance(pat)),
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info(
|
// export const integrationRun = task({
|
||||||
`${integrationDefinition.url}/${integrationDefinition.version}/backend/index.js`,
|
// id: "integration-run",
|
||||||
);
|
// run: async ({
|
||||||
|
// pat,
|
||||||
|
// eventBody,
|
||||||
|
// integrationAccount,
|
||||||
|
// integrationDefinition,
|
||||||
|
// event,
|
||||||
|
// }: {
|
||||||
|
// pat: string;
|
||||||
|
// // This is the event you want to pass to the integration
|
||||||
|
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
// event: any;
|
||||||
|
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
// eventBody?: any;
|
||||||
|
// integrationDefinition: IntegrationDefinition;
|
||||||
|
// integrationAccount?: IntegrationAccount;
|
||||||
|
// }) => {
|
||||||
|
// const remoteModuleLoad = await loadRemoteModule(
|
||||||
|
// getRequires(createAxiosInstance(pat)),
|
||||||
|
// );
|
||||||
|
|
||||||
const integrationFunction = await remoteModuleLoad(
|
// logger.info(
|
||||||
`${integrationDefinition.url}/${integrationDefinition.version}/backend/index.js`,
|
// `${integrationDefinition.url}/${integrationDefinition.version}/backend/index.js`,
|
||||||
);
|
// );
|
||||||
|
|
||||||
// const integrationFunction = await remoteModuleLoad(
|
// const integrationFunction = await remoteModuleLoad(
|
||||||
// `${integrationDefinition.url}`,
|
// `${integrationDefinition.url}/${integrationDefinition.version}/backend/index.js`,
|
||||||
// );
|
// );
|
||||||
|
|
||||||
return await integrationFunction.run({
|
// // const integrationFunction = await remoteModuleLoad(
|
||||||
integrationAccount,
|
// // `${integrationDefinition.url}`,
|
||||||
integrationDefinition,
|
// // );
|
||||||
event,
|
|
||||||
eventBody: {
|
// return await integrationFunction.run({
|
||||||
...(eventBody ? eventBody : {}),
|
// integrationAccount,
|
||||||
},
|
// integrationDefinition,
|
||||||
});
|
// event,
|
||||||
},
|
// eventBody: {
|
||||||
});
|
// ...(eventBody ? eventBody : {}),
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|||||||
@ -1,64 +1,64 @@
|
|||||||
import { PrismaClient } from "@prisma/client";
|
// import { PrismaClient } from "@prisma/client";
|
||||||
import { logger, schedules, task } from "@trigger.dev/sdk/v3";
|
// import { logger, schedules, task } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
import { integrationRunSchedule } from "./integration-run-schedule";
|
// import { integrationRunSchedule } from "./integration-run-schedule";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
// const prisma = new PrismaClient();
|
||||||
|
|
||||||
export const scheduler = task({
|
// export const scheduler = task({
|
||||||
id: "scheduler",
|
// id: "scheduler",
|
||||||
run: async (payload: { integrationAccountId: string }) => {
|
// run: async (payload: { integrationAccountId: string }) => {
|
||||||
const { integrationAccountId } = payload;
|
// const { integrationAccountId } = payload;
|
||||||
|
|
||||||
const integrationAccount = await prisma.integrationAccount.findUnique({
|
// const integrationAccount = await prisma.integrationAccount.findUnique({
|
||||||
where: { id: integrationAccountId, deleted: null },
|
// where: { id: integrationAccountId, deleted: null },
|
||||||
include: {
|
// include: {
|
||||||
integrationDefinition: true,
|
// integrationDefinition: true,
|
||||||
workspace: true,
|
// workspace: true,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (!integrationAccount) {
|
// if (!integrationAccount) {
|
||||||
logger.error("Integration account not found");
|
// logger.error("Integration account not found");
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!integrationAccount.workspace) {
|
// if (!integrationAccount.workspace) {
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const spec = integrationAccount.integrationDefinition.spec as any;
|
// const spec = integrationAccount.integrationDefinition.spec as any;
|
||||||
|
|
||||||
if (spec.schedule && spec.schedule.frequency) {
|
// if (spec.schedule && spec.schedule.frequency) {
|
||||||
const createdSchedule = await schedules.create({
|
// const createdSchedule = await schedules.create({
|
||||||
// The id of the scheduled task you want to attach to.
|
// // The id of the scheduled task you want to attach to.
|
||||||
task: integrationRunSchedule.id,
|
// task: integrationRunSchedule.id,
|
||||||
// The schedule in cron format.
|
// // The schedule in cron format.
|
||||||
cron: spec.schedule.frequency,
|
// cron: spec.schedule.frequency,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
timezone: (integrationAccount.workspace.preferences as any).timezone,
|
// timezone: (integrationAccount.workspace.preferences as any).timezone,
|
||||||
// this is required, it prevents you from creating duplicate schedules. It will update the schedule if it already exists.
|
// // this is required, it prevents you from creating duplicate schedules. It will update the schedule if it already exists.
|
||||||
deduplicationKey: integrationAccount.id,
|
// deduplicationKey: integrationAccount.id,
|
||||||
externalId: integrationAccount.id,
|
// externalId: integrationAccount.id,
|
||||||
});
|
// });
|
||||||
|
|
||||||
await prisma.integrationAccount.update({
|
// await prisma.integrationAccount.update({
|
||||||
where: {
|
// where: {
|
||||||
id: integrationAccount.id,
|
// id: integrationAccount.id,
|
||||||
},
|
// },
|
||||||
data: {
|
// data: {
|
||||||
settings: {
|
// settings: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
...(integrationAccount.settings as any),
|
// ...(integrationAccount.settings as any),
|
||||||
scheduleId: createdSchedule.id,
|
// scheduleId: createdSchedule.id,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
return createdSchedule;
|
// return createdSchedule;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return "No schedule for this task";
|
// return "No schedule for this task";
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|||||||
@ -8,7 +8,8 @@
|
|||||||
"dev": "node ./server.mjs",
|
"dev": "node ./server.mjs",
|
||||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||||
"start": "remix-serve ./build/server/index.js",
|
"start": "remix-serve ./build/server/index.js",
|
||||||
"typecheck": "tsc"
|
"typecheck": "tsc",
|
||||||
|
"trigger:dev": "npx trigger.dev@latest dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^1.2.12",
|
"@ai-sdk/anthropic": "^1.2.12",
|
||||||
@ -20,6 +21,7 @@
|
|||||||
"@core/database": "workspace:*",
|
"@core/database": "workspace:*",
|
||||||
"@core/types": "workspace:*",
|
"@core/types": "workspace:*",
|
||||||
"@mjackson/headers": "0.11.1",
|
"@mjackson/headers": "0.11.1",
|
||||||
|
"@modelcontextprotocol/sdk": "1.13.2",
|
||||||
"@nichtsam/remix-auth-email-link": "3.0.0",
|
"@nichtsam/remix-auth-email-link": "3.0.0",
|
||||||
"@opentelemetry/api": "1.9.0",
|
"@opentelemetry/api": "1.9.0",
|
||||||
"@prisma/client": "*",
|
"@prisma/client": "*",
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { defineConfig } from "@trigger.dev/sdk/v3";
|
import { defineConfig } from "@trigger.dev/sdk/v3";
|
||||||
import { env } from "~/env.server";
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
project: env.TRIGGER_PROJECT_ID,
|
project: process.env.TRIGGER_PROJECT_ID as string,
|
||||||
runtime: "node",
|
runtime: "node",
|
||||||
logLevel: "log",
|
logLevel: "log",
|
||||||
// The max compute seconds a task is allowed to run. If the task run exceeds this duration, it will be stopped.
|
// The max compute seconds a task is allowed to run. If the task run exceeds this duration, it will be stopped.
|
||||||
|
|||||||
@ -17,7 +17,8 @@
|
|||||||
"db:seed": "dotenv -- turbo run db:seed",
|
"db:seed": "dotenv -- turbo run db:seed",
|
||||||
"db:studio": "dotenv -- turbo run db:studio",
|
"db:studio": "dotenv -- turbo run db:studio",
|
||||||
"db:populate": "dotenv -- turbo run db:populate",
|
"db:populate": "dotenv -- turbo run db:populate",
|
||||||
"generate": "dotenv -- turbo run generate"
|
"generate": "dotenv -- turbo run generate",
|
||||||
|
"trigger:dev": "dotenv -- turbo run trigger:dev"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"dotenv-cli": "^7.4.4",
|
"dotenv-cli": "^7.4.4",
|
||||||
|
|||||||
9
packages/sdk/.eslintrc.js
Normal file
9
packages/sdk/.eslintrc.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['@redplanethq/eslint-config/internal.js'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
rules: {
|
||||||
|
'no-redeclare': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
4
packages/sdk/.prettierrc.json
Normal file
4
packages/sdk/.prettierrc.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
49
packages/sdk/package.json
Normal file
49
packages/sdk/package.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "@redplanethq/sol-sdk",
|
||||||
|
"version": "0.2.18",
|
||||||
|
"description": "Sol Node.JS SDK",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/index.d.mts",
|
||||||
|
"default": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"build": "npm run clean && npm run build:tsup",
|
||||||
|
"build:tsup": "tsup --dts-resolve",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@core/types": "workspace:*",
|
||||||
|
"@types/configstore": "^6.0.2",
|
||||||
|
"@types/debug": "^4.1.7",
|
||||||
|
"@types/node": "18",
|
||||||
|
"@types/slug": "^5.0.3",
|
||||||
|
"@types/uuid": "^9.0.0",
|
||||||
|
"encoding": "^0.1.13",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"tsup": "^8.0.1",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.3.0"
|
||||||
|
}
|
||||||
1
packages/sdk/src/index.ts
Normal file
1
packages/sdk/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@core/types';
|
||||||
38
packages/sdk/tsconfig.json
Normal file
38
packages/sdk/tsconfig.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"include": ["./src/**/*.ts", "tsup.config.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src",
|
||||||
|
|
||||||
|
"moduleResolution": "node",
|
||||||
|
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"useUnknownInCatchVariables": false,
|
||||||
|
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
|
||||||
|
"removeComments": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "ES2022",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable"],
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"stripInternal": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
22
packages/sdk/tsup.config.ts
Normal file
22
packages/sdk/tsup.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Options, defineConfig as defineConfigTSUP } from 'tsup';
|
||||||
|
|
||||||
|
const options: Options = {
|
||||||
|
name: 'main',
|
||||||
|
config: 'tsconfig.json',
|
||||||
|
entry: ['./src/index.ts'],
|
||||||
|
outDir: './dist',
|
||||||
|
platform: 'node',
|
||||||
|
format: ['cjs', 'esm'],
|
||||||
|
legacyOutput: false,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: true,
|
||||||
|
bundle: true,
|
||||||
|
splitting: false,
|
||||||
|
dts: true,
|
||||||
|
treeshake: {
|
||||||
|
preset: 'recommended',
|
||||||
|
},
|
||||||
|
external: ['axios'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineConfigTSUP(options);
|
||||||
@ -3,3 +3,4 @@ export * from "./graph";
|
|||||||
export * from "./conversation-execution-step";
|
export * from "./conversation-execution-step";
|
||||||
export * from "./oauth";
|
export * from "./oauth";
|
||||||
export * from "./integration";
|
export * from "./integration";
|
||||||
|
export * from "./user";
|
||||||
|
|||||||
5
packages/types/src/user/index.ts
Normal file
5
packages/types/src/user/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum UserTypeEnum {
|
||||||
|
Agent = "Agent",
|
||||||
|
User = "User",
|
||||||
|
System = "System",
|
||||||
|
}
|
||||||
564
pnpm-lock.yaml
generated
564
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generate": {
|
"generate": {
|
||||||
"dependsOn": [ "^generate" ]
|
"dependsOn": [ "^generate" ]
|
||||||
|
},
|
||||||
|
"trigger:dev": {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"globalDependencies": [ ".env" ],
|
"globalDependencies": [ ".env" ],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user