@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.
- package/README.md +6 -6
- package/build/logger.js +26 -1
- package/build/server.js +1 -1
- package/build/services/clickup/base.js +22 -1
- package/build/services/clickup/bulk.js +111 -69
- package/build/services/clickup/index.js +2 -2
- package/build/services/clickup/task/index.js +32 -0
- package/build/services/clickup/task/task-attachments.js +97 -0
- package/build/services/clickup/task/task-comments.js +104 -0
- package/build/services/clickup/task/task-core.js +439 -0
- package/build/services/clickup/task/task-custom-fields.js +97 -0
- package/build/services/clickup/task/task-search.js +462 -0
- package/build/services/clickup/task/task-service.js +25 -0
- package/build/services/clickup/task/task-tags.js +101 -0
- package/build/services/clickup/workspace.js +81 -36
- package/build/tools/folder.js +1 -1
- package/build/tools/list.js +2 -4
- package/build/tools/task/attachments.js +49 -20
- package/build/tools/task/attachments.types.js +9 -0
- package/build/tools/task/bulk-operations.js +102 -18
- package/build/tools/task/handlers.js +216 -53
- package/build/tools/task/index.js +1 -1
- package/build/tools/task/main.js +161 -32
- package/build/tools/task/single-operations.js +82 -17
- package/build/tools/task/utilities.js +47 -75
- package/build/tools/utils.js +2 -2
- package/build/utils/date-utils.js +149 -30
- package/build/utils/resolver-utils.js +33 -40
- package/build/utils/sponsor-service.js +1 -1
- package/package.json +1 -1
- package/build/mcp-tools.js +0 -64
- package/build/server-state.js +0 -93
- package/build/server.log +0 -0
- package/build/services/clickup/task.js +0 -701
- package/build/tools/bulk-tasks.js +0 -36
- package/build/tools/debug.js +0 -76
- package/build/tools/logs.js +0 -55
- package/build/tools/task.js +0 -1554
- package/build/utils/params-utils.js +0 -39
- package/build/utils/sponsor-analytics.js +0 -100
- 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
|
|
123
|
-
2. Use taskName
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
479
|
-
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
106
|
-
throw new Error(
|
|
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
|
-
|
|
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
|
}
|
package/build/tools/utils.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
*
|
|
12
|
+
* Check if a task name matches search criteria
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
}
|