@taazkareem/clickup-mcp-server 0.4.70 → 0.4.71
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 +33 -16
- package/build/config.js +7 -4
- package/build/index.js +1 -1
- package/build/mcp-tools.js +64 -0
- package/build/server.js +4 -4
- package/build/server.log +374 -134
- package/build/services/clickup/bulk.js +132 -101
- package/build/services/clickup/task.js +40 -239
- package/build/tools/bulk-tasks.js +36 -0
- package/build/tools/task.js +614 -537
- package/build/tools/utils.js +8 -147
- package/build/utils/concurrency-utils.js +245 -0
- package/build/utils/date-utils.js +152 -0
- package/build/utils/params-utils.js +39 -0
- package/build/utils/resolver-utils.js +66 -0
- package/build/utils/sponsor-utils.js +49 -0
- package/package.json +1 -1
package/build/tools/utils.js
CHANGED
|
@@ -1,150 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility functions for ClickUp MCP tools
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Get a timestamp for a relative time
|
|
6
|
-
*
|
|
7
|
-
* @param hours Hours from now
|
|
8
|
-
* @param days Days from now
|
|
9
|
-
* @param weeks Weeks from now
|
|
10
|
-
* @param months Months from now
|
|
11
|
-
* @returns Timestamp in milliseconds
|
|
12
|
-
*/
|
|
13
|
-
export function getRelativeTimestamp(hours = 0, days = 0, weeks = 0, months = 0) {
|
|
14
|
-
const now = new Date();
|
|
15
|
-
if (hours)
|
|
16
|
-
now.setHours(now.getHours() + hours);
|
|
17
|
-
if (days)
|
|
18
|
-
now.setDate(now.getDate() + days);
|
|
19
|
-
if (weeks)
|
|
20
|
-
now.setDate(now.getDate() + (weeks * 7));
|
|
21
|
-
if (months)
|
|
22
|
-
now.setMonth(now.getMonth() + months);
|
|
23
|
-
return now.getTime();
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Parse a due date string into a timestamp
|
|
27
|
-
* Supports ISO 8601 format or natural language like "tomorrow"
|
|
28
3
|
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const lowerDate = dateString.toLowerCase();
|
|
38
|
-
const now = new Date();
|
|
39
|
-
if (lowerDate === 'today') {
|
|
40
|
-
const today = new Date();
|
|
41
|
-
today.setHours(23, 59, 59, 999);
|
|
42
|
-
return today.getTime();
|
|
43
|
-
}
|
|
44
|
-
// Handle relative dates with specific times
|
|
45
|
-
const relativeTimeRegex = /(?:(\d+)\s*(days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
|
|
46
|
-
const match = lowerDate.match(relativeTimeRegex);
|
|
47
|
-
if (match) {
|
|
48
|
-
const date = new Date();
|
|
49
|
-
const [_, amount, unit, hours, minutes, meridian] = match;
|
|
50
|
-
// Calculate the future date
|
|
51
|
-
if (amount && unit) {
|
|
52
|
-
const value = parseInt(amount);
|
|
53
|
-
if (unit.startsWith('day')) {
|
|
54
|
-
date.setDate(date.getDate() + value);
|
|
55
|
-
}
|
|
56
|
-
else if (unit.startsWith('week')) {
|
|
57
|
-
date.setDate(date.getDate() + (value * 7));
|
|
58
|
-
}
|
|
59
|
-
else if (unit.startsWith('month')) {
|
|
60
|
-
date.setMonth(date.getMonth() + value);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
else if (lowerDate.startsWith('tomorrow')) {
|
|
64
|
-
date.setDate(date.getDate() + 1);
|
|
65
|
-
}
|
|
66
|
-
else if (lowerDate.includes('next week')) {
|
|
67
|
-
date.setDate(date.getDate() + 7);
|
|
68
|
-
}
|
|
69
|
-
else if (lowerDate.includes('next month')) {
|
|
70
|
-
date.setMonth(date.getMonth() + 1);
|
|
71
|
-
}
|
|
72
|
-
// Set the time if specified
|
|
73
|
-
if (hours) {
|
|
74
|
-
let parsedHours = parseInt(hours);
|
|
75
|
-
const parsedMinutes = minutes ? parseInt(minutes) : 0;
|
|
76
|
-
// Convert to 24-hour format if meridian is specified
|
|
77
|
-
if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
|
|
78
|
-
parsedHours += 12;
|
|
79
|
-
if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
|
|
80
|
-
parsedHours = 0;
|
|
81
|
-
date.setHours(parsedHours, parsedMinutes, 0, 0);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
// Default to end of day if no time specified
|
|
85
|
-
date.setHours(23, 59, 59, 999);
|
|
86
|
-
}
|
|
87
|
-
return date.getTime();
|
|
88
|
-
}
|
|
89
|
-
// Handle hours from now
|
|
90
|
-
const hoursRegex = /(\d+)\s*hours?\s*from\s*now/i;
|
|
91
|
-
const daysRegex = /(\d+)\s*days?\s*from\s*now/i;
|
|
92
|
-
const weeksRegex = /(\d+)\s*weeks?\s*from\s*now/i;
|
|
93
|
-
const monthsRegex = /(\d+)\s*months?\s*from\s*now/i;
|
|
94
|
-
if (hoursRegex.test(lowerDate)) {
|
|
95
|
-
const hours = parseInt(lowerDate.match(hoursRegex)[1]);
|
|
96
|
-
return getRelativeTimestamp(hours);
|
|
97
|
-
}
|
|
98
|
-
if (daysRegex.test(lowerDate)) {
|
|
99
|
-
const days = parseInt(lowerDate.match(daysRegex)[1]);
|
|
100
|
-
return getRelativeTimestamp(0, days);
|
|
101
|
-
}
|
|
102
|
-
if (weeksRegex.test(lowerDate)) {
|
|
103
|
-
const weeks = parseInt(lowerDate.match(weeksRegex)[1]);
|
|
104
|
-
return getRelativeTimestamp(0, 0, weeks);
|
|
105
|
-
}
|
|
106
|
-
if (monthsRegex.test(lowerDate)) {
|
|
107
|
-
const months = parseInt(lowerDate.match(monthsRegex)[1]);
|
|
108
|
-
return getRelativeTimestamp(0, 0, 0, months);
|
|
109
|
-
}
|
|
110
|
-
// Try to parse as a date string
|
|
111
|
-
const date = new Date(dateString);
|
|
112
|
-
if (!isNaN(date.getTime())) {
|
|
113
|
-
return date.getTime();
|
|
114
|
-
}
|
|
115
|
-
// If all parsing fails, return undefined
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
console.warn(`Failed to parse due date: ${dateString}`, error);
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Format a due date timestamp into a human-readable string
|
|
125
|
-
*
|
|
126
|
-
* @param timestamp Unix timestamp in milliseconds
|
|
127
|
-
* @returns Formatted date string or undefined if timestamp is invalid
|
|
128
|
-
*/
|
|
129
|
-
export function formatDueDate(timestamp) {
|
|
130
|
-
if (!timestamp)
|
|
131
|
-
return undefined;
|
|
132
|
-
try {
|
|
133
|
-
const date = new Date(timestamp);
|
|
134
|
-
if (isNaN(date.getTime()))
|
|
135
|
-
return undefined;
|
|
136
|
-
// Format: "March 10, 2025 at 10:56 PM"
|
|
137
|
-
return date.toLocaleString('en-US', {
|
|
138
|
-
year: 'numeric',
|
|
139
|
-
month: 'long',
|
|
140
|
-
day: 'numeric',
|
|
141
|
-
hour: 'numeric',
|
|
142
|
-
minute: '2-digit',
|
|
143
|
-
hour12: true
|
|
144
|
-
}).replace(' at', ',');
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
console.warn(`Failed to format due date: ${timestamp}`, error);
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
4
|
+
* Re-exports specialized utilities from dedicated modules.
|
|
5
|
+
*/
|
|
6
|
+
// Re-export date utilities
|
|
7
|
+
export { getRelativeTimestamp, parseDueDate, formatDueDate } from '../utils/date-utils.js';
|
|
8
|
+
// Re-export sponsor utilities
|
|
9
|
+
export { getSponsorMessage, enhanceResponseWithSponsor } from '../utils/sponsor-utils.js';
|
|
10
|
+
// Re-export resolver utilities
|
|
11
|
+
export { resolveListId, resolveTaskId } from '../utils/resolver-utils.js';
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concurrency Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for handling concurrent operations,
|
|
5
|
+
* batch processing, rate limiting, and retry logic.
|
|
6
|
+
*/
|
|
7
|
+
import { Logger } from '../logger.js';
|
|
8
|
+
// Create logger instance for this module
|
|
9
|
+
const logger = new Logger('ConcurrencyUtils');
|
|
10
|
+
/**
|
|
11
|
+
* Process a collection of items in batches with configurable concurrency
|
|
12
|
+
*
|
|
13
|
+
* This utility handles:
|
|
14
|
+
* - Breaking items into manageable batches
|
|
15
|
+
* - Processing multiple items concurrently
|
|
16
|
+
* - Retrying failed operations with backoff
|
|
17
|
+
* - Tracking progress and aggregating results
|
|
18
|
+
* - Graceful error handling
|
|
19
|
+
*
|
|
20
|
+
* @param items Array of items to process
|
|
21
|
+
* @param processor Function that processes a single item
|
|
22
|
+
* @param options Configuration options for batch processing
|
|
23
|
+
* @returns Results of the processing with success and failure information
|
|
24
|
+
*/
|
|
25
|
+
export async function processBatch(items, processor, options) {
|
|
26
|
+
// Apply default options
|
|
27
|
+
const opts = {
|
|
28
|
+
batchSize: options?.batchSize ?? 10,
|
|
29
|
+
concurrency: options?.concurrency ?? 3,
|
|
30
|
+
continueOnError: options?.continueOnError ?? true,
|
|
31
|
+
retryCount: options?.retryCount ?? 3,
|
|
32
|
+
retryDelay: options?.retryDelay ?? 1000,
|
|
33
|
+
exponentialBackoff: options?.exponentialBackoff ?? true,
|
|
34
|
+
progressCallback: options?.progressCallback ?? (() => { })
|
|
35
|
+
};
|
|
36
|
+
// Initialize results
|
|
37
|
+
const result = {
|
|
38
|
+
successful: [],
|
|
39
|
+
failed: [],
|
|
40
|
+
totals: {
|
|
41
|
+
success: 0,
|
|
42
|
+
failure: 0,
|
|
43
|
+
total: items.length
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
// Handle empty input array
|
|
47
|
+
if (items.length === 0) {
|
|
48
|
+
logger.info('processBatch called with empty items array');
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const totalBatches = Math.ceil(items.length / opts.batchSize);
|
|
53
|
+
let processedItems = 0;
|
|
54
|
+
logger.info(`Starting batch processing of ${items.length} items`, {
|
|
55
|
+
totalBatches,
|
|
56
|
+
batchSize: opts.batchSize,
|
|
57
|
+
concurrency: opts.concurrency
|
|
58
|
+
});
|
|
59
|
+
// Process items in batches
|
|
60
|
+
for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
|
|
61
|
+
const startIdx = batchIndex * opts.batchSize;
|
|
62
|
+
const endIdx = Math.min(startIdx + opts.batchSize, items.length);
|
|
63
|
+
const batch = items.slice(startIdx, endIdx);
|
|
64
|
+
logger.debug(`Processing batch ${batchIndex + 1}/${totalBatches}`, {
|
|
65
|
+
batchSize: batch.length,
|
|
66
|
+
startIdx,
|
|
67
|
+
endIdx
|
|
68
|
+
});
|
|
69
|
+
// Process the current batch
|
|
70
|
+
const batchResults = await processSingleBatch(batch, processor, startIdx, opts);
|
|
71
|
+
// Aggregate results
|
|
72
|
+
result.successful.push(...batchResults.successful);
|
|
73
|
+
result.failed.push(...batchResults.failed);
|
|
74
|
+
result.totals.success += batchResults.totals.success;
|
|
75
|
+
result.totals.failure += batchResults.totals.failure;
|
|
76
|
+
// Stop processing if an error occurred and continueOnError is false
|
|
77
|
+
if (batchResults.totals.failure > 0 && !opts.continueOnError) {
|
|
78
|
+
logger.warn(`Stopping batch processing due to failure and continueOnError=false`, {
|
|
79
|
+
failedItems: batchResults.totals.failure
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
// Update progress
|
|
84
|
+
processedItems += batch.length;
|
|
85
|
+
opts.progressCallback(processedItems, items.length, result.totals.success, result.totals.failure);
|
|
86
|
+
}
|
|
87
|
+
logger.info(`Batch processing completed`, {
|
|
88
|
+
totalItems: items.length,
|
|
89
|
+
successful: result.totals.success,
|
|
90
|
+
failed: result.totals.failure
|
|
91
|
+
});
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
logger.error(`Unexpected error in batch processing`, {
|
|
96
|
+
error: error.message || String(error)
|
|
97
|
+
});
|
|
98
|
+
// Add any unprocessed items as failures
|
|
99
|
+
const processedCount = result.totals.success + result.totals.failure;
|
|
100
|
+
if (processedCount < items.length) {
|
|
101
|
+
const remainingItems = items.slice(processedCount);
|
|
102
|
+
for (let i = 0; i < remainingItems.length; i++) {
|
|
103
|
+
const index = processedCount + i;
|
|
104
|
+
result.failed.push({
|
|
105
|
+
item: remainingItems[i],
|
|
106
|
+
error: new Error('Batch processing failed: ' + (error.message || 'Unknown error')),
|
|
107
|
+
index
|
|
108
|
+
});
|
|
109
|
+
result.totals.failure++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Process a single batch of items with concurrency
|
|
117
|
+
*
|
|
118
|
+
* @param batch The batch of items to process
|
|
119
|
+
* @param processor The function to process each item
|
|
120
|
+
* @param startIndex The starting index of the batch in the original array
|
|
121
|
+
* @param opts Processing options
|
|
122
|
+
* @returns Results for this batch
|
|
123
|
+
*/
|
|
124
|
+
async function processSingleBatch(batch, processor, startIndex, opts) {
|
|
125
|
+
const result = {
|
|
126
|
+
successful: [],
|
|
127
|
+
failed: [],
|
|
128
|
+
totals: {
|
|
129
|
+
success: 0,
|
|
130
|
+
failure: 0,
|
|
131
|
+
total: batch.length
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
// Process items in concurrent chunks
|
|
136
|
+
for (let i = 0; i < batch.length; i += opts.concurrency) {
|
|
137
|
+
const concurrentBatch = batch.slice(i, Math.min(i + opts.concurrency, batch.length));
|
|
138
|
+
// Create a promise for each item in the concurrent batch
|
|
139
|
+
const promises = concurrentBatch.map((item, idx) => {
|
|
140
|
+
const index = startIndex + i + idx;
|
|
141
|
+
return processWithRetry(() => processor(item, index), item, index, opts);
|
|
142
|
+
});
|
|
143
|
+
// Wait for all promises to settle (either resolve or reject)
|
|
144
|
+
const results = await Promise.allSettled(promises);
|
|
145
|
+
// Process the results
|
|
146
|
+
results.forEach((promiseResult, idx) => {
|
|
147
|
+
const index = startIndex + i + idx;
|
|
148
|
+
if (promiseResult.status === 'fulfilled') {
|
|
149
|
+
// Operation succeeded
|
|
150
|
+
result.successful.push(promiseResult.value);
|
|
151
|
+
result.totals.success++;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Operation failed
|
|
155
|
+
const error = promiseResult.reason;
|
|
156
|
+
result.failed.push({
|
|
157
|
+
item: batch[i + idx],
|
|
158
|
+
error,
|
|
159
|
+
index
|
|
160
|
+
});
|
|
161
|
+
result.totals.failure++;
|
|
162
|
+
// If continueOnError is false, stop processing
|
|
163
|
+
if (!opts.continueOnError) {
|
|
164
|
+
throw new Error(`Operation failed at index ${index}: ${error.message || String(error)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
logger.error(`Error in batch processing`, {
|
|
173
|
+
batchSize: batch.length,
|
|
174
|
+
startIndex,
|
|
175
|
+
error: error instanceof Error ? error.message : String(error)
|
|
176
|
+
});
|
|
177
|
+
// If we've hit an error that stopped the whole batch (continueOnError=false),
|
|
178
|
+
// we need to record any unprocessed items as failures
|
|
179
|
+
const processedCount = result.totals.success + result.totals.failure;
|
|
180
|
+
if (processedCount < batch.length) {
|
|
181
|
+
const remainingItems = batch.slice(processedCount);
|
|
182
|
+
for (let i = 0; i < remainingItems.length; i++) {
|
|
183
|
+
const index = startIndex + processedCount + i;
|
|
184
|
+
result.failed.push({
|
|
185
|
+
item: remainingItems[i],
|
|
186
|
+
error: new Error('Batch processing aborted: ' +
|
|
187
|
+
(error instanceof Error ? error.message : String(error))),
|
|
188
|
+
index
|
|
189
|
+
});
|
|
190
|
+
result.totals.failure++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Process a single item with retry logic
|
|
198
|
+
*
|
|
199
|
+
* @param operation The operation to perform
|
|
200
|
+
* @param item The item being processed (for context)
|
|
201
|
+
* @param index The index of the item (for logging)
|
|
202
|
+
* @param options Processing options
|
|
203
|
+
* @returns The result of the operation if successful
|
|
204
|
+
* @throws Error if all retry attempts fail
|
|
205
|
+
*/
|
|
206
|
+
async function processWithRetry(operation, item, index, options) {
|
|
207
|
+
let attempts = 0;
|
|
208
|
+
let lastError = null;
|
|
209
|
+
while (attempts <= options.retryCount) {
|
|
210
|
+
try {
|
|
211
|
+
// Attempt the operation
|
|
212
|
+
attempts++;
|
|
213
|
+
return await operation();
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
217
|
+
lastError = err;
|
|
218
|
+
logger.warn(`Operation failed for item at index ${index}`, {
|
|
219
|
+
attempt: attempts,
|
|
220
|
+
maxAttempts: options.retryCount + 1,
|
|
221
|
+
error: err.message
|
|
222
|
+
});
|
|
223
|
+
// If this was our last attempt, don't delay, just throw
|
|
224
|
+
if (attempts > options.retryCount) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
// Calculate delay for next retry
|
|
228
|
+
let delay = options.retryDelay;
|
|
229
|
+
if (options.exponentialBackoff) {
|
|
230
|
+
// Use exponential backoff with jitter
|
|
231
|
+
delay = options.retryDelay * Math.pow(2, attempts - 1) + Math.random() * 1000;
|
|
232
|
+
}
|
|
233
|
+
logger.debug(`Retrying operation after delay`, {
|
|
234
|
+
index,
|
|
235
|
+
attempt: attempts,
|
|
236
|
+
delayMs: delay
|
|
237
|
+
});
|
|
238
|
+
// Wait before next attempt
|
|
239
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// If we get here, all retry attempts failed
|
|
243
|
+
throw new Error(`Operation failed after ${attempts} attempts for item at index ${index}: ` +
|
|
244
|
+
(lastError?.message || 'Unknown error'));
|
|
245
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for handling dates, timestamps, and due date parsing.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Get a timestamp for a relative time
|
|
8
|
+
*
|
|
9
|
+
* @param hours Hours from now
|
|
10
|
+
* @param days Days from now
|
|
11
|
+
* @param weeks Weeks from now
|
|
12
|
+
* @param months Months from now
|
|
13
|
+
* @returns Timestamp in milliseconds
|
|
14
|
+
*/
|
|
15
|
+
export function getRelativeTimestamp(hours = 0, days = 0, weeks = 0, months = 0) {
|
|
16
|
+
const now = new Date();
|
|
17
|
+
if (hours)
|
|
18
|
+
now.setHours(now.getHours() + hours);
|
|
19
|
+
if (days)
|
|
20
|
+
now.setDate(now.getDate() + days);
|
|
21
|
+
if (weeks)
|
|
22
|
+
now.setDate(now.getDate() + (weeks * 7));
|
|
23
|
+
if (months)
|
|
24
|
+
now.setMonth(now.getMonth() + months);
|
|
25
|
+
return now.getTime();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse a due date string into a timestamp
|
|
29
|
+
* Supports ISO 8601 format or natural language like "tomorrow"
|
|
30
|
+
*
|
|
31
|
+
* @param dateString Date string to parse
|
|
32
|
+
* @returns Timestamp in milliseconds or undefined if parsing fails
|
|
33
|
+
*/
|
|
34
|
+
export function parseDueDate(dateString) {
|
|
35
|
+
if (!dateString)
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
// Handle natural language dates
|
|
39
|
+
const lowerDate = dateString.toLowerCase();
|
|
40
|
+
const now = new Date();
|
|
41
|
+
if (lowerDate === 'today') {
|
|
42
|
+
const today = new Date();
|
|
43
|
+
today.setHours(23, 59, 59, 999);
|
|
44
|
+
return today.getTime();
|
|
45
|
+
}
|
|
46
|
+
// Handle relative dates with specific times
|
|
47
|
+
const relativeTimeRegex = /(?:(\d+)\s*(days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
|
|
48
|
+
const match = lowerDate.match(relativeTimeRegex);
|
|
49
|
+
if (match) {
|
|
50
|
+
const date = new Date();
|
|
51
|
+
const [_, amount, unit, hours, minutes, meridian] = match;
|
|
52
|
+
// Calculate the future date
|
|
53
|
+
if (amount && unit) {
|
|
54
|
+
const value = parseInt(amount);
|
|
55
|
+
if (unit.startsWith('day')) {
|
|
56
|
+
date.setDate(date.getDate() + value);
|
|
57
|
+
}
|
|
58
|
+
else if (unit.startsWith('week')) {
|
|
59
|
+
date.setDate(date.getDate() + (value * 7));
|
|
60
|
+
}
|
|
61
|
+
else if (unit.startsWith('month')) {
|
|
62
|
+
date.setMonth(date.getMonth() + value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (lowerDate.startsWith('tomorrow')) {
|
|
66
|
+
date.setDate(date.getDate() + 1);
|
|
67
|
+
}
|
|
68
|
+
else if (lowerDate.includes('next week')) {
|
|
69
|
+
date.setDate(date.getDate() + 7);
|
|
70
|
+
}
|
|
71
|
+
else if (lowerDate.includes('next month')) {
|
|
72
|
+
date.setMonth(date.getMonth() + 1);
|
|
73
|
+
}
|
|
74
|
+
// Set the time if specified
|
|
75
|
+
if (hours) {
|
|
76
|
+
let parsedHours = parseInt(hours);
|
|
77
|
+
const parsedMinutes = minutes ? parseInt(minutes) : 0;
|
|
78
|
+
// Convert to 24-hour format if meridian is specified
|
|
79
|
+
if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
|
|
80
|
+
parsedHours += 12;
|
|
81
|
+
if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
|
|
82
|
+
parsedHours = 0;
|
|
83
|
+
date.setHours(parsedHours, parsedMinutes, 0, 0);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Default to end of day if no time specified
|
|
87
|
+
date.setHours(23, 59, 59, 999);
|
|
88
|
+
}
|
|
89
|
+
return date.getTime();
|
|
90
|
+
}
|
|
91
|
+
// Handle hours from now
|
|
92
|
+
const hoursRegex = /(\d+)\s*hours?\s*from\s*now/i;
|
|
93
|
+
const daysRegex = /(\d+)\s*days?\s*from\s*now/i;
|
|
94
|
+
const weeksRegex = /(\d+)\s*weeks?\s*from\s*now/i;
|
|
95
|
+
const monthsRegex = /(\d+)\s*months?\s*from\s*now/i;
|
|
96
|
+
if (hoursRegex.test(lowerDate)) {
|
|
97
|
+
const hours = parseInt(lowerDate.match(hoursRegex)[1]);
|
|
98
|
+
return getRelativeTimestamp(hours);
|
|
99
|
+
}
|
|
100
|
+
if (daysRegex.test(lowerDate)) {
|
|
101
|
+
const days = parseInt(lowerDate.match(daysRegex)[1]);
|
|
102
|
+
return getRelativeTimestamp(0, days);
|
|
103
|
+
}
|
|
104
|
+
if (weeksRegex.test(lowerDate)) {
|
|
105
|
+
const weeks = parseInt(lowerDate.match(weeksRegex)[1]);
|
|
106
|
+
return getRelativeTimestamp(0, 0, weeks);
|
|
107
|
+
}
|
|
108
|
+
if (monthsRegex.test(lowerDate)) {
|
|
109
|
+
const months = parseInt(lowerDate.match(monthsRegex)[1]);
|
|
110
|
+
return getRelativeTimestamp(0, 0, 0, months);
|
|
111
|
+
}
|
|
112
|
+
// Try to parse as a date string
|
|
113
|
+
const date = new Date(dateString);
|
|
114
|
+
if (!isNaN(date.getTime())) {
|
|
115
|
+
return date.getTime();
|
|
116
|
+
}
|
|
117
|
+
// If all parsing fails, return undefined
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.warn(`Failed to parse due date: ${dateString}`, error);
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Format a due date timestamp into a human-readable string
|
|
127
|
+
*
|
|
128
|
+
* @param timestamp Unix timestamp in milliseconds
|
|
129
|
+
* @returns Formatted date string or undefined if timestamp is invalid
|
|
130
|
+
*/
|
|
131
|
+
export function formatDueDate(timestamp) {
|
|
132
|
+
if (!timestamp)
|
|
133
|
+
return undefined;
|
|
134
|
+
try {
|
|
135
|
+
const date = new Date(timestamp);
|
|
136
|
+
if (isNaN(date.getTime()))
|
|
137
|
+
return undefined;
|
|
138
|
+
// Format: "March 10, 2025 at 10:56 PM"
|
|
139
|
+
return date.toLocaleString('en-US', {
|
|
140
|
+
year: 'numeric',
|
|
141
|
+
month: 'long',
|
|
142
|
+
day: 'numeric',
|
|
143
|
+
hour: 'numeric',
|
|
144
|
+
minute: '2-digit',
|
|
145
|
+
hour12: true
|
|
146
|
+
}).replace(' at', ',');
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.warn(`Failed to format due date: ${timestamp}`, error);
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameter Processing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for processing MCP tool parameters, especially handling
|
|
5
|
+
* JSON string conversion for array and object parameters.
|
|
6
|
+
*/
|
|
7
|
+
import { Logger } from '../logger.js';
|
|
8
|
+
// Create a logger instance
|
|
9
|
+
const logger = new Logger('ParamsUtils');
|
|
10
|
+
/**
|
|
11
|
+
* Process parameters that might contain JSON strings
|
|
12
|
+
* Handles cases where the MCP protocol sends stringified JSON for array or object parameters
|
|
13
|
+
*
|
|
14
|
+
* @param params The raw parameters received from the MCP tool call
|
|
15
|
+
* @returns Processed parameters with JSON strings parsed to objects/arrays
|
|
16
|
+
*/
|
|
17
|
+
export function processParams(params) {
|
|
18
|
+
if (!params)
|
|
19
|
+
return {};
|
|
20
|
+
const result = { ...params };
|
|
21
|
+
// Process special parameters that could be JSON strings
|
|
22
|
+
const jsonFields = ['tasks', 'options'];
|
|
23
|
+
for (const field of jsonFields) {
|
|
24
|
+
if (typeof result[field] === 'string') {
|
|
25
|
+
try {
|
|
26
|
+
if ((result[field].startsWith('[') && result[field].endsWith(']')) ||
|
|
27
|
+
(result[field].startsWith('{') && result[field].endsWith('}'))) {
|
|
28
|
+
result[field] = JSON.parse(result[field]);
|
|
29
|
+
logger.debug(`Parsed JSON parameter: ${field}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
logger.error(`Failed to parse JSON for ${field}`, { error });
|
|
34
|
+
// Keep original string if parse fails
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for resolving entity IDs from names or other identifiers.
|
|
5
|
+
*/
|
|
6
|
+
import { clickUpServices } from '../services/shared.js';
|
|
7
|
+
import { findListIDByName } from '../tools/list.js';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a list ID from either a direct ID or list name
|
|
10
|
+
*
|
|
11
|
+
* @param listId Optional direct list ID
|
|
12
|
+
* @param listName Optional list name to resolve
|
|
13
|
+
* @param workspaceService Workspace service to use for lookup
|
|
14
|
+
* @returns Resolved list ID
|
|
15
|
+
* @throws Error if neither listId nor listName is provided, or if list name can't be resolved
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveListId(listId, listName, workspaceService = clickUpServices.workspace) {
|
|
18
|
+
// If list ID is directly provided, use it
|
|
19
|
+
if (listId) {
|
|
20
|
+
return listId;
|
|
21
|
+
}
|
|
22
|
+
// If list name is provided, find the corresponding ID
|
|
23
|
+
if (listName) {
|
|
24
|
+
const listInfo = await findListIDByName(workspaceService, listName);
|
|
25
|
+
if (!listInfo) {
|
|
26
|
+
throw new Error(`List "${listName}" not found`);
|
|
27
|
+
}
|
|
28
|
+
return listInfo.id;
|
|
29
|
+
}
|
|
30
|
+
// If neither is provided, throw an error
|
|
31
|
+
throw new Error("Either listId or listName must be provided");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a task ID from either a direct ID or task name + list info
|
|
35
|
+
*
|
|
36
|
+
* @param taskId Optional direct task ID
|
|
37
|
+
* @param taskName Optional task name to resolve
|
|
38
|
+
* @param listId Optional list ID for task lookup
|
|
39
|
+
* @param listName Optional list name for task lookup
|
|
40
|
+
* @param taskService Task service to use for lookup
|
|
41
|
+
* @returns Resolved task ID
|
|
42
|
+
* @throws Error if parameters are insufficient or task can't be found
|
|
43
|
+
*/
|
|
44
|
+
export async function resolveTaskId(taskId, taskName, listId, listName, taskService = clickUpServices.task) {
|
|
45
|
+
// If task ID is directly provided, use it
|
|
46
|
+
if (taskId) {
|
|
47
|
+
return taskId;
|
|
48
|
+
}
|
|
49
|
+
// If task name is provided, we need list info to find it
|
|
50
|
+
if (taskName) {
|
|
51
|
+
// We need either listId or listName to find a task by name
|
|
52
|
+
if (!listId && !listName) {
|
|
53
|
+
throw new Error(`List name or ID is required when using task name for task "${taskName}"`);
|
|
54
|
+
}
|
|
55
|
+
// Get list ID
|
|
56
|
+
const targetListId = await resolveListId(listId, listName);
|
|
57
|
+
// Find the task in the list
|
|
58
|
+
const foundTask = await taskService.findTaskByName(targetListId, taskName);
|
|
59
|
+
if (!foundTask) {
|
|
60
|
+
throw new Error(`Task "${taskName}" not found in list`);
|
|
61
|
+
}
|
|
62
|
+
return foundTask.id;
|
|
63
|
+
}
|
|
64
|
+
// If neither is provided, throw an error
|
|
65
|
+
throw new Error("Either taskId or taskName must be provided");
|
|
66
|
+
}
|