@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.
@@ -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
- * @param dateString Date string to parse
30
- * @returns Timestamp in milliseconds or undefined if parsing fails
31
- */
32
- export function parseDueDate(dateString) {
33
- if (!dateString)
34
- return undefined;
35
- try {
36
- // Handle natural language dates
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
+ }