UNPKG

@taazkareem/clickup-mcp-server

Version:

ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol

136 lines (135 loc) 4.9 kB
/** * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com> * SPDX-License-Identifier: MIT * * Resolver Utility Functions * * This module provides utilities for resolving entity IDs from names or other identifiers. */ import { clickUpServices } from '../services/shared.js'; import { findListIDByName } from '../tools/list.js'; /** * Check if a name matches another name using a variety of matching strategies * Returns a structured result with match quality information rather than just a boolean * * @param actualName The actual name to check * @param searchName The name being searched for * @returns A structured result with match details */ export function isNameMatch(actualName, searchName) { if (!actualName || !searchName) { return { isMatch: false, score: 0, exactMatch: false, reason: 'One of the names is empty' }; } // Remove any extra whitespace const normalizedActualName = actualName.trim(); const normalizedSearchName = searchName.trim(); // Handle empty names after normalization if (normalizedActualName === '') { return { isMatch: false, score: 0, exactMatch: false, reason: 'Actual name is empty' }; } if (normalizedSearchName === '') { return { isMatch: false, score: 0, exactMatch: false, reason: 'Search name is empty' }; } // 1. Exact match (highest quality) if (normalizedActualName === normalizedSearchName) { return { isMatch: true, score: 100, exactMatch: true, reason: 'Exact match' }; } // 2. Case-insensitive exact match (high quality) if (normalizedActualName.toLowerCase() === normalizedSearchName.toLowerCase()) { return { isMatch: true, score: 90, exactMatch: true, reason: 'Case-insensitive exact match' }; } // 3. Match after removing emojis (moderate quality) const actualNameWithoutEmoji = normalizedActualName.replace(/[\p{Emoji}\u{FE00}-\u{FE0F}\u200d]+/gu, '').trim(); const searchNameWithoutEmoji = normalizedSearchName.replace(/[\p{Emoji}\u{FE00}-\u{FE0F}\u200d]+/gu, '').trim(); if (actualNameWithoutEmoji === searchNameWithoutEmoji) { return { isMatch: true, score: 80, exactMatch: false, reason: 'Exact match after removing emojis' }; } if (actualNameWithoutEmoji.toLowerCase() === searchNameWithoutEmoji.toLowerCase()) { return { isMatch: true, score: 70, exactMatch: false, reason: 'Case-insensitive match after removing emojis' }; } // 4. Substring matches (lower quality) const lowerActual = normalizedActualName.toLowerCase(); const lowerSearch = normalizedSearchName.toLowerCase(); // Full substring (term completely contained) if (lowerActual.includes(lowerSearch)) { return { isMatch: true, score: 60, exactMatch: false, reason: 'Search term found as substring in actual name' }; } if (lowerSearch.includes(lowerActual)) { return { isMatch: true, score: 50, exactMatch: false, reason: 'Actual name found as substring in search term' }; } // 5. Fuzzy emoji-less matches (lowest quality) const lowerActualNoEmoji = actualNameWithoutEmoji.toLowerCase(); const lowerSearchNoEmoji = searchNameWithoutEmoji.toLowerCase(); if (lowerActualNoEmoji.includes(lowerSearchNoEmoji)) { return { isMatch: true, score: 40, exactMatch: false, reason: 'Search term (without emoji) found as substring in actual name' }; } if (lowerSearchNoEmoji.includes(lowerActualNoEmoji)) { return { isMatch: true, score: 30, exactMatch: false, reason: 'Actual name (without emoji) found as substring in search term' }; } // No match found return { isMatch: false, score: 0, exactMatch: false, reason: 'No match found with any matching strategy' }; } /** * Resolve a list ID from either a direct ID or list name */ export async function resolveListId(listId, listName, workspaceService = clickUpServices.workspace) { // If list ID is directly provided, use it if (listId) { return listId; } // If list name is provided, find the corresponding ID if (listName) { const listInfo = await findListIDByName(workspaceService, listName); if (!listInfo) { throw new Error(`List "${listName}" not found`); } return listInfo.id; } // If neither is provided, throw an error throw new Error("Either listId or listName must be provided"); }