UNPKG

@hauptsache.net/clickup-mcp

Version:

Search, create, and retrieve tasks, add comments, and track time through natural language commands.

509 lines (508 loc) 21 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.downloadImages = void 0; exports.isTaskId = isTaskId; exports.getCurrentUser = getCurrentUser; exports.getSpaceDetails = getSpaceDetails; exports.getTaskSearchIndex = getTaskSearchIndex; exports.generateTaskUrl = generateTaskUrl; exports.generateListUrl = generateListUrl; exports.generateSpaceUrl = generateSpaceUrl; exports.generateFolderUrl = generateFolderUrl; exports.generateDocumentUrl = generateDocumentUrl; exports.formatSpaceTree = formatSpaceTree; exports.getSpaceSearchIndex = getSpaceSearchIndex; exports.getSpaceContent = getSpaceContent; exports.getAllTeamMembers = getAllTeamMembers; exports.performMultiTermSearch = performMultiTermSearch; const config_1 = require("./config"); const fuse_js_1 = __importDefault(require("fuse.js")); const GLOBAL_REFRESH_INTERVAL = 60000; // 60 seconds - that is the rate limit time frame /** * Checks if a string looks like a valid ClickUp task ID * Valid task IDs are 6-9 characters long and contain only alphanumeric characters */ function isTaskId(str) { // Task IDs are 6-9 characters long and contain only alphanumeric characters return /^[a-z0-9]{6,9}$/i.test(str); } // Cache for current user info to avoid repeated API calls and race conditions let cachedUserPromise = null; /** * Get current authenticated user information from ClickUp API * Caches the promise to prevent race conditions on concurrent calls */ async function getCurrentUser() { // Return cached promise if available if (cachedUserPromise) { return cachedUserPromise; } // Create the fetch promise const fetchPromise = (async () => { const userResponse = await fetch("https://api.clickup.com/api/v2/user", { headers: { Authorization: config_1.CONFIG.apiKey }, }); if (!userResponse.ok) { throw new Error(`Error fetching user info: ${userResponse.status} ${userResponse.statusText}`); } return await userResponse.json(); })(); // Cache the promise cachedUserPromise = fetchPromise; // Auto-cleanup after 60 seconds setTimeout(() => { cachedUserPromise = null; console.error(`Auto-cleaned user data cache`); }, GLOBAL_REFRESH_INTERVAL); return fetchPromise; } // Re-export image processing functions for backward compatibility var image_processing_1 = require("./image-processing"); Object.defineProperty(exports, "downloadImages", { enumerable: true, get: function () { return image_processing_1.downloadImages; } }); const spaceCache = new Map(); // Global cache for space details promises /** * Function to get space details, using a cache to avoid redundant fetches */ function getSpaceDetails(spaceId) { if (!spaceId) { return Promise.reject(new Error('Invalid space ID')); } const cachedSpace = spaceCache.get(spaceId); if (cachedSpace) { return cachedSpace; } const fetchPromise = fetch(`https://api.clickup.com/api/v2/space/${spaceId}`, { headers: { Authorization: config_1.CONFIG.apiKey } }) .then(res => { if (!res.ok) { throw new Error(`Error fetching space ${spaceId}: ${res.status}`); } return res.json(); }) .catch(error => { console.error(`Network error fetching space ${spaceId}:`, error); throw new Error(`Error fetching space ${spaceId}: ${error}`); }); spaceCache.set(spaceId, fetchPromise); return fetchPromise; } // Task search index management - cache promises to prevent race conditions const taskIndices = new Map(); /** * Get or create a task search index with specified filters * Caches promises to prevent race conditions on concurrent calls */ async function getTaskSearchIndex(space_ids, list_ids, assignees) { // Create cache key from sorted filter arrays const key = JSON.stringify({ space_ids: space_ids?.sort(), list_ids: list_ids?.sort(), assignees: assignees?.sort() }); // Check for existing valid index promise const cachedPromise = taskIndices.get(key); if (cachedPromise) { return cachedPromise; } // Create the fetch promise const fetchPromise = (async () => { console.error(`Refreshing task index for filters: ${key}`); const tasks = await fetchTasks(space_ids, list_ids, assignees); const index = createFuseIndex(tasks); console.error(`Task index created with ${tasks.length} tasks`); return index; })(); // Store promise with auto-cleanup taskIndices.set(key, fetchPromise); setTimeout(() => { taskIndices.delete(key); console.error(`Auto-cleaned index for filters: ${key}`); }, GLOBAL_REFRESH_INTERVAL); return fetchPromise; } /** * Fetch tasks using team endpoint with dynamic filters */ async function fetchTasks(space_ids, list_ids, assignees) { const queryParams = ['order_by=updated', 'subtasks=true']; // Add filter parameters if (space_ids?.length) { space_ids.forEach(id => queryParams.push(`space_ids[]=${id}`)); } if (list_ids?.length) { list_ids.forEach(id => queryParams.push(`list_ids[]=${id}`)); } if (assignees?.length) { assignees.forEach(id => queryParams.push(`assignees[]=${id}`)); } const queryString = queryParams.join('&'); // Fetch multiple pages in parallel const maxPages = space_ids?.length || list_ids?.length || assignees?.length ? 10 : 30; // Fewer pages for filtered searches const taskListsPromises = [...Array(maxPages)].map(async (_, i) => { const url = `https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/task?${queryString}&page=${i}`; try { const res = await fetch(url, { headers: { Authorization: config_1.CONFIG.apiKey } }); return await res.json(); } catch (e) { console.error(`Error fetching page ${i}:`, e); return { tasks: [] }; } }); const taskLists = await Promise.all(taskListsPromises); return taskLists.flatMap(taskList => taskList.tasks || []); } /** * Create a Fuse index from tasks array */ function createFuseIndex(tasks) { return new fuse_js_1.default(tasks, { keys: [ { name: 'name', weight: 0.7 }, { name: 'id', weight: 0.6 }, { name: 'text_content', weight: 0.5 }, { name: 'tags.name', weight: 0.4 }, { name: 'assignees.username', weight: 0.4 }, { name: 'list.name', weight: 0.3 }, { name: 'folder.name', weight: 0.2 }, { name: 'space.name', weight: 0.1 } ], findAllMatches: true, includeScore: true, minMatchCharLength: 2, threshold: 0.4, }); } // ===== LINK UTILITIES ===== /** * Generate a ClickUp task URL from a task ID */ function generateTaskUrl(taskId) { return `https://app.clickup.com/t/${taskId}`; } /** * Generate a ClickUp list URL from a list ID */ function generateListUrl(listId) { return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/li/${listId}`; } /** * Generate a ClickUp space URL from a space ID */ function generateSpaceUrl(spaceId) { return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/s/${spaceId}`; } /** * Generate a ClickUp folder URL from a folder ID */ function generateFolderUrl(folderId) { return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/f/${folderId}`; } /** * Generate a ClickUp document URL from a document ID and optional page ID */ function generateDocumentUrl(docId, pageId) { if (pageId) { return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/dc/${docId}/${pageId}`; } return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/dc/${docId}`; } /** * Format space content as tree structure * Shared function used by both searchSpaces tool and space resources */ function formatSpaceTree(space, lists, folders, documents) { const spaceLines = []; const totalLists = lists.length + folders.reduce((sum, f) => sum + (f.lists?.length || 0), 0); // Space header spaceLines.push(`🏢 SPACE: ${space.name} (space_id: ${space.id}${space.private ? ', private' : ''}${space.archived ? ', archived' : ''}) ${generateSpaceUrl(space.id)}`, ` ${totalLists} lists, ${folders.length} folders, ${documents.length} documents`); // Create a tree structure const hasDirectLists = lists.length > 0; const hasFolders = folders.length > 0; const hasDocuments = documents.length > 0; // Direct lists (not in folders) if (hasDirectLists) { lists.forEach((list, listIndex) => { const isLastDirectList = listIndex === lists.length - 1; const isLastOverall = !hasFolders && !hasDocuments && isLastDirectList; const treeChar = isLastOverall ? '└──' : '├──'; const extraInfo = [ ...(list.task_count ? [`${list.task_count} tasks`] : []), ...(list.private ? ['private'] : []), ...(list.archived ? ['archived'] : []) ].join(', '); const listLine = `${treeChar} 📝 ${list.name} (list_id: ${list.id}${extraInfo ? `, ${extraInfo}` : ''}) ${generateListUrl(list.id)}`; spaceLines.push(listLine); }); } // Folders and their lists if (hasFolders) { folders.forEach((folder, folderIndex) => { const isLastFolder = folderIndex === folders.length - 1; const isLastOverall = !hasDocuments && isLastFolder; const folderTreeChar = isLastOverall ? '└──' : '├──'; const folderContinuation = isLastOverall ? ' ' : '│ '; const folderExtraInfo = [ ...(folder.lists?.length ? [`${folder.lists.length} lists`] : []), ...(folder.private ? ['private'] : []), ...(folder.archived ? ['archived'] : []) ].join(', '); const folderLine = `${folderTreeChar} 📂 ${folder.name} (folder_id: ${folder.id}${folderExtraInfo ? `, ${folderExtraInfo}` : ''}) ${generateFolderUrl(folder.id)}`; spaceLines.push(folderLine); // Lists within this folder if (folder.lists && folder.lists.length > 0) { folder.lists.forEach((list, listIndex) => { const isLastListInFolder = listIndex === folder.lists.length - 1; const listTreeChar = isLastListInFolder ? '└──' : '├──'; const listExtraInfo = [ ...(list.task_count ? [`${list.task_count} tasks`] : []), ...(list.private ? ['private'] : []), ...(list.archived ? ['archived'] : []) ].join(', '); const listLine = `${folderContinuation}${listTreeChar} 📝 ${list.name} (list_id: ${list.id}${listExtraInfo ? `, ${listExtraInfo}` : ''}) ${generateListUrl(list.id)}`; spaceLines.push(listLine); }); } }); } // Documents attached to this space if (hasDocuments) { documents.forEach((document, docIndex) => { const isLastDocument = docIndex === documents.length - 1; const docTreeChar = isLastDocument ? '└──' : '├──'; const docLine = `${docTreeChar} 📄 ${document.name} (doc_id: ${document.id}) ${generateDocumentUrl(document.id)}`; spaceLines.push(docLine); }); } return spaceLines.join('\n'); } // Space search index cache - cache promise to prevent race conditions let spaceSearchIndexPromise = null; /** * Get or refresh the space search index * Caches promise to prevent race conditions on concurrent calls */ async function getSpaceSearchIndex() { // Return cached promise if available if (spaceSearchIndexPromise) { return spaceSearchIndexPromise; } // Create the fetch promise const fetchPromise = (async () => { try { const url = `https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/space`; const response = await fetch(url, { headers: { Authorization: config_1.CONFIG.apiKey }, }); if (!response.ok) { throw new Error(`Error fetching spaces: ${response.status} ${response.statusText}`); } const data = await response.json(); const spacesData = data.spaces || []; // Create Fuse search index return new fuse_js_1.default(spacesData, { keys: [ { name: 'name', weight: 0.7 }, { name: 'id', weight: 0.6 } ], includeScore: true, threshold: 0.4, minMatchCharLength: 1, }); } catch (error) { console.error('Error creating space search index:', error); return null; } })(); // Cache the promise spaceSearchIndexPromise = fetchPromise; // Auto-cleanup after 60 seconds setTimeout(() => { spaceSearchIndexPromise = null; console.error('Auto-cleaned space search index'); }, GLOBAL_REFRESH_INTERVAL); return fetchPromise; } const listCache = new Map(); // Cache for space lists/folders /** * Get lists, folders, and documents for a specific space with caching */ async function getSpaceContent(spaceId) { const cacheKey = `space-content-${spaceId}`; // Check cache first const cachedContent = listCache.get(cacheKey); if (cachedContent) { return cachedContent; } // Fetch content with parallel requests const fetchPromise = (async () => { try { const [folders, lists, documents] = await Promise.all([ fetch(`https://api.clickup.com/api/v2/space/${spaceId}/folder`, { headers: { Authorization: config_1.CONFIG.apiKey }, }) .then(response => response.json()) .then(json => json.folders || []) .catch(e => { console.error(e); return []; }), fetch(`https://api.clickup.com/api/v2/space/${spaceId}/list`, { headers: { Authorization: config_1.CONFIG.apiKey }, }) .then(response => response.json()) .then(json => json.lists || []) .catch(e => { console.error(e); return []; }), fetch(`https://api.clickup.com/api/v3/workspaces/${config_1.CONFIG.teamId}/docs?parent_id=${spaceId}`, { headers: { Authorization: config_1.CONFIG.apiKey }, }) .then(response => response.json()) .then(json => json.docs || []) .catch(e => { console.error(e); return []; }) ]); // For each folder, also fetch its lists const folderListPromises = folders.map(async (folder) => { try { const folderListResponse = await fetch(`https://api.clickup.com/api/v2/folder/${folder.id}/list`, { headers: { Authorization: config_1.CONFIG.apiKey } }); if (folderListResponse.ok) { const folderListData = await folderListResponse.json(); folder.lists = folderListData.lists || []; } return folder; } catch (error) { console.error(`Error fetching lists for folder ${folder.id}:`, error); folder.lists = []; return folder; } }); const foldersWithLists = await Promise.all(folderListPromises); return { lists, folders: foldersWithLists, documents }; } catch (error) { console.error(`Error fetching space content for ${spaceId}:`, error); return { lists: [], folders: [], documents: [] }; } })(); // Cache the promise listCache.set(cacheKey, fetchPromise); // Auto-cleanup after 60 seconds setTimeout(() => { listCache.delete(cacheKey); console.error(`Auto-cleaned space content cache for ${spaceId}`); }, GLOBAL_REFRESH_INTERVAL); return fetchPromise; } // Cache for team members to avoid repeated API calls and race conditions let cachedTeamMembersPromise = null; /** * Gets all team members from ClickUp API with caching */ async function getAllTeamMembers() { // Return cached promise if available if (cachedTeamMembersPromise) { return cachedTeamMembersPromise; } // Create the fetch promise const fetchPromise = (async () => { try { const response = await fetch(`https://api.clickup.com/api/v2/team`, { headers: { Authorization: config_1.CONFIG.apiKey }, }); if (!response.ok) { console.error(`Error fetching teams: ${response.status} ${response.statusText}`); return []; } const data = await response.json(); if (!data.teams || !Array.isArray(data.teams)) { return []; } // Find the team that matches our configured team ID and extract all user IDs const currentTeam = data.teams.find((team) => team.id === config_1.CONFIG.teamId); if (!currentTeam || !currentTeam.members || !Array.isArray(currentTeam.members)) { console.error(`Team ${config_1.CONFIG.teamId} not found or has no members`); return []; } return currentTeam.members.map((member) => member.user?.id).filter(Boolean); } catch (error) { console.error('Error fetching team members:', error); return []; } })(); // Cache the promise cachedTeamMembersPromise = fetchPromise; // Auto-cleanup after 60 seconds setTimeout(() => { cachedTeamMembersPromise = null; console.error(`Auto-cleaned team members cache`); }, GLOBAL_REFRESH_INTERVAL); return fetchPromise; } /** * Performs multi-term search with aggressive boosting for items matching multiple terms * @param searchIndex Fuse search index to search within * @param terms Array of search terms * @returns Array of items sorted by relevance (multi-term matches ranked higher) */ async function performMultiTermSearch(searchIndex, terms) { // Filter valid search terms const validTerms = terms.filter(term => term && term.trim().length > 0); if (validTerms.length === 0) { return []; } // Track multiple matches per item for aggressive boosting const itemMatches = new Map(); // Collect all matches for each term validTerms.forEach(term => { const results = searchIndex.search(term); results.forEach(result => { if (result.item && typeof result.item.id === 'string') { const itemId = result.item.id; const currentScore = result.score ?? 1; const existing = itemMatches.get(itemId); if (!existing) { itemMatches.set(itemId, { item: result.item, scores: [currentScore], matchedTerms: [term] }); } else { existing.scores.push(currentScore); existing.matchedTerms.push(term); } } }); }); // Calculate aggressively boosted scores for multi-term matches const uniqueResults = new Map(); itemMatches.forEach((match, itemId) => { const bestScore = Math.min(...match.scores); const matchCount = match.scores.length; const totalTerms = validTerms.length; // Aggressive multi-term boost: exponential improvement for multiple matches // 1 match: base score // 2+ matches: exponentially better score based on match ratio const matchRatio = matchCount / totalTerms; const boostFactor = Math.pow(0.1, matchRatio * 4); // Very aggressive boost const finalScore = bestScore * boostFactor; uniqueResults.set(itemId, { item: match.item, score: finalScore }); }); // Return sorted results (best scores first) return Array.from(uniqueResults.values()) .sort((a, b) => a.score - b.score) .map(entry => entry.item); }