diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx index 25eeb4cab..3cd1fffe6 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx @@ -16,6 +16,7 @@ export function getDocumentTypeLabel(type: string): string { FILE: "File", SLACK_CONNECTOR: "Slack", TEAMS_CONNECTOR: "Microsoft Teams", + ONEDRIVE_FILE: "OneDrive", NOTION_CONNECTOR: "Notion", YOUTUBE_VIDEO: "YouTube Video", GITHUB_CONNECTOR: "GitHub", diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/onedrive-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/onedrive-config.tsx new file mode 100644 index 000000000..e5f6caf06 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/onedrive-config.tsx @@ -0,0 +1,443 @@ +"use client"; + +import { + ChevronRight, + File, + FileSpreadsheet, + FileText, + FolderClosed, + FolderOpen, + Image, + Presentation, + X, +} from "lucide-react"; +import type { FC } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Spinner } from "@/components/ui/spinner"; +import { Switch } from "@/components/ui/switch"; +import { useAuth } from "@/context/AuthContext"; +import type { ConnectorConfigProps } from "../index"; + +interface SelectedItem { + id: string; + name: string; +} + +interface IndexingOptions { + max_files_per_folder: number; + incremental_sync: boolean; + include_subfolders: boolean; +} + +interface OneDriveItem { + id: string; + name: string; + isFolder: boolean; + size?: number; + lastModifiedDateTime?: string; + file?: { mimeType: string }; + folder?: { childCount: number }; + webUrl?: string; +} + +const DEFAULT_INDEXING_OPTIONS: IndexingOptions = { + max_files_per_folder: 100, + incremental_sync: true, + include_subfolders: true, +}; + +function getFileIconFromName(fileName: string, className = "size-3.5 shrink-0") { + const lowerName = fileName.toLowerCase(); + if (lowerName.endsWith(".xlsx") || lowerName.endsWith(".xls") || lowerName.endsWith(".csv")) { + return ; + } + if (lowerName.endsWith(".pptx") || lowerName.endsWith(".ppt")) { + return ; + } + if (lowerName.endsWith(".docx") || lowerName.endsWith(".doc") || lowerName.endsWith(".txt")) { + return ; + } + if (/\.(png|jpe?g|gif|webp|svg)$/.test(lowerName)) { + return ; + } + return ; +} + +export const OneDriveConfig: FC = ({ connector, onConfigChange }) => { + const { authenticatedFetch } = useAuth(); + const existingFolders = (connector.config?.selected_folders as SelectedItem[] | undefined) || []; + const existingFiles = (connector.config?.selected_files as SelectedItem[] | undefined) || []; + const existingIndexingOptions = + (connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS; + + const [selectedFolders, setSelectedFolders] = useState(existingFolders); + const [selectedFiles, setSelectedFiles] = useState(existingFiles); + const [indexingOptions, setIndexingOptions] = useState(existingIndexingOptions); + + const [browserOpen, setBrowserOpen] = useState(false); + const [browseItems, setBrowseItems] = useState([]); + const [browseLoading, setBrowseLoading] = useState(false); + const [browseError, setBrowseError] = useState(null); + const [breadcrumbs, setBreadcrumbs] = useState<{ id: string; name: string }[]>([ + { id: "root", name: "My files" }, + ]); + + useEffect(() => { + const folders = (connector.config?.selected_folders as SelectedItem[] | undefined) || []; + const files = (connector.config?.selected_files as SelectedItem[] | undefined) || []; + const options = + (connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS; + setSelectedFolders(folders); + setSelectedFiles(files); + setIndexingOptions(options); + }, [connector.config]); + + const updateConfig = useCallback( + (folders: SelectedItem[], files: SelectedItem[], options: IndexingOptions) => { + if (onConfigChange) { + onConfigChange({ + ...connector.config, + selected_folders: folders, + selected_files: files, + indexing_options: options, + }); + } + }, + [onConfigChange, connector.config], + ); + + const fetchFolderContents = useCallback( + async (parentId: string) => { + setBrowseLoading(true); + setBrowseError(null); + try { + const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000"; + const url = `${backendUrl}/api/v1/connectors/${connector.id}/onedrive/folders?parent_id=${encodeURIComponent(parentId)}`; + const response = await authenticatedFetch(url); + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.detail || `Failed to load folder contents (${response.status})`); + } + const data = await response.json(); + setBrowseItems(data.items || []); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "Failed to load folder contents"; + setBrowseError(message); + } finally { + setBrowseLoading(false); + } + }, + [connector.id, authenticatedFetch], + ); + + const handleOpenBrowser = useCallback(() => { + setBrowserOpen(true); + setBreadcrumbs([{ id: "root", name: "My files" }]); + fetchFolderContents("root"); + }, [fetchFolderContents]); + + const handleNavigateFolder = useCallback( + (folderId: string, folderName: string) => { + setBreadcrumbs((prev) => [...prev, { id: folderId, name: folderName }]); + fetchFolderContents(folderId); + }, + [fetchFolderContents], + ); + + const handleBreadcrumbClick = useCallback( + (index: number) => { + const newBreadcrumbs = breadcrumbs.slice(0, index + 1); + setBreadcrumbs(newBreadcrumbs); + fetchFolderContents(newBreadcrumbs[newBreadcrumbs.length - 1].id); + }, + [breadcrumbs, fetchFolderContents], + ); + + const isItemSelected = useCallback( + (item: OneDriveItem) => { + if (item.isFolder) { + return selectedFolders.some((f) => f.id === item.id); + } + return selectedFiles.some((f) => f.id === item.id); + }, + [selectedFolders, selectedFiles], + ); + + const handleToggleItem = useCallback( + (item: OneDriveItem) => { + if (item.isFolder) { + const exists = selectedFolders.some((f) => f.id === item.id); + const newFolders = exists + ? selectedFolders.filter((f) => f.id !== item.id) + : [...selectedFolders, { id: item.id, name: item.name }]; + setSelectedFolders(newFolders); + updateConfig(newFolders, selectedFiles, indexingOptions); + } else { + const exists = selectedFiles.some((f) => f.id === item.id); + const newFiles = exists + ? selectedFiles.filter((f) => f.id !== item.id) + : [...selectedFiles, { id: item.id, name: item.name }]; + setSelectedFiles(newFiles); + updateConfig(selectedFolders, newFiles, indexingOptions); + } + }, + [selectedFolders, selectedFiles, indexingOptions, updateConfig], + ); + + const handleRemoveFolder = (folderId: string) => { + const newFolders = selectedFolders.filter((f) => f.id !== folderId); + setSelectedFolders(newFolders); + updateConfig(newFolders, selectedFiles, indexingOptions); + }; + + const handleRemoveFile = (fileId: string) => { + const newFiles = selectedFiles.filter((f) => f.id !== fileId); + setSelectedFiles(newFiles); + updateConfig(selectedFolders, newFiles, indexingOptions); + }; + + const handleIndexingOptionChange = (key: keyof IndexingOptions, value: number | boolean) => { + const newOptions = { ...indexingOptions, [key]: value }; + setIndexingOptions(newOptions); + updateConfig(selectedFolders, selectedFiles, newOptions); + }; + + const isAuthExpired = connector.config?.auth_expired === true; + const totalSelected = selectedFolders.length + selectedFiles.length; + + return ( +
+ {/* Folder & File Selection */} +
+
+

Folder & File Selection

+

+ Browse and select specific folders and/or files to index from your OneDrive. +

+
+ + {totalSelected > 0 && ( +
+

+ Selected {totalSelected} item{totalSelected > 1 ? "s" : ""} +

+
+ {selectedFolders.map((folder) => ( +
+ + {folder.name} + +
+ ))} + {selectedFiles.map((file) => ( +
+ {getFileIconFromName(file.name)} + {file.name} + +
+ ))} +
+
+ )} + + {!browserOpen ? ( + + ) : ( +
+ {/* Breadcrumbs */} +
+ {breadcrumbs.map((crumb, index) => ( + + {index > 0 && } + + + ))} +
+ + {/* File list */} +
+ {browseLoading ? ( +
+ +
+ ) : browseError ? ( +
{browseError}
+ ) : browseItems.length === 0 ? ( +
This folder is empty
+ ) : ( + browseItems.map((item) => ( +
+ handleToggleItem(item)} + className="size-3.5" + /> + {item.isFolder ? ( + + ) : ( +
+ {getFileIconFromName(item.name)} + {item.name} +
+ )} +
+ )) + )} +
+ +
+ +
+
+ )} + + {isAuthExpired && ( +

+ Your OneDrive authentication has expired. Please re-authenticate using the button below. +

+ )} +
+ + {/* Indexing Options */} +
+
+

Indexing Options

+

+ Configure how files are indexed from your OneDrive. +

+
+ +
+
+
+ +

+ Maximum number of files to index from each folder +

+
+ +
+
+ +
+
+ +

+ Only sync changes since last index (faster). Disable for a full re-index. +

+
+ handleIndexingOptionChange("incremental_sync", checked)} + /> +
+ +
+
+ +

+ Recursively index files in subfolders of selected folders +

+
+ handleIndexingOptionChange("include_subfolders", checked)} + /> +
+
+
+ ); +}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx index cef0c99ac..ba43ce823 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx @@ -21,6 +21,7 @@ import { MCPConfig } from "./components/mcp-config"; import { ObsidianConfig } from "./components/obsidian-config"; import { SlackConfig } from "./components/slack-config"; import { TavilyApiConfig } from "./components/tavily-api-config"; +import { OneDriveConfig } from "./components/onedrive-config"; import { TeamsConfig } from "./components/teams-config"; import { WebcrawlerConfig } from "./components/webcrawler-config"; @@ -58,6 +59,8 @@ export function getConnectorConfigComponent( return DiscordConfig; case "TEAMS_CONNECTOR": return TeamsConfig; + case "ONEDRIVE_CONNECTOR": + return OneDriveConfig; case "CONFLUENCE_CONNECTOR": return ConfluenceConfig; case "BOOKSTACK_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx index 93d280a15..e50f61692 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx @@ -27,6 +27,7 @@ const REAUTH_ENDPOINTS: Partial> = { [EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR]: "/api/v1/auth/composio/connector/reauth", [EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: "/api/v1/auth/composio/connector/reauth", [EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: "/api/v1/auth/composio/connector/reauth", + [EnumConnectorName.ONEDRIVE_CONNECTOR]: "/api/v1/auth/onedrive/connector/reauth", }; interface ConnectorEditViewProps { diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index ab69d4ca2..969ae1897 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -61,6 +61,13 @@ export const OAUTH_CONNECTORS = [ connectorType: EnumConnectorName.TEAMS_CONNECTOR, authEndpoint: "/api/v1/auth/teams/connector/add/", }, + { + id: "onedrive-connector", + title: "OneDrive", + description: "Search your OneDrive files", + connectorType: EnumConnectorName.ONEDRIVE_CONNECTOR, + authEndpoint: "/api/v1/auth/onedrive/connector/add/", + }, { id: "discord-connector", title: "Discord", diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index 03d8a8fb4..0ee34d7c2 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -729,10 +729,11 @@ export const useConnectorDialog = () => { async (refreshConnectors: () => void) => { if (!indexingConfig || !searchSpaceId) return; - // Validate date range (skip for Google Drive, Composio Drive, and Webcrawler) + // Validate date range (skip for Google Drive, Composio Drive, OneDrive, and Webcrawler) if ( indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && indexingConfig.connectorType !== "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" && + indexingConfig.connectorType !== "ONEDRIVE_CONNECTOR" && indexingConfig.connectorType !== "WEBCRAWLER_CONNECTOR" ) { const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); @@ -778,10 +779,11 @@ export const useConnectorDialog = () => { }); } - // Handle Google Drive folder selection (regular and Composio) - if ( - (indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" || - indexingConfig.connectorType === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR") && + // Handle Google Drive / OneDrive folder selection (regular and Composio) + if ( + (indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" || + indexingConfig.connectorType === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" || + indexingConfig.connectorType === "ONEDRIVE_CONNECTOR") && indexingConnectorConfig ) { const selectedFolders = indexingConnectorConfig.selected_folders as @@ -967,10 +969,11 @@ export const useConnectorDialog = () => { async (refreshConnectors: () => void) => { if (!editingConnector || !searchSpaceId || isSaving) return; - // Validate date range (skip for Google Drive which uses folder selection, Webcrawler which uses config, and non-indexable connectors) + // Validate date range (skip for Google Drive/OneDrive which uses folder selection, Webcrawler which uses config, and non-indexable connectors) if ( editingConnector.is_indexable && editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && + editingConnector.connector_type !== "ONEDRIVE_CONNECTOR" && editingConnector.connector_type !== "WEBCRAWLER_CONNECTOR" ) { const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); @@ -986,11 +989,12 @@ export const useConnectorDialog = () => { return; } - // Prevent periodic indexing for Google Drive (regular or Composio) without folders/files selected + // Prevent periodic indexing for Google Drive / OneDrive (regular or Composio) without folders/files selected if ( periodicEnabled && (editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || - editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR") + editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" || + editingConnector.connector_type === "ONEDRIVE_CONNECTOR") ) { const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as | Array<{ id: string; name: string }> @@ -1043,7 +1047,8 @@ export const useConnectorDialog = () => { indexingDescription = "Settings saved."; } else if ( editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || - editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" + editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" || + editingConnector.connector_type === "ONEDRIVE_CONNECTOR" ) { // Google Drive (both regular and Composio) uses folder selection from config, not date ranges const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx index da6ad8540..8a1a78807 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx @@ -25,6 +25,7 @@ const REAUTH_ENDPOINTS: Partial> = { [EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR]: "/api/v1/auth/composio/connector/reauth", [EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: "/api/v1/auth/composio/connector/reauth", [EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: "/api/v1/auth/composio/connector/reauth", + [EnumConnectorName.ONEDRIVE_CONNECTOR]: "/api/v1/auth/onedrive/connector/reauth", [EnumConnectorName.JIRA_CONNECTOR]: "/api/v1/auth/jira/connector/reauth", [EnumConnectorName.CONFLUENCE_CONNECTOR]: "/api/v1/auth/confluence/connector/reauth", }; diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 1644b0163..ba3883adf 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -1090,6 +1090,12 @@ const TOOL_GROUPS: ToolGroup[] = [ connectorIcon: "google_drive", tooltip: "Create and delete files in Google Drive.", }, + { + label: "OneDrive", + tools: ["create_onedrive_file", "delete_onedrive_file"], + connectorIcon: "onedrive", + tooltip: "Create and delete files in OneDrive.", + }, { label: "Notion", tools: ["create_notion_page", "update_notion_page", "delete_notion_page"], diff --git a/surfsense_web/contracts/enums/connector.ts b/surfsense_web/contracts/enums/connector.ts index 45b13a20b..36d39f4fc 100644 --- a/surfsense_web/contracts/enums/connector.ts +++ b/surfsense_web/contracts/enums/connector.ts @@ -6,6 +6,7 @@ export enum EnumConnectorName { BAIDU_SEARCH_API = "BAIDU_SEARCH_API", SLACK_CONNECTOR = "SLACK_CONNECTOR", TEAMS_CONNECTOR = "TEAMS_CONNECTOR", + ONEDRIVE_CONNECTOR = "ONEDRIVE_CONNECTOR", NOTION_CONNECTOR = "NOTION_CONNECTOR", GITHUB_CONNECTOR = "GITHUB_CONNECTOR", LINEAR_CONNECTOR = "LINEAR_CONNECTOR", diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx index c9375a5ca..19b24cd59 100644 --- a/surfsense_web/contracts/enums/connectorIcons.tsx +++ b/surfsense_web/contracts/enums/connectorIcons.tsx @@ -39,6 +39,8 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas return Slack; case EnumConnectorName.TEAMS_CONNECTOR: return Microsoft Teams; + case EnumConnectorName.ONEDRIVE_CONNECTOR: + return OneDrive; case EnumConnectorName.NOTION_CONNECTOR: return Notion; case EnumConnectorName.DISCORD_CONNECTOR: @@ -98,6 +100,9 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas return ; case "GOOGLE_DRIVE_FILE": return Google Drive; + case "ONEDRIVE_FILE": + case "ONEDRIVE_CONNECTOR": + return OneDrive; case "COMPOSIO_GOOGLE_DRIVE_CONNECTOR": return Google Drive; case "COMPOSIO_GMAIL_CONNECTOR": diff --git a/surfsense_web/lib/apis/connectors-api.service.ts b/surfsense_web/lib/apis/connectors-api.service.ts index fafe1a8fa..062d3b780 100644 --- a/surfsense_web/lib/apis/connectors-api.service.ts +++ b/surfsense_web/lib/apis/connectors-api.service.ts @@ -277,6 +277,19 @@ class ConnectorsApiService { }>(`/api/v1/connectors/${connectorId}/drive-picker-token`); }; + /** + * List OneDrive folders and files + */ + listOneDriveFolders = async (request: { connector_id: number; parent_id?: string }) => { + const queryParams = request.parent_id + ? `?parent_id=${encodeURIComponent(request.parent_id)}` + : ""; + return baseApiService.get( + `/api/v1/connectors/${request.connector_id}/onedrive/folders${queryParams}`, + listGoogleDriveFoldersResponse + ); + }; + // ============================================================================= // MCP Connector Methods // ============================================================================= diff --git a/surfsense_web/lib/chat/streaming-state.ts b/surfsense_web/lib/chat/streaming-state.ts index cd0a4d7f6..71965a2cb 100644 --- a/surfsense_web/lib/chat/streaming-state.ts +++ b/surfsense_web/lib/chat/streaming-state.ts @@ -132,11 +132,30 @@ export function buildContentForPersistence( return parts.length > 0 ? parts : [{ type: "text", text: "" }]; } +export type SSEEvent = + | { type: "text-delta"; delta: string } + | { type: "tool-input-start"; toolCallId: string; toolName: string } + | { + type: "tool-input-available"; + toolCallId: string; + toolName: string; + input: Record; + } + | { + type: "tool-output-available"; + toolCallId: string; + output: Record; + } + | { type: "data-thinking-step"; data: ThinkingStepData } + | { type: "data-thread-title-update"; data: { threadId: number; title: string } } + | { type: "data-interrupt-request"; data: Record } + | { type: "error"; errorText: string }; + /** * Async generator that reads an SSE stream and yields parsed JSON objects. * Handles buffering, event splitting, and skips malformed JSON / [DONE] lines. */ -export async function* readSSEStream(response: Response): AsyncGenerator { +export async function* readSSEStream(response: Response): AsyncGenerator { if (!response.body) { throw new Error("No response body"); } diff --git a/surfsense_web/lib/connectors/utils.ts b/surfsense_web/lib/connectors/utils.ts index 27da40cc3..623a7b862 100644 --- a/surfsense_web/lib/connectors/utils.ts +++ b/surfsense_web/lib/connectors/utils.ts @@ -8,6 +8,7 @@ export const getConnectorTypeDisplay = (type: string): string => { BAIDU_SEARCH_API: "Baidu Search", SLACK_CONNECTOR: "Slack", TEAMS_CONNECTOR: "Microsoft Teams", + ONEDRIVE_CONNECTOR: "OneDrive", NOTION_CONNECTOR: "Notion", GITHUB_CONNECTOR: "GitHub", LINEAR_CONNECTOR: "Linear",