@taazkareem/clickup-mcp-server 0.6.2 → 0.6.4

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.
Files changed (41) hide show
  1. package/README.md +6 -6
  2. package/build/logger.js +26 -1
  3. package/build/server.js +1 -1
  4. package/build/services/clickup/base.js +22 -1
  5. package/build/services/clickup/bulk.js +111 -69
  6. package/build/services/clickup/index.js +2 -2
  7. package/build/services/clickup/task/index.js +32 -0
  8. package/build/services/clickup/task/task-attachments.js +97 -0
  9. package/build/services/clickup/task/task-comments.js +104 -0
  10. package/build/services/clickup/task/task-core.js +439 -0
  11. package/build/services/clickup/task/task-custom-fields.js +97 -0
  12. package/build/services/clickup/task/task-search.js +462 -0
  13. package/build/services/clickup/task/task-service.js +25 -0
  14. package/build/services/clickup/task/task-tags.js +101 -0
  15. package/build/services/clickup/workspace.js +81 -36
  16. package/build/tools/folder.js +1 -1
  17. package/build/tools/list.js +2 -4
  18. package/build/tools/task/attachments.js +49 -20
  19. package/build/tools/task/attachments.types.js +9 -0
  20. package/build/tools/task/bulk-operations.js +102 -18
  21. package/build/tools/task/handlers.js +216 -53
  22. package/build/tools/task/index.js +1 -1
  23. package/build/tools/task/main.js +161 -32
  24. package/build/tools/task/single-operations.js +82 -17
  25. package/build/tools/task/utilities.js +47 -75
  26. package/build/tools/utils.js +2 -2
  27. package/build/utils/date-utils.js +149 -30
  28. package/build/utils/resolver-utils.js +33 -40
  29. package/build/utils/sponsor-service.js +1 -1
  30. package/package.json +1 -1
  31. package/build/mcp-tools.js +0 -64
  32. package/build/server-state.js +0 -93
  33. package/build/server.log +0 -0
  34. package/build/services/clickup/task.js +0 -701
  35. package/build/tools/bulk-tasks.js +0 -36
  36. package/build/tools/debug.js +0 -76
  37. package/build/tools/logs.js +0 -55
  38. package/build/tools/task.js +0 -1554
  39. package/build/utils/params-utils.js +0 -39
  40. package/build/utils/sponsor-analytics.js +0 -100
  41. package/build/utils/sponsor-utils.js +0 -57
@@ -61,7 +61,8 @@ Requirements:
61
61
  Notes:
62
62
  - For multiple tasks, use create_bulk_tasks instead
63
63
  - Reuse list IDs from previous responses when possible to avoid redundant lookups
64
- - To create a subtask, set the parent parameter to the ID of the parent task`,
64
+ - To create a subtask, set the parent parameter to the ID of the parent task
65
+ - Custom fields can be set using the custom_fields parameter (array of {id, value} objects)`,
65
66
  inputSchema: {
66
67
  type: "object",
67
68
  properties: {
@@ -97,6 +98,10 @@ Notes:
97
98
  type: "string",
98
99
  description: "Optional due date. Supports Unix timestamps (ms) or natural language like '1 hour from now', 'tomorrow', 'next week', etc."
99
100
  },
101
+ startDate: {
102
+ type: "string",
103
+ description: "Optional start date. Supports Unix timestamps (ms) or natural language like 'now', 'start of today', etc."
104
+ },
100
105
  parent: {
101
106
  type: "string",
102
107
  description: "Optional ID of the parent task. When specified, this task will be created as a subtask of the specified parent task."
@@ -107,6 +112,27 @@ Notes:
107
112
  type: "string"
108
113
  },
109
114
  description: "Optional array of tag names to assign to the task. The tags must already exist in the space."
115
+ },
116
+ custom_fields: {
117
+ type: "array",
118
+ items: {
119
+ type: "object",
120
+ properties: {
121
+ id: {
122
+ type: "string",
123
+ description: "ID of the custom field"
124
+ },
125
+ value: {
126
+ description: "Value for the custom field. Type depends on the field type."
127
+ }
128
+ },
129
+ required: ["id", "value"]
130
+ },
131
+ description: "Optional array of custom field values to set on the task. Each object must have an 'id' and 'value' property."
132
+ },
133
+ check_required_custom_fields: {
134
+ type: "boolean",
135
+ description: "Optional flag to check if all required custom fields are set before saving the task."
110
136
  }
111
137
  }
112
138
  }
@@ -119,30 +145,39 @@ export const updateTaskTool = {
119
145
  description: `Purpose: Modify properties of an existing task.
120
146
 
121
147
  Valid Usage:
122
- 1. Use taskId alone (preferred if available)
123
- 2. Use taskName + listName
148
+ 1. Use taskId alone (preferred) - works with both regular and custom IDs
149
+ 2. Use taskName alone (will search across all lists)
150
+ 3. Use taskName + listName (for faster, targeted search)
124
151
 
125
152
  Requirements:
126
153
  - At least one update field (name, description, status, priority, dueDate) must be provided
127
- - When using taskName, listName is REQUIRED
154
+ - EITHER taskId OR taskName: REQUIRED
155
+ - listName: Optional, but recommended when using taskName
128
156
 
129
157
  Notes:
158
+ - The tool automatically searches for tasks using smart name matching
159
+ - When only taskName is provided, it searches across all lists
160
+ - Adding listName narrows the search to a specific list for better performance
130
161
  - Only specified fields will be updated
131
- - Using taskId is more reliable than taskName`,
162
+ - Custom fields can be set using the custom_fields parameter (array of {id, value} objects)
163
+
164
+ Warning:
165
+ - Using taskName without listName may match multiple tasks
166
+ - If multiple matches are found, the operation will fail with a disambiguation error`,
132
167
  inputSchema: {
133
168
  type: "object",
134
169
  properties: {
135
170
  taskId: {
136
171
  type: "string",
137
- description: "ID of the task to update (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
172
+ description: "ID of task to update (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
138
173
  },
139
174
  taskName: {
140
175
  type: "string",
141
- description: "Name of the task to update. Only use this if you don't have taskId. When using this parameter, you MUST also provide listName."
176
+ description: "Name of task to update. The tool will search for tasks with this name across all lists unless listName is specified."
142
177
  },
143
178
  listName: {
144
179
  type: "string",
145
- description: "Name of the list containing the task. REQUIRED when using taskName."
180
+ description: "Optional: Name of list containing the task. Providing this narrows the search to a specific list, improving performance and reducing ambiguity."
146
181
  },
147
182
  name: {
148
183
  type: "string",
@@ -168,9 +203,29 @@ Notes:
168
203
  dueDate: {
169
204
  type: "string",
170
205
  description: "New due date. Supports both Unix timestamps (in milliseconds) and natural language expressions like '1 hour from now', 'tomorrow', 'next week', or '3 days from now'."
206
+ },
207
+ startDate: {
208
+ type: "string",
209
+ description: "New start date. Supports both Unix timestamps (in milliseconds) and natural language expressions."
210
+ },
211
+ custom_fields: {
212
+ type: "array",
213
+ items: {
214
+ type: "object",
215
+ properties: {
216
+ id: {
217
+ type: "string",
218
+ description: "ID of the custom field"
219
+ },
220
+ value: {
221
+ description: "Value for the custom field. Type depends on the field type."
222
+ }
223
+ },
224
+ required: ["id", "value"]
225
+ },
226
+ description: "Optional array of custom field values to set on the task. Each object must have an 'id' and 'value' property."
171
227
  }
172
- },
173
- required: []
228
+ }
174
229
  }
175
230
  };
176
231
  /**
@@ -309,8 +364,7 @@ Note:
309
364
  type: "boolean",
310
365
  description: "Whether to include subtasks in the response. Set to true to retrieve full details of all subtasks."
311
366
  }
312
- },
313
- required: []
367
+ }
314
368
  }
315
369
  };
316
370
  /**
@@ -471,12 +525,23 @@ export const deleteTaskTool = {
471
525
 
472
526
  Valid Usage:
473
527
  1. Use taskId alone (preferred and safest)
474
- 2. Use taskName + optional listName
528
+ 2. Use taskName alone (will search across all lists)
529
+ 3. Use taskName + listName (for faster, targeted search)
530
+
531
+ Requirements:
532
+ - EITHER taskId OR taskName: REQUIRED
533
+ - listName: Optional, but recommended when using taskName
534
+
535
+ Notes:
536
+ - The tool automatically searches for tasks using smart name matching
537
+ - When only taskName is provided, it searches across all lists
538
+ - Adding listName narrows the search to a specific list for better performance
539
+ - Supports both regular task IDs and custom IDs (like 'DEV-1234')
475
540
 
476
541
  Warning:
477
542
  - This action CANNOT be undone
478
- - Using taskName is risky as names may not be unique
479
- - Provide listName when using taskName for more precise targeting`,
543
+ - Using taskName without listName may match multiple tasks
544
+ - If multiple matches are found, the operation will fail with a disambiguation error`,
480
545
  inputSchema: {
481
546
  type: "object",
482
547
  properties: {
@@ -486,11 +551,11 @@ Warning:
486
551
  },
487
552
  taskName: {
488
553
  type: "string",
489
- description: "Name of task to delete. Use with extreme caution as names may not be unique."
554
+ description: "Name of task to delete. The tool will search for tasks with this name across all lists unless listName is specified."
490
555
  },
491
556
  listName: {
492
557
  type: "string",
493
- description: "Name of list containing the task. Helps ensure correct task deletion when using taskName."
558
+ description: "Optional: Name of list containing the task. Providing this narrows the search to a specific list, improving performance and reducing ambiguity."
494
559
  }
495
560
  }
496
561
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { formatDueDate } from '../utils.js';
11
11
  import { clickUpServices } from '../../services/shared.js';
12
+ import { findListIDByName } from '../../tools/list.js';
12
13
  // Use shared services instance for ID resolution
13
14
  const { workspace: workspaceService } = clickUpServices;
14
15
  //=============================================================================
@@ -39,7 +40,7 @@ export function formatTaskData(task, additional = {}) {
39
40
  parent: task.parent,
40
41
  priority: task.priority,
41
42
  due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
42
- start_date: task.start_date,
43
+ start_date: task.start_date ? formatDueDate(Number(task.start_date)) : undefined,
43
44
  time_estimate: task.time_estimate,
44
45
  time_spent: task.time_spent,
45
46
  custom_fields: task.custom_fields,
@@ -70,13 +71,15 @@ export function formatTaskData(task, additional = {}) {
70
71
  /**
71
72
  * Validates task identification parameters
72
73
  * Ensures either taskId, customTaskId, or both taskName and listName are provided
74
+ * When useGlobalLookup is true, allows taskName without listName
73
75
  */
74
- export function validateTaskIdentification(taskId, taskName, listName, customTaskId) {
76
+ export function validateTaskIdentification(taskId, taskName, listName, customTaskId, useGlobalLookup = true) {
75
77
  if (!taskId && !taskName && !customTaskId) {
76
78
  throw new Error("Either taskId, customTaskId, or taskName must be provided");
77
79
  }
78
- if (!taskId && !customTaskId && taskName && !listName) {
79
- throw new Error("listName is required when using taskName");
80
+ // When global lookup is not enabled, we need list context for task name lookup
81
+ if (!useGlobalLookup && !taskId && !customTaskId && taskName && !listName) {
82
+ throw new Error("listName is required when using taskName and global lookup is disabled");
80
83
  }
81
84
  }
82
85
  /**
@@ -93,18 +96,52 @@ export function validateListIdentification(listId, listName) {
93
96
  * Ensures at least one update field is provided
94
97
  */
95
98
  export function validateTaskUpdateData(updateData) {
96
- const hasUpdates = Object.keys(updateData).length > 0;
99
+ // Check if there are any valid update fields present
100
+ const hasUpdates = Object.keys(updateData).some(key => {
101
+ return ['name', 'description', 'markdown_description', 'status', 'priority',
102
+ 'dueDate', 'startDate', 'taskId', 'taskName', 'custom_fields'].includes(key);
103
+ });
97
104
  if (!hasUpdates) {
98
105
  throw new Error("At least one field to update must be provided");
99
106
  }
107
+ // Validate custom_fields if provided
108
+ if (updateData.custom_fields) {
109
+ if (!Array.isArray(updateData.custom_fields)) {
110
+ throw new Error("custom_fields must be an array");
111
+ }
112
+ for (const field of updateData.custom_fields) {
113
+ if (!field.id || field.value === undefined) {
114
+ throw new Error("Each custom field must have both id and value properties");
115
+ }
116
+ }
117
+ }
100
118
  }
101
119
  /**
102
- * Validate bulk operation tasks array
120
+ * Validate bulk task array and task identification
121
+ * @param tasks Array of tasks to validate
122
+ * @param operation The bulk operation type ('create', 'update', 'move', 'delete')
103
123
  */
104
- export function validateBulkTasks(tasks) {
105
- if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
106
- throw new Error('You must provide a non-empty array of tasks');
124
+ export function validateBulkTasks(tasks, operation = 'update') {
125
+ if (!Array.isArray(tasks) || tasks.length === 0) {
126
+ throw new Error("tasks must be a non-empty array");
107
127
  }
128
+ tasks.forEach((task, index) => {
129
+ if (!task || typeof task !== 'object') {
130
+ throw new Error(`Task at index ${index} must be an object`);
131
+ }
132
+ // Skip task identification validation for create operations
133
+ if (operation === 'create') {
134
+ return;
135
+ }
136
+ // For bulk operations, require listName when using taskName
137
+ if (task.taskName && !task.listName) {
138
+ throw new Error(`Task at index ${index} using taskName must also provide listName`);
139
+ }
140
+ // At least one identifier is required for non-create operations
141
+ if (!task.taskId && !task.taskName && !task.customTaskId) {
142
+ throw new Error(`Task at index ${index} must provide either taskId, taskName + listName, or customTaskId`);
143
+ }
144
+ });
108
145
  }
109
146
  /**
110
147
  * Parse options for bulk operations
@@ -142,69 +179,6 @@ export function isCustomTaskId(id) {
142
179
  const customIdPattern = /^[A-Z]+-\d+$/;
143
180
  return customIdPattern.test(id);
144
181
  }
145
- //=============================================================================
146
- // ID RESOLUTION UTILITIES
147
- //=============================================================================
148
- /**
149
- * Resolves a task ID from direct ID, custom ID, or name
150
- * Handles validation and throws appropriate errors
151
- */
152
- export async function resolveTaskIdWithValidation(taskId, taskName, listName, customTaskId) {
153
- // Validate parameters
154
- validateTaskIdentification(taskId, taskName, listName, customTaskId);
155
- // If customTaskId is explicitly provided, use it
156
- if (customTaskId) {
157
- const { task: taskService } = clickUpServices;
158
- try {
159
- // First try to get the task by custom ID
160
- // If listName is provided, we can also look up in a specific list for better performance
161
- let listId;
162
- if (listName) {
163
- listId = await resolveListIdWithValidation(undefined, listName);
164
- }
165
- // Look up by custom ID
166
- const foundTask = await taskService.getTaskByCustomId(customTaskId, listId);
167
- return foundTask.id;
168
- }
169
- catch (error) {
170
- throw new Error(`Task with custom ID "${customTaskId}" not found`);
171
- }
172
- }
173
- // If taskId is provided, check if it looks like a custom ID
174
- if (taskId) {
175
- if (isCustomTaskId(taskId)) {
176
- console.log(`Detected task ID "${taskId}" as a custom ID, using custom ID lookup`);
177
- // If it looks like a custom ID, try to get it as a custom ID first
178
- const { task: taskService } = clickUpServices;
179
- try {
180
- // Look up by custom ID
181
- let listId;
182
- if (listName) {
183
- listId = await resolveListIdWithValidation(undefined, listName);
184
- }
185
- const foundTask = await taskService.getTaskByCustomId(taskId, listId);
186
- return foundTask.id;
187
- }
188
- catch (error) {
189
- // If it fails as a custom ID, try as a regular ID
190
- console.log(`Failed to find task with custom ID "${taskId}", falling back to regular ID`);
191
- return taskId;
192
- }
193
- }
194
- // Regular task ID
195
- return taskId;
196
- }
197
- // At this point we know we have taskName and listName (validation ensures this)
198
- // Find the list ID from its name
199
- const listId = await resolveListIdWithValidation(undefined, listName);
200
- // Find the task in the list
201
- const { task: taskService } = clickUpServices;
202
- const foundTask = await taskService.findTaskByName(listId, taskName);
203
- if (!foundTask) {
204
- throw new Error(`Task "${taskName}" not found in list "${listName}"`);
205
- }
206
- return foundTask.id;
207
- }
208
182
  /**
209
183
  * Resolves a list ID from either direct ID or name
210
184
  * Handles validation and throws appropriate errors
@@ -216,9 +190,7 @@ export async function resolveListIdWithValidation(listId, listName) {
216
190
  if (listId)
217
191
  return listId;
218
192
  // At this point we know we have listName (validation ensures this)
219
- // Find the list ID from its name
220
- const hierarchy = await workspaceService.getWorkspaceHierarchy();
221
- const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
193
+ const listInfo = await findListIDByName(workspaceService, listName);
222
194
  if (!listInfo) {
223
195
  throw new Error(`List "${listName}" not found`);
224
196
  }
@@ -7,6 +7,6 @@
7
7
  * Re-exports specialized utilities from dedicated modules.
8
8
  */
9
9
  // Re-export date utilities
10
- export { getRelativeTimestamp, parseDueDate, formatDueDate } from '../utils/date-utils.js';
10
+ export { getRelativeTimestamp, parseDueDate, formatDueDate, formatRelativeTime } from '../utils/date-utils.js';
11
11
  // Re-export resolver utilities
12
- export { resolveListId, resolveTaskId } from '../utils/resolver-utils.js';
12
+ export { resolveListId } from '../utils/resolver-utils.js';
@@ -30,6 +30,31 @@ export function getRelativeTimestamp(minutes = 0, hours = 0, days = 0, weeks = 0
30
30
  now.setMonth(now.getMonth() + months);
31
31
  return now.getTime();
32
32
  }
33
+ /**
34
+ * Get the start of today (midnight) in Unix milliseconds
35
+ * @returns Timestamp in milliseconds for start of current day
36
+ */
37
+ export function getStartOfDay() {
38
+ const now = new Date();
39
+ now.setHours(0, 0, 0, 0);
40
+ return now.getTime();
41
+ }
42
+ /**
43
+ * Get the end of today (23:59:59.999) in Unix milliseconds
44
+ * @returns Timestamp in milliseconds for end of current day
45
+ */
46
+ export function getEndOfDay() {
47
+ const now = new Date();
48
+ now.setHours(23, 59, 59, 999);
49
+ return now.getTime();
50
+ }
51
+ /**
52
+ * Get the current time in Unix milliseconds
53
+ * @returns Current timestamp in milliseconds
54
+ */
55
+ export function getCurrentTimestamp() {
56
+ return new Date().getTime();
57
+ }
33
58
  /**
34
59
  * Parse a due date string into a timestamp
35
60
  * Supports ISO 8601 format or natural language like "tomorrow"
@@ -42,15 +67,36 @@ export function parseDueDate(dateString) {
42
67
  return undefined;
43
68
  try {
44
69
  // Handle natural language dates
45
- const lowerDate = dateString.toLowerCase();
46
- const now = new Date();
70
+ const lowerDate = dateString.toLowerCase().trim();
71
+ // Handle "now" specifically
72
+ if (lowerDate === 'now') {
73
+ return getCurrentTimestamp();
74
+ }
75
+ // Handle "today" with different options
47
76
  if (lowerDate === 'today') {
48
- const today = new Date();
49
- today.setHours(23, 59, 59, 999);
50
- return today.getTime();
77
+ return getEndOfDay();
78
+ }
79
+ if (lowerDate === 'today start' || lowerDate === 'start of today') {
80
+ return getStartOfDay();
81
+ }
82
+ if (lowerDate === 'today end' || lowerDate === 'end of today') {
83
+ return getEndOfDay();
84
+ }
85
+ // Handle "yesterday" and "tomorrow"
86
+ if (lowerDate === 'yesterday') {
87
+ const yesterday = new Date();
88
+ yesterday.setDate(yesterday.getDate() - 1);
89
+ yesterday.setHours(23, 59, 59, 999);
90
+ return yesterday.getTime();
91
+ }
92
+ if (lowerDate === 'tomorrow') {
93
+ const tomorrow = new Date();
94
+ tomorrow.setDate(tomorrow.getDate() + 1);
95
+ tomorrow.setHours(23, 59, 59, 999);
96
+ return tomorrow.getTime();
51
97
  }
52
98
  // Handle relative dates with specific times
53
- const relativeTimeRegex = /(?:(\d+)\s*(minutes?|days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
99
+ const relativeTimeRegex = /(?:(\d+)\s*(minutes?|hours?|days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month|year))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
54
100
  const match = lowerDate.match(relativeTimeRegex);
55
101
  if (match) {
56
102
  const date = new Date();
@@ -61,6 +107,9 @@ export function parseDueDate(dateString) {
61
107
  if (unit.startsWith('minute')) {
62
108
  date.setMinutes(date.getMinutes() + value);
63
109
  }
110
+ else if (unit.startsWith('hour')) {
111
+ date.setHours(date.getHours() + value);
112
+ }
64
113
  else if (unit.startsWith('day')) {
65
114
  date.setDate(date.getDate() + value);
66
115
  }
@@ -80,6 +129,9 @@ export function parseDueDate(dateString) {
80
129
  else if (lowerDate.includes('next month')) {
81
130
  date.setMonth(date.getMonth() + 1);
82
131
  }
132
+ else if (lowerDate.includes('next year')) {
133
+ date.setFullYear(date.getFullYear() + 1);
134
+ }
83
135
  // Set the time if specified
84
136
  if (hours) {
85
137
  let parsedHours = parseInt(hours);
@@ -97,31 +149,44 @@ export function parseDueDate(dateString) {
97
149
  }
98
150
  return date.getTime();
99
151
  }
100
- // Handle hours from now
101
- const minutesRegex = /(\d+)\s*minutes?\s*from\s*now/i;
102
- const hoursRegex = /(\d+)\s*hours?\s*from\s*now/i;
103
- const daysRegex = /(\d+)\s*days?\s*from\s*now/i;
104
- const weeksRegex = /(\d+)\s*weeks?\s*from\s*now/i;
105
- const monthsRegex = /(\d+)\s*months?\s*from\s*now/i;
106
- if (minutesRegex.test(lowerDate)) {
107
- const minutes = parseInt(lowerDate.match(minutesRegex)[1]);
108
- return getRelativeTimestamp(minutes);
109
- }
110
- if (hoursRegex.test(lowerDate)) {
111
- const hours = parseInt(lowerDate.match(hoursRegex)[1]);
112
- return getRelativeTimestamp(0, hours);
113
- }
114
- if (daysRegex.test(lowerDate)) {
115
- const days = parseInt(lowerDate.match(daysRegex)[1]);
116
- return getRelativeTimestamp(0, 0, days);
117
- }
118
- if (weeksRegex.test(lowerDate)) {
119
- const weeks = parseInt(lowerDate.match(weeksRegex)[1]);
120
- return getRelativeTimestamp(0, 0, 0, weeks);
152
+ // Handle various relative formats
153
+ const relativeFormats = [
154
+ { regex: /(\d+)\s*minutes?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(m) },
155
+ { regex: /(\d+)\s*hours?\s*from\s*now/i, handler: (h) => getRelativeTimestamp(0, h) },
156
+ { regex: /(\d+)\s*days?\s*from\s*now/i, handler: (d) => getRelativeTimestamp(0, 0, d) },
157
+ { regex: /(\d+)\s*weeks?\s*from\s*now/i, handler: (w) => getRelativeTimestamp(0, 0, 0, w) },
158
+ { regex: /(\d+)\s*months?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(0, 0, 0, 0, m) }
159
+ ];
160
+ for (const format of relativeFormats) {
161
+ if (format.regex.test(lowerDate)) {
162
+ const value = parseInt(lowerDate.match(format.regex)[1]);
163
+ return format.handler(value);
164
+ }
121
165
  }
122
- if (monthsRegex.test(lowerDate)) {
123
- const months = parseInt(lowerDate.match(monthsRegex)[1]);
124
- return getRelativeTimestamp(0, 0, 0, 0, months);
166
+ // Handle specific date formats
167
+ // Format: MM/DD/YYYY
168
+ const usDateRegex = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2})(?::(\d{1,2}))?(?:\s+(am|pm))?)?$/i;
169
+ const usDateMatch = lowerDate.match(usDateRegex);
170
+ if (usDateMatch) {
171
+ const [_, month, day, year, hours, minutes, meridian] = usDateMatch;
172
+ const date = new Date(parseInt(year), parseInt(month) - 1, // JS months are 0-indexed
173
+ parseInt(day));
174
+ // Add time if specified
175
+ if (hours) {
176
+ let parsedHours = parseInt(hours);
177
+ const parsedMinutes = minutes ? parseInt(minutes) : 0;
178
+ // Convert to 24-hour format if meridian is specified
179
+ if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
180
+ parsedHours += 12;
181
+ if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
182
+ parsedHours = 0;
183
+ date.setHours(parsedHours, parsedMinutes, 0, 0);
184
+ }
185
+ else {
186
+ // Default to end of day if no time specified
187
+ date.setHours(23, 59, 59, 999);
188
+ }
189
+ return date.getTime();
125
190
  }
126
191
  // Try to parse as a date string
127
192
  const date = new Date(dateString);
@@ -164,3 +229,57 @@ export function formatDueDate(timestamp) {
164
229
  return undefined;
165
230
  }
166
231
  }
232
+ /**
233
+ * Checks if a timestamp is for today
234
+ *
235
+ * @param timestamp Unix timestamp in milliseconds
236
+ * @returns Boolean indicating if the timestamp is for today
237
+ */
238
+ export function isToday(timestamp) {
239
+ const date = new Date(timestamp);
240
+ const today = new Date();
241
+ return date.getDate() === today.getDate() &&
242
+ date.getMonth() === today.getMonth() &&
243
+ date.getFullYear() === today.getFullYear();
244
+ }
245
+ /**
246
+ * Get timestamp range for today (start to end)
247
+ *
248
+ * @returns Object with start and end timestamps for today
249
+ */
250
+ export function getTodayRange() {
251
+ return {
252
+ start: getStartOfDay(),
253
+ end: getEndOfDay()
254
+ };
255
+ }
256
+ /**
257
+ * Format a date for display in errors and messages
258
+ * @param timestamp The timestamp to format
259
+ * @returns A human-readable relative time (e.g., "2 hours ago")
260
+ */
261
+ export function formatRelativeTime(timestamp) {
262
+ if (!timestamp)
263
+ return 'Unknown';
264
+ const timestampNum = typeof timestamp === 'string' ? parseInt(timestamp, 10) : timestamp;
265
+ const now = Date.now();
266
+ const diffMs = now - timestampNum;
267
+ // Convert to appropriate time unit
268
+ const diffSec = Math.floor(diffMs / 1000);
269
+ if (diffSec < 60)
270
+ return `${diffSec} seconds ago`;
271
+ const diffMin = Math.floor(diffSec / 60);
272
+ if (diffMin < 60)
273
+ return `${diffMin} minutes ago`;
274
+ const diffHour = Math.floor(diffMin / 60);
275
+ if (diffHour < 24)
276
+ return `${diffHour} hours ago`;
277
+ const diffDays = Math.floor(diffHour / 24);
278
+ if (diffDays < 30)
279
+ return `${diffDays} days ago`;
280
+ const diffMonths = Math.floor(diffDays / 30);
281
+ if (diffMonths < 12)
282
+ return `${diffMonths} months ago`;
283
+ const diffYears = Math.floor(diffMonths / 12);
284
+ return `${diffYears} years ago`;
285
+ }
@@ -9,13 +9,40 @@
9
9
  import { clickUpServices } from '../services/shared.js';
10
10
  import { findListIDByName } from '../tools/list.js';
11
11
  /**
12
- * Resolve a list ID from either a direct ID or list name
12
+ * Check if a task name matches search criteria
13
13
  *
14
- * @param listId Optional direct list ID
15
- * @param listName Optional list name to resolve
16
- * @param workspaceService Workspace service to use for lookup
17
- * @returns Resolved list ID
18
- * @throws Error if neither listId nor listName is provided, or if list name can't be resolved
14
+ * Performs flexible case-insensitive and emoji-aware text matching
15
+ * Used by multiple components for consistent name matching behavior
16
+ */
17
+ export function isNameMatch(taskName, searchTerm) {
18
+ const normalizedTask = taskName.toLowerCase().trim();
19
+ const normalizedSearch = searchTerm.toLowerCase().trim();
20
+ // Handle empty strings - don't match empty task names
21
+ if (normalizedTask === '')
22
+ return false;
23
+ if (normalizedSearch === '')
24
+ return false;
25
+ // Exact match check
26
+ if (normalizedTask === normalizedSearch)
27
+ return true;
28
+ // Substring match check
29
+ if (normalizedTask.includes(normalizedSearch) || normalizedSearch.includes(normalizedTask))
30
+ return true;
31
+ // Handle emoji characters in names
32
+ if (/[\p{Emoji}]/u.test(normalizedSearch) || /[\p{Emoji}]/u.test(normalizedTask)) {
33
+ const taskWithoutEmoji = normalizedTask.replace(/[\p{Emoji}]/gu, '').trim();
34
+ const searchWithoutEmoji = normalizedSearch.replace(/[\p{Emoji}]/gu, '').trim();
35
+ // Don't match if either becomes empty after emoji removal
36
+ if (taskWithoutEmoji === '' || searchWithoutEmoji === '')
37
+ return false;
38
+ return taskWithoutEmoji === searchWithoutEmoji ||
39
+ taskWithoutEmoji.includes(searchWithoutEmoji) ||
40
+ searchWithoutEmoji.includes(taskWithoutEmoji);
41
+ }
42
+ return false;
43
+ }
44
+ /**
45
+ * Resolve a list ID from either a direct ID or list name
19
46
  */
20
47
  export async function resolveListId(listId, listName, workspaceService = clickUpServices.workspace) {
21
48
  // If list ID is directly provided, use it
@@ -33,37 +60,3 @@ export async function resolveListId(listId, listName, workspaceService = clickUp
33
60
  // If neither is provided, throw an error
34
61
  throw new Error("Either listId or listName must be provided");
35
62
  }
36
- /**
37
- * Resolve a task ID from either a direct ID or task name + list info
38
- *
39
- * @param taskId Optional direct task ID
40
- * @param taskName Optional task name to resolve
41
- * @param listId Optional list ID for task lookup
42
- * @param listName Optional list name for task lookup
43
- * @param taskService Task service to use for lookup
44
- * @returns Resolved task ID
45
- * @throws Error if parameters are insufficient or task can't be found
46
- */
47
- export async function resolveTaskId(taskId, taskName, listId, listName, taskService = clickUpServices.task) {
48
- // If task ID is directly provided, use it
49
- if (taskId) {
50
- return taskId;
51
- }
52
- // If task name is provided, we need list info to find it
53
- if (taskName) {
54
- // We need either listId or listName to find a task by name
55
- if (!listId && !listName) {
56
- throw new Error(`List name or ID is required when using task name for task "${taskName}"`);
57
- }
58
- // Get list ID
59
- const targetListId = await resolveListId(listId, listName);
60
- // Find the task in the list
61
- const foundTask = await taskService.findTaskByName(targetListId, taskName);
62
- if (!foundTask) {
63
- throw new Error(`Task "${taskName}" not found in list`);
64
- }
65
- return foundTask.id;
66
- }
67
- // If neither is provided, throw an error
68
- throw new Error("Either taskId or taskName must be provided");
69
- }