@taazkareem/clickup-mcp-server 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/build/server.js +3 -6
- package/build/services/clickup/base.js +156 -45
- package/build/services/clickup/bulk.js +10 -22
- package/build/services/clickup/tag.js +51 -2
- package/build/services/clickup/task/task-core.js +76 -11
- package/build/services/clickup/task/task-search.js +378 -25
- package/build/services/clickup/workspace.js +14 -12
- package/build/tools/folder.js +5 -53
- package/build/tools/list.js +5 -66
- package/build/tools/tag.js +92 -76
- package/build/tools/task/attachments.js +16 -29
- package/build/tools/task/bulk-operations.js +7 -52
- package/build/tools/task/handlers.js +253 -136
- package/build/tools/task/main.js +9 -33
- package/build/tools/task/single-operations.js +15 -106
- package/build/tools/task/utilities.js +59 -12
- package/build/tools/task/workspace-operations.js +15 -8
- package/build/tools/workspace.js +1 -12
- package/build/utils/date-utils.js +7 -4
- package/build/utils/resolver-utils.js +102 -29
- package/package.json +1 -1
|
@@ -48,20 +48,7 @@ const handleOperationError = (operation, error) => {
|
|
|
48
48
|
*/
|
|
49
49
|
export const createTaskTool = {
|
|
50
50
|
name: "create_task",
|
|
51
|
-
description: `
|
|
52
|
-
|
|
53
|
-
Valid Usage:
|
|
54
|
-
1. Provide listId (preferred if available)
|
|
55
|
-
2. Provide listName (will look up the list ID)
|
|
56
|
-
|
|
57
|
-
Requirements:
|
|
58
|
-
- name: REQUIRED
|
|
59
|
-
- List identification: EITHER listId OR listName REQUIRED
|
|
60
|
-
|
|
61
|
-
Notes:
|
|
62
|
-
- Use create_bulk_tasks for multiple tasks
|
|
63
|
-
- Set parent parameter to create a subtask
|
|
64
|
-
- Custom fields can be set via custom_fields parameter`,
|
|
51
|
+
description: `Creates a single task in a ClickUp list. Use listId (preferred) or listName. Required: name + list info. For multiple tasks use create_bulk_tasks. Can create subtasks via parent param. Supports custom fields as array of {id, value}.`,
|
|
65
52
|
inputSchema: {
|
|
66
53
|
type: "object",
|
|
67
54
|
properties: {
|
|
@@ -141,20 +128,7 @@ Notes:
|
|
|
141
128
|
*/
|
|
142
129
|
export const updateTaskTool = {
|
|
143
130
|
name: "update_task",
|
|
144
|
-
description: `
|
|
145
|
-
|
|
146
|
-
Valid Usage:
|
|
147
|
-
1. Use taskId (preferred) - works with both regular and custom IDs
|
|
148
|
-
2. Use taskName + listName for targeted search
|
|
149
|
-
|
|
150
|
-
Requirements:
|
|
151
|
-
- Task identification: EITHER taskId OR taskName REQUIRED
|
|
152
|
-
- Updates: At least one update field (name, description, status, priority, dueDate) REQUIRED
|
|
153
|
-
|
|
154
|
-
Notes:
|
|
155
|
-
- Only specified fields will be updated
|
|
156
|
-
- Custom fields can be set using the custom_fields parameter
|
|
157
|
-
- Using taskName without listName may match multiple tasks`,
|
|
131
|
+
description: `Updates task properties. Use taskId (preferred) or taskName + optional listName. At least one update field required. Custom fields supported as array of {id, value}. WARNING: Using taskName without listName may match multiple tasks.`,
|
|
158
132
|
inputSchema: {
|
|
159
133
|
type: "object",
|
|
160
134
|
properties: {
|
|
@@ -224,18 +198,7 @@ Notes:
|
|
|
224
198
|
*/
|
|
225
199
|
export const moveTaskTool = {
|
|
226
200
|
name: "move_task",
|
|
227
|
-
description: `
|
|
228
|
-
|
|
229
|
-
Valid Usage:
|
|
230
|
-
1. Use taskId + destination list (preferred)
|
|
231
|
-
2. Use taskName + sourceListName + destination list
|
|
232
|
-
|
|
233
|
-
Requirements:
|
|
234
|
-
- Task identification: EITHER taskId OR (taskName + sourceListName) REQUIRED
|
|
235
|
-
- Destination: EITHER listId OR listName REQUIRED
|
|
236
|
-
|
|
237
|
-
Warning:
|
|
238
|
-
- Task statuses may reset if destination list has different status options`,
|
|
201
|
+
description: `Moves task to different list. Use taskId + (listId/listName) preferred, or taskName + sourceListName + (listId/listName). WARNING: Task statuses may reset if destination list has different status options.`,
|
|
239
202
|
inputSchema: {
|
|
240
203
|
type: "object",
|
|
241
204
|
properties: {
|
|
@@ -268,18 +231,7 @@ Warning:
|
|
|
268
231
|
*/
|
|
269
232
|
export const duplicateTaskTool = {
|
|
270
233
|
name: "duplicate_task",
|
|
271
|
-
description: `
|
|
272
|
-
|
|
273
|
-
Valid Usage:
|
|
274
|
-
1. Use taskId (preferred)
|
|
275
|
-
2. Use taskName + sourceListName
|
|
276
|
-
|
|
277
|
-
Requirements:
|
|
278
|
-
- Task identification: EITHER taskId OR (taskName + sourceListName) REQUIRED
|
|
279
|
-
- Destination: OPTIONAL - defaults to original list
|
|
280
|
-
|
|
281
|
-
Notes:
|
|
282
|
-
- The duplicate preserves the original task's properties`,
|
|
234
|
+
description: `Creates copy of task in same/different list. Use taskId + optional (listId/listName), or taskName + sourceListName + optional (listId/listName). Preserves original properties. Default: same list as original.`,
|
|
283
235
|
inputSchema: {
|
|
284
236
|
type: "object",
|
|
285
237
|
properties: {
|
|
@@ -308,22 +260,11 @@ Notes:
|
|
|
308
260
|
}
|
|
309
261
|
};
|
|
310
262
|
/**
|
|
311
|
-
* Tool definition for retrieving
|
|
263
|
+
* Tool definition for retrieving task details
|
|
312
264
|
*/
|
|
313
265
|
export const getTaskTool = {
|
|
314
266
|
name: "get_task",
|
|
315
|
-
description: `
|
|
316
|
-
|
|
317
|
-
Valid Usage:
|
|
318
|
-
1. Use taskId (preferred) - works with both regular and custom IDs
|
|
319
|
-
2. Use taskName + listName for targeted search
|
|
320
|
-
|
|
321
|
-
Requirements:
|
|
322
|
-
- Task identification: EITHER taskId OR (taskName + listName) REQUIRED
|
|
323
|
-
|
|
324
|
-
Notes:
|
|
325
|
-
- Task names are only unique within a list
|
|
326
|
-
- Set subtasks=true to include all subtasks in the response`,
|
|
267
|
+
description: `Gets task details by taskId (works with regular/custom IDs) or taskName. For taskName search, provide listName for faster lookup. Set subtasks=true to include all subtask details.`,
|
|
327
268
|
inputSchema: {
|
|
328
269
|
type: "object",
|
|
329
270
|
properties: {
|
|
@@ -333,11 +274,11 @@ Notes:
|
|
|
333
274
|
},
|
|
334
275
|
taskName: {
|
|
335
276
|
type: "string",
|
|
336
|
-
description: "Name of task to retrieve.
|
|
277
|
+
description: "Name of task to retrieve. Can be used alone for a global search, or with listName for faster lookup."
|
|
337
278
|
},
|
|
338
279
|
listName: {
|
|
339
280
|
type: "string",
|
|
340
|
-
description: "Name of list containing the task.
|
|
281
|
+
description: "Name of list containing the task. Optional but recommended when using taskName."
|
|
341
282
|
},
|
|
342
283
|
customTaskId: {
|
|
343
284
|
type: "string",
|
|
@@ -362,11 +303,12 @@ Valid Usage:
|
|
|
362
303
|
2. Use listName
|
|
363
304
|
|
|
364
305
|
Requirements:
|
|
365
|
-
-
|
|
306
|
+
- EITHER listId OR listName is REQUIRED
|
|
366
307
|
|
|
367
308
|
Notes:
|
|
368
|
-
- Use filters (archived, statuses) to narrow down results
|
|
369
|
-
- Pagination
|
|
309
|
+
- Use filters (archived, statuses, etc.) to narrow down results
|
|
310
|
+
- Pagination available through page parameter
|
|
311
|
+
- Sorting available through order_by and reverse parameters`,
|
|
370
312
|
inputSchema: {
|
|
371
313
|
type: "object",
|
|
372
314
|
properties: {
|
|
@@ -412,18 +354,7 @@ Notes:
|
|
|
412
354
|
*/
|
|
413
355
|
export const getTaskCommentsTool = {
|
|
414
356
|
name: "get_task_comments",
|
|
415
|
-
description: `
|
|
416
|
-
|
|
417
|
-
Valid Usage:
|
|
418
|
-
1. Use taskId (preferred)
|
|
419
|
-
2. Use taskName + listName for targeted search
|
|
420
|
-
|
|
421
|
-
Requirements:
|
|
422
|
-
- Task identification: EITHER taskId OR taskName REQUIRED
|
|
423
|
-
|
|
424
|
-
Notes:
|
|
425
|
-
- Task names may not be unique across different lists
|
|
426
|
-
- Use start/startId parameters for pagination`,
|
|
357
|
+
description: `Gets task comments. Use taskId (preferred) or taskName + optional listName. Use start/startId params for pagination. Task names may not be unique across lists.`,
|
|
427
358
|
inputSchema: {
|
|
428
359
|
type: "object",
|
|
429
360
|
properties: {
|
|
@@ -455,18 +386,7 @@ Notes:
|
|
|
455
386
|
*/
|
|
456
387
|
export const createTaskCommentTool = {
|
|
457
388
|
name: "create_task_comment",
|
|
458
|
-
description: `
|
|
459
|
-
|
|
460
|
-
Valid Usage:
|
|
461
|
-
1. Use taskId (preferred)
|
|
462
|
-
2. Use taskName + listName
|
|
463
|
-
|
|
464
|
-
Requirements:
|
|
465
|
-
- Task identification: EITHER taskId OR (taskName + listName) REQUIRED
|
|
466
|
-
- commentText: REQUIRED
|
|
467
|
-
|
|
468
|
-
Notes:
|
|
469
|
-
- Set notifyAll=true to notify all task assignees`,
|
|
389
|
+
description: `Creates task comment. Use taskId (preferred) or taskName + listName. Required: commentText. Optional: notifyAll to notify assignees, assignee to assign comment.`,
|
|
470
390
|
inputSchema: {
|
|
471
391
|
type: "object",
|
|
472
392
|
properties: {
|
|
@@ -503,18 +423,7 @@ Notes:
|
|
|
503
423
|
*/
|
|
504
424
|
export const deleteTaskTool = {
|
|
505
425
|
name: "delete_task",
|
|
506
|
-
description: `
|
|
507
|
-
|
|
508
|
-
Valid Usage:
|
|
509
|
-
1. Use taskId (preferred and safest)
|
|
510
|
-
2. Use taskName + listName for targeted search
|
|
511
|
-
|
|
512
|
-
Requirements:
|
|
513
|
-
- Task identification: EITHER taskId OR taskName REQUIRED
|
|
514
|
-
|
|
515
|
-
Warning:
|
|
516
|
-
- This action CANNOT be undone
|
|
517
|
-
- Using taskName without listName may match multiple tasks`,
|
|
426
|
+
description: `PERMANENTLY deletes task. Use taskId (preferred/safest) or taskName + optional listName. WARNING: Cannot be undone. Using taskName without listName may match multiple tasks.`,
|
|
518
427
|
inputSchema: {
|
|
519
428
|
type: "object",
|
|
520
429
|
properties: {
|
|
@@ -11,7 +11,7 @@ import { formatDueDate } from '../utils.js';
|
|
|
11
11
|
import { clickUpServices } from '../../services/shared.js';
|
|
12
12
|
import { findListIDByName } from '../../tools/list.js';
|
|
13
13
|
// Use shared services instance for ID resolution
|
|
14
|
-
const { workspace: workspaceService } = clickUpServices;
|
|
14
|
+
const { workspace: workspaceService, task: taskService } = clickUpServices;
|
|
15
15
|
//=============================================================================
|
|
16
16
|
// DATA FORMATTING UTILITIES
|
|
17
17
|
//=============================================================================
|
|
@@ -65,22 +65,38 @@ export function formatTaskData(task, additional = {}) {
|
|
|
65
65
|
...additional
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
-
//=============================================================================
|
|
69
|
-
// VALIDATION UTILITIES
|
|
70
|
-
//=============================================================================
|
|
71
68
|
/**
|
|
72
69
|
* Validates task identification parameters
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
*
|
|
71
|
+
* @param params - Task identification parameters
|
|
72
|
+
* @param options - Validation options
|
|
73
|
+
* @returns Validation result with error message if any
|
|
75
74
|
*/
|
|
76
|
-
export function validateTaskIdentification(
|
|
75
|
+
export function validateTaskIdentification(params, options = {}) {
|
|
76
|
+
const { taskId, taskName, customTaskId, listName } = params;
|
|
77
|
+
const { requireTaskId = false, useGlobalLookup = true } = options;
|
|
78
|
+
// If taskId is required, it must be provided
|
|
79
|
+
if (requireTaskId && !taskId) {
|
|
80
|
+
return {
|
|
81
|
+
isValid: false,
|
|
82
|
+
errorMessage: 'Task ID is required for this operation'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// At least one identification method must be provided
|
|
77
86
|
if (!taskId && !taskName && !customTaskId) {
|
|
78
|
-
|
|
87
|
+
return {
|
|
88
|
+
isValid: false,
|
|
89
|
+
errorMessage: 'Either taskId, taskName, or customTaskId must be provided to identify the task'
|
|
90
|
+
};
|
|
79
91
|
}
|
|
80
|
-
// When
|
|
81
|
-
if (
|
|
82
|
-
|
|
92
|
+
// When using taskName without global lookup, listName is required
|
|
93
|
+
if (taskName && !taskId && !customTaskId && !useGlobalLookup && !listName) {
|
|
94
|
+
return {
|
|
95
|
+
isValid: false,
|
|
96
|
+
errorMessage: 'When identifying a task by name, you must also provide the listName parameter'
|
|
97
|
+
};
|
|
83
98
|
}
|
|
99
|
+
return { isValid: true };
|
|
84
100
|
}
|
|
85
101
|
/**
|
|
86
102
|
* Validates list identification parameters
|
|
@@ -99,7 +115,7 @@ export function validateTaskUpdateData(updateData) {
|
|
|
99
115
|
// Check if there are any valid update fields present
|
|
100
116
|
const hasUpdates = Object.keys(updateData).some(key => {
|
|
101
117
|
return ['name', 'description', 'markdown_description', 'status', 'priority',
|
|
102
|
-
'dueDate', 'startDate', '
|
|
118
|
+
'dueDate', 'startDate', 'custom_fields'].includes(key);
|
|
103
119
|
});
|
|
104
120
|
if (!hasUpdates) {
|
|
105
121
|
throw new Error("At least one field to update must be provided");
|
|
@@ -231,3 +247,34 @@ export function extractTreePath(root, targetId) {
|
|
|
231
247
|
// Not found in this branch
|
|
232
248
|
return [];
|
|
233
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Get task ID from various identification methods
|
|
252
|
+
*/
|
|
253
|
+
export async function getTaskId(taskId, taskName, listName, customTaskId, requireId = false) {
|
|
254
|
+
// Validate task identification
|
|
255
|
+
const validationResult = validateTaskIdentification({ taskId, taskName, listName, customTaskId }, { requireTaskId: requireId, useGlobalLookup: true });
|
|
256
|
+
if (!validationResult.isValid) {
|
|
257
|
+
throw new Error(validationResult.errorMessage);
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const result = await taskService.findTasks({
|
|
261
|
+
taskId,
|
|
262
|
+
customTaskId,
|
|
263
|
+
taskName,
|
|
264
|
+
listName,
|
|
265
|
+
allowMultipleMatches: false,
|
|
266
|
+
useSmartDisambiguation: true,
|
|
267
|
+
includeFullDetails: false
|
|
268
|
+
});
|
|
269
|
+
if (!result || Array.isArray(result)) {
|
|
270
|
+
throw new Error(`Task not found with the provided identification`);
|
|
271
|
+
}
|
|
272
|
+
return result.id;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (error.message.includes('Multiple tasks found')) {
|
|
276
|
+
throw new Error(`Multiple tasks found with name "${taskName}". Please provide list name to disambiguate.`);
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -12,20 +12,27 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export const getWorkspaceTasksTool = {
|
|
14
14
|
name: "get_workspace_tasks",
|
|
15
|
-
description: `Purpose: Retrieve tasks from across the entire workspace with filtering.
|
|
15
|
+
description: `Purpose: Retrieve tasks from across the entire workspace with powerful filtering options, including tag-based filtering.
|
|
16
16
|
|
|
17
17
|
Valid Usage:
|
|
18
|
-
1. Apply any combination of filters (tags, lists, statuses, etc.)
|
|
19
|
-
2. Use pagination
|
|
18
|
+
1. Apply any combination of filters (tags, lists, folders, spaces, statuses, etc.)
|
|
19
|
+
2. Use pagination to manage large result sets
|
|
20
20
|
|
|
21
21
|
Requirements:
|
|
22
|
-
- At least one filter parameter REQUIRED (tags, list_ids, statuses,
|
|
22
|
+
- At least one filter parameter is REQUIRED (tags, list_ids, folder_ids, space_ids, statuses, assignees, or date filters)
|
|
23
|
+
- Pagination parameters (page, order_by, reverse) alone are not considered filters
|
|
23
24
|
|
|
24
25
|
Notes:
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
26
|
+
- Provides workspace-wide task access (unlike get_tasks which only searches in one list)
|
|
27
|
+
- Returns complete task details including descriptions, assignees, custom fields, and all metadata
|
|
28
|
+
- Tag filtering is especially useful for cross-list organization (e.g., "project-x", "blocker", "needs-review")
|
|
29
|
+
- Combine multiple filters to narrow down your search scope
|
|
30
|
+
- Use pagination for large result sets
|
|
31
|
+
- Use the detail_level parameter to control the amount of data returned:
|
|
32
|
+
- "summary": Returns lightweight task data (name, status, list, tags)
|
|
33
|
+
- "detailed": Returns complete task data with all fields (DEFAULT if not specified)
|
|
34
|
+
- Responses exceeding 50,000 tokens automatically switch to summary format to avoid hitting LLM token limits
|
|
35
|
+
`,
|
|
29
36
|
parameters: {
|
|
30
37
|
type: 'object',
|
|
31
38
|
properties: {
|
package/build/tools/workspace.js
CHANGED
|
@@ -19,18 +19,7 @@ const { workspace: workspaceService } = clickUpServices;
|
|
|
19
19
|
*/
|
|
20
20
|
export const workspaceHierarchyTool = {
|
|
21
21
|
name: 'get_workspace_hierarchy',
|
|
22
|
-
description: `
|
|
23
|
-
|
|
24
|
-
Valid Usage:
|
|
25
|
-
1. Call without parameters to get the full hierarchy
|
|
26
|
-
|
|
27
|
-
Requirements:
|
|
28
|
-
- No parameters required
|
|
29
|
-
|
|
30
|
-
Notes:
|
|
31
|
-
- Returns a tree structure showing all spaces, folders, and lists
|
|
32
|
-
- Each item includes its name and ID
|
|
33
|
-
- Use this to navigate the workspace and understand its organization`,
|
|
22
|
+
description: `Gets complete workspace hierarchy (spaces, folders, lists). No parameters needed. Returns tree structure with names and IDs for navigation.`,
|
|
34
23
|
inputSchema: {
|
|
35
24
|
type: 'object',
|
|
36
25
|
properties: {}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* This module provides utilities for handling dates, timestamps, and due date parsing.
|
|
8
8
|
*/
|
|
9
|
+
import { Logger } from '../logger.js';
|
|
10
|
+
// Create a logger instance for date utilities
|
|
11
|
+
const logger = new Logger('DateUtils');
|
|
9
12
|
/**
|
|
10
13
|
* Get a timestamp for a relative time
|
|
11
14
|
*
|
|
@@ -197,8 +200,8 @@ export function parseDueDate(dateString) {
|
|
|
197
200
|
return undefined;
|
|
198
201
|
}
|
|
199
202
|
catch (error) {
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
logger.warn(`Failed to parse due date: ${dateString}`, error);
|
|
204
|
+
throw new Error(`Invalid date format: ${dateString}`);
|
|
202
205
|
}
|
|
203
206
|
}
|
|
204
207
|
/**
|
|
@@ -225,8 +228,8 @@ export function formatDueDate(timestamp) {
|
|
|
225
228
|
}).replace(' at', ',');
|
|
226
229
|
}
|
|
227
230
|
catch (error) {
|
|
228
|
-
|
|
229
|
-
|
|
231
|
+
logger.warn(`Failed to format due date: ${timestamp}`, error);
|
|
232
|
+
throw new Error(`Invalid timestamp: ${timestamp}`);
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
/**
|
|
@@ -9,37 +9,110 @@
|
|
|
9
9
|
import { clickUpServices } from '../services/shared.js';
|
|
10
10
|
import { findListIDByName } from '../tools/list.js';
|
|
11
11
|
/**
|
|
12
|
-
* Check if a
|
|
12
|
+
* Check if a name matches another name using a variety of matching strategies
|
|
13
|
+
* Returns a structured result with match quality information rather than just a boolean
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* @param actualName The actual name to check
|
|
16
|
+
* @param searchName The name being searched for
|
|
17
|
+
* @returns A structured result with match details
|
|
16
18
|
*/
|
|
17
|
-
export function isNameMatch(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
19
|
+
export function isNameMatch(actualName, searchName) {
|
|
20
|
+
if (!actualName || !searchName) {
|
|
21
|
+
return { isMatch: false, score: 0, exactMatch: false, reason: 'One of the names is empty' };
|
|
22
|
+
}
|
|
23
|
+
// Remove any extra whitespace
|
|
24
|
+
const normalizedActualName = actualName.trim();
|
|
25
|
+
const normalizedSearchName = searchName.trim();
|
|
26
|
+
// Handle empty names after normalization
|
|
27
|
+
if (normalizedActualName === '') {
|
|
28
|
+
return { isMatch: false, score: 0, exactMatch: false, reason: 'Actual name is empty' };
|
|
29
|
+
}
|
|
30
|
+
if (normalizedSearchName === '') {
|
|
31
|
+
return { isMatch: false, score: 0, exactMatch: false, reason: 'Search name is empty' };
|
|
32
|
+
}
|
|
33
|
+
// 1. Exact match (highest quality)
|
|
34
|
+
if (normalizedActualName === normalizedSearchName) {
|
|
35
|
+
return {
|
|
36
|
+
isMatch: true,
|
|
37
|
+
score: 100,
|
|
38
|
+
exactMatch: true,
|
|
39
|
+
reason: 'Exact match'
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// 2. Case-insensitive exact match (high quality)
|
|
43
|
+
if (normalizedActualName.toLowerCase() === normalizedSearchName.toLowerCase()) {
|
|
44
|
+
return {
|
|
45
|
+
isMatch: true,
|
|
46
|
+
score: 90,
|
|
47
|
+
exactMatch: true,
|
|
48
|
+
reason: 'Case-insensitive exact match'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// 3. Match after removing emojis (moderate quality)
|
|
52
|
+
const actualNameWithoutEmoji = normalizedActualName.replace(/[\p{Emoji}\u{FE00}-\u{FE0F}\u200d]+/gu, '').trim();
|
|
53
|
+
const searchNameWithoutEmoji = normalizedSearchName.replace(/[\p{Emoji}\u{FE00}-\u{FE0F}\u200d]+/gu, '').trim();
|
|
54
|
+
if (actualNameWithoutEmoji === searchNameWithoutEmoji) {
|
|
55
|
+
return {
|
|
56
|
+
isMatch: true,
|
|
57
|
+
score: 80,
|
|
58
|
+
exactMatch: false,
|
|
59
|
+
reason: 'Exact match after removing emojis'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (actualNameWithoutEmoji.toLowerCase() === searchNameWithoutEmoji.toLowerCase()) {
|
|
63
|
+
return {
|
|
64
|
+
isMatch: true,
|
|
65
|
+
score: 70,
|
|
66
|
+
exactMatch: false,
|
|
67
|
+
reason: 'Case-insensitive match after removing emojis'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// 4. Substring matches (lower quality)
|
|
71
|
+
const lowerActual = normalizedActualName.toLowerCase();
|
|
72
|
+
const lowerSearch = normalizedSearchName.toLowerCase();
|
|
73
|
+
// Full substring (term completely contained)
|
|
74
|
+
if (lowerActual.includes(lowerSearch)) {
|
|
75
|
+
return {
|
|
76
|
+
isMatch: true,
|
|
77
|
+
score: 60,
|
|
78
|
+
exactMatch: false,
|
|
79
|
+
reason: 'Search term found as substring in actual name'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (lowerSearch.includes(lowerActual)) {
|
|
83
|
+
return {
|
|
84
|
+
isMatch: true,
|
|
85
|
+
score: 50,
|
|
86
|
+
exactMatch: false,
|
|
87
|
+
reason: 'Actual name found as substring in search term'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// 5. Fuzzy emoji-less matches (lowest quality)
|
|
91
|
+
const lowerActualNoEmoji = actualNameWithoutEmoji.toLowerCase();
|
|
92
|
+
const lowerSearchNoEmoji = searchNameWithoutEmoji.toLowerCase();
|
|
93
|
+
if (lowerActualNoEmoji.includes(lowerSearchNoEmoji)) {
|
|
94
|
+
return {
|
|
95
|
+
isMatch: true,
|
|
96
|
+
score: 40,
|
|
97
|
+
exactMatch: false,
|
|
98
|
+
reason: 'Search term (without emoji) found as substring in actual name'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (lowerSearchNoEmoji.includes(lowerActualNoEmoji)) {
|
|
102
|
+
return {
|
|
103
|
+
isMatch: true,
|
|
104
|
+
score: 30,
|
|
105
|
+
exactMatch: false,
|
|
106
|
+
reason: 'Actual name (without emoji) found as substring in search term'
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// No match found
|
|
110
|
+
return {
|
|
111
|
+
isMatch: false,
|
|
112
|
+
score: 0,
|
|
113
|
+
exactMatch: false,
|
|
114
|
+
reason: 'No match found with any matching strategy'
|
|
115
|
+
};
|
|
43
116
|
}
|
|
44
117
|
/**
|
|
45
118
|
* Resolve a list ID from either a direct ID or list name
|
package/package.json
CHANGED