@taazkareem/clickup-mcp-server 0.6.1 → 0.6.3

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.
@@ -11,7 +11,7 @@ import { toTaskPriority } from '../../services/clickup/types.js';
11
11
  import { clickUpServices } from '../../services/shared.js';
12
12
  import { BulkService } from '../../services/clickup/bulk.js';
13
13
  import { parseDueDate } from '../utils.js';
14
- import { validateTaskIdentification, validateListIdentification, validateTaskUpdateData, validateBulkTasks, parseBulkOptions, resolveTaskIdWithValidation, resolveListIdWithValidation } from './utilities.js';
14
+ import { validateTaskIdentification, validateListIdentification, validateTaskUpdateData, validateBulkTasks, parseBulkOptions, resolveListIdWithValidation } from './utilities.js';
15
15
  // Use shared services instance
16
16
  const { task: taskService, list: listService } = clickUpServices;
17
17
  // Create a bulk service instance that uses the task service
@@ -32,18 +32,49 @@ function buildUpdateData(params) {
32
32
  updateData.markdown_description = params.markdown_description;
33
33
  if (params.status !== undefined)
34
34
  updateData.status = params.status;
35
+ // Skip toTaskPriority conversion since we're handling priority in the main handler
35
36
  if (params.priority !== undefined)
36
- updateData.priority = toTaskPriority(params.priority);
37
- if (params.dueDate !== undefined)
37
+ updateData.priority = params.priority;
38
+ if (params.dueDate !== undefined) {
38
39
  updateData.due_date = parseDueDate(params.dueDate);
40
+ updateData.due_date_time = true;
41
+ }
42
+ if (params.startDate !== undefined) {
43
+ updateData.start_date = parseDueDate(params.startDate);
44
+ updateData.start_date_time = true;
45
+ }
46
+ // Handle custom fields if provided
47
+ if (params.custom_fields !== undefined) {
48
+ updateData.custom_fields = params.custom_fields;
49
+ }
39
50
  return updateData;
40
51
  }
41
52
  /**
42
- * Process a task identification validation, returning the task ID
53
+ * Resolves a task ID from various input formats
54
+ * Smart disambiguation is used for task name lookups
55
+ *
56
+ * @param taskId Direct task ID
57
+ * @param taskName Task name to search for
58
+ * @param listName List name for context
59
+ * @param customTaskId Custom task ID (prefixed format)
60
+ * @returns Resolved task ID
43
61
  */
44
- async function getTaskId(taskId, taskName, listName, customTaskId) {
45
- validateTaskIdentification(taskId, taskName, listName, customTaskId);
46
- return await resolveTaskIdWithValidation(taskId, taskName, listName, customTaskId);
62
+ export async function getTaskId(taskId, taskName, listName, customTaskId) {
63
+ validateTaskIdentification(taskId, taskName, listName, customTaskId, true);
64
+ const result = await taskService.findTasks({
65
+ taskId,
66
+ customTaskId,
67
+ taskName,
68
+ listName,
69
+ allowMultipleMatches: false,
70
+ useSmartDisambiguation: true,
71
+ includeFullDetails: false,
72
+ includeListContext: false
73
+ });
74
+ if (result && !Array.isArray(result)) {
75
+ return result.id;
76
+ }
77
+ throw new Error("Task not found");
47
78
  }
48
79
  /**
49
80
  * Process a list identification validation, returning the list ID
@@ -72,11 +103,12 @@ function buildTaskFilters(params) {
72
103
  }
73
104
  /**
74
105
  * Map tasks for bulk operations, resolving task IDs
106
+ * Uses smart disambiguation for tasks without list context
75
107
  */
76
108
  async function mapTaskIds(tasks) {
77
109
  return Promise.all(tasks.map(async (task) => {
78
110
  validateTaskIdentification(task.taskId, task.taskName, task.listName, task.customTaskId);
79
- return await resolveTaskIdWithValidation(task.taskId, task.taskName, task.listName, task.customTaskId);
111
+ return await getTaskId(task.taskId, task.taskName, task.listName, task.customTaskId);
80
112
  }));
81
113
  }
82
114
  //=============================================================================
@@ -86,30 +118,46 @@ async function mapTaskIds(tasks) {
86
118
  * Handler for creating a task
87
119
  */
88
120
  export async function createTaskHandler(params) {
89
- const { name, description, markdown_description, status, dueDate, parent, tags } = params;
121
+ const { name, description, markdown_description, status, dueDate, startDate, parent, tags, custom_fields, check_required_custom_fields } = params;
90
122
  if (!name)
91
123
  throw new Error("Task name is required");
92
124
  // Use our helper function to validate and convert priority
93
125
  const priority = toTaskPriority(params.priority);
94
126
  const listId = await getListId(params.listId, params.listName);
95
- return await taskService.createTask(listId, {
127
+ const taskData = {
96
128
  name,
97
129
  description,
98
130
  markdown_description,
99
131
  status,
100
132
  priority,
101
- due_date: dueDate ? parseDueDate(dueDate) : undefined,
102
133
  parent,
103
- tags
104
- });
134
+ tags,
135
+ custom_fields,
136
+ check_required_custom_fields
137
+ };
138
+ // Add due date if specified
139
+ if (dueDate) {
140
+ taskData.due_date = parseDueDate(dueDate);
141
+ taskData.due_date_time = true;
142
+ }
143
+ // Add start date if specified
144
+ if (startDate) {
145
+ taskData.start_date = parseDueDate(startDate);
146
+ taskData.start_date_time = true;
147
+ }
148
+ return await taskService.createTask(listId, taskData);
105
149
  }
106
150
  /**
107
151
  * Handler for updating a task
108
152
  */
109
153
  export async function updateTaskHandler(params) {
154
+ console.log('Update Task Handler - Raw params:', JSON.stringify(params));
155
+ console.log('Update Task Handler - Priority type:', typeof params.priority, 'Value:', params.priority);
110
156
  validateTaskUpdateData(params);
111
157
  const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
112
- return await taskService.updateTask(taskId, buildUpdateData(params));
158
+ const updateData = buildUpdateData(params);
159
+ console.log('Update Task Handler - Update data:', JSON.stringify(updateData));
160
+ return await taskService.updateTask(taskId, updateData);
113
161
  }
114
162
  /**
115
163
  * Handler for moving a task
@@ -134,15 +182,107 @@ export async function duplicateTaskHandler(params) {
134
182
  * Handler for getting a task
135
183
  */
136
184
  export async function getTaskHandler(params) {
137
- // resolveTaskIdWithValidation now auto-detects whether taskId is a regular ID or custom ID
138
- const taskId = await getTaskId(params.taskId, params.taskName, params.listName, params.customTaskId);
139
- // If subtasks parameter is provided and true, use the getTaskWithSubtasks method
140
- if (params.subtasks) {
141
- const task = await taskService.getTask(taskId);
142
- const subtasks = await taskService.getSubtasks(taskId);
143
- return { ...task, subtasks };
185
+ try {
186
+ // Direct path for taskId - most efficient
187
+ if (params.taskId) {
188
+ const task = await taskService.getTask(params.taskId);
189
+ // Add subtasks if requested
190
+ if (params.subtasks) {
191
+ const subtasks = await taskService.getSubtasks(task.id);
192
+ return { ...task, subtasks };
193
+ }
194
+ return task;
195
+ }
196
+ // Direct path for customTaskId - also efficient
197
+ if (params.customTaskId) {
198
+ const task = await taskService.getTaskByCustomId(params.customTaskId);
199
+ // Add subtasks if requested
200
+ if (params.subtasks) {
201
+ const subtasks = await taskService.getSubtasks(task.id);
202
+ return { ...task, subtasks };
203
+ }
204
+ return task;
205
+ }
206
+ // Special optimized path for taskName + listName combination
207
+ if (params.taskName && params.listName) {
208
+ // First, get the list ID
209
+ const listId = await getListId(null, params.listName);
210
+ if (!listId) {
211
+ throw new Error(`List "${params.listName}" not found`);
212
+ }
213
+ // Use the ClickUp API to get filtered tasks
214
+ // Need to get all tasks and filter on client side
215
+ // This is more efficient than the original approach because it's a dedicated path
216
+ // that skips the global lookup framework entirely
217
+ const allTasks = await taskService.getTasks(listId);
218
+ // Find the matching task
219
+ // Extract this to avoid dependency on internal isNameMatch implementation
220
+ const matchingTask = findTaskByName(allTasks, params.taskName);
221
+ if (!matchingTask) {
222
+ throw new Error(`Task "${params.taskName}" not found in list "${params.listName}"`);
223
+ }
224
+ // Add subtasks if requested
225
+ if (params.subtasks) {
226
+ const subtasks = await taskService.getSubtasks(matchingTask.id);
227
+ return { ...matchingTask, subtasks };
228
+ }
229
+ return matchingTask;
230
+ }
231
+ // Fallback to the original global lookup for all other cases
232
+ const result = await taskService.findTasks({
233
+ taskName: params.taskName,
234
+ allowMultipleMatches: true,
235
+ useSmartDisambiguation: false,
236
+ includeFullDetails: true,
237
+ includeListContext: true
238
+ });
239
+ // Handle the response based on the result type
240
+ if (Array.isArray(result)) {
241
+ // If multiple tasks matched, format them with task count
242
+ return {
243
+ matches: result,
244
+ count: result.length
245
+ };
246
+ }
247
+ else if (result) {
248
+ // Single task found, check if we need to include subtasks
249
+ if (params.subtasks) {
250
+ const subtasks = await taskService.getSubtasks(result.id);
251
+ return { ...result, subtasks };
252
+ }
253
+ // Return the single task
254
+ return result;
255
+ }
256
+ else {
257
+ throw new Error("Task not found");
258
+ }
259
+ }
260
+ catch (error) {
261
+ // Enhance error message for non-existent tasks
262
+ if (params.taskName && error.message.includes('not found')) {
263
+ throw new Error(`Task "${params.taskName}" not found. Please check the task name and try again.`);
264
+ }
265
+ // Pass along other formatted errors
266
+ throw error;
144
267
  }
145
- return await taskService.getTask(taskId);
268
+ }
269
+ /**
270
+ * Helper function to find a task by name in an array of tasks
271
+ */
272
+ function findTaskByName(tasks, name) {
273
+ if (!tasks || !Array.isArray(tasks) || !name)
274
+ return null;
275
+ // Try exact match first
276
+ let match = tasks.find(task => task.name === name);
277
+ if (match)
278
+ return match;
279
+ // Try case-insensitive match
280
+ match = tasks.find(task => task.name.toLowerCase() === name.toLowerCase());
281
+ if (match)
282
+ return match;
283
+ // Try fuzzy match - looking for name as substring
284
+ match = tasks.find(task => task.name.toLowerCase().includes(name.toLowerCase()));
285
+ return match || null;
146
286
  }
147
287
  /**
148
288
  * Handler for getting tasks
@@ -212,7 +352,8 @@ export async function getWorkspaceTasksHandler(taskService, params) {
212
352
  if (!hasFilter) {
213
353
  throw new Error('At least one filter parameter is required (tags, list_ids, folder_ids, space_ids, statuses, assignees, or date filters)');
214
354
  }
215
- // Create filter object from parameters
355
+ // For workspace tasks, we'll continue to use the direct getWorkspaceTasks method
356
+ // since it supports specific workspace-wide filters that aren't part of the unified findTasks
216
357
  const filters = {
217
358
  tags: params.tags,
218
359
  list_ids: params.list_ids,
@@ -260,6 +401,10 @@ export async function createBulkTasksHandler(params) {
260
401
  processedTask.due_date = parseDueDate(task.dueDate);
261
402
  delete processedTask.dueDate;
262
403
  }
404
+ // Make sure custom_fields is preserved in the processed task
405
+ if (task.custom_fields) {
406
+ processedTask.custom_fields = task.custom_fields;
407
+ }
263
408
  return processedTask;
264
409
  });
265
410
  const result = await bulkService.createTasks(listId, tasks, parseBulkOptions(params.options));
@@ -288,7 +433,7 @@ export async function moveBulkTasksHandler(params) {
288
433
  }
289
434
  const targetListId = await getListId(params.targetListId, params.targetListName);
290
435
  const taskIds = await mapTaskIds(params.tasks);
291
- const result = await bulkService.moveTasks(taskIds, targetListId, parseBulkOptions(params.options));
436
+ const result = await bulkService.moveTasks(taskIds.map(taskId => ({ taskId })), targetListId, parseBulkOptions(params.options));
292
437
  return result.successful;
293
438
  }
294
439
  /**
@@ -25,4 +25,4 @@ createBulkTasksHandler, updateBulkTasksHandler, moveBulkTasksHandler, deleteBulk
25
25
  // Team task operation handlers
26
26
  getWorkspaceTasksHandler } from './handlers.js';
27
27
  // Re-export utilities
28
- export { formatTaskData, validateTaskIdentification, validateListIdentification, validateTaskUpdateData, validateBulkTasks, parseBulkOptions, resolveTaskIdWithValidation, resolveListIdWithValidation } from './utilities.js';
28
+ export { formatTaskData, validateTaskIdentification, validateListIdentification, validateTaskUpdateData, validateBulkTasks, parseBulkOptions, resolveListIdWithValidation } from './utilities.js';
@@ -43,7 +43,42 @@ export const handleGetTasks = createHandlerWrapper(getTasksHandler, (tasks) => (
43
43
  tasks,
44
44
  count: tasks.length
45
45
  }));
46
- export const handleUpdateTask = createHandlerWrapper(updateTaskHandler);
46
+ export const handleUpdateTask = async (parameters) => {
47
+ try {
48
+ // Special handling for priority parameter
49
+ if (parameters.priority !== undefined) {
50
+ // Ensure priority is converted to a number if it's a valid value
51
+ if (parameters.priority === null) {
52
+ // null is valid for clearing priority
53
+ }
54
+ else if (typeof parameters.priority === 'number' && [1, 2, 3, 4].includes(parameters.priority)) {
55
+ // Valid priority number, keep as is
56
+ }
57
+ else if (typeof parameters.priority === 'string') {
58
+ // Try to convert string to number
59
+ const numPriority = parseInt(parameters.priority, 10);
60
+ if (!isNaN(numPriority) && [1, 2, 3, 4].includes(numPriority)) {
61
+ parameters.priority = numPriority;
62
+ }
63
+ else if (parameters.priority === 'null') {
64
+ parameters.priority = null;
65
+ }
66
+ else {
67
+ throw new Error(`Invalid priority value: ${parameters.priority}. Must be 1, 2, 3, 4, or null.`);
68
+ }
69
+ }
70
+ else {
71
+ throw new Error(`Invalid priority value: ${parameters.priority}. Must be 1, 2, 3, 4, or null.`);
72
+ }
73
+ }
74
+ // Proceed with normal handling
75
+ const result = await updateTaskHandler(parameters);
76
+ return sponsorService.createResponse(result, true);
77
+ }
78
+ catch (error) {
79
+ return sponsorService.createErrorResponse(error, parameters);
80
+ }
81
+ };
47
82
  export const handleMoveTask = createHandlerWrapper(moveTaskHandler);
48
83
  export const handleDuplicateTask = createHandlerWrapper(duplicateTaskHandler);
49
84
  export const handleDeleteTask = createHandlerWrapper(deleteTaskHandler, () => ({
@@ -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
  }
@@ -128,7 +154,8 @@ Requirements:
128
154
 
129
155
  Notes:
130
156
  - Only specified fields will be updated
131
- - Using taskId is more reliable than taskName`,
157
+ - Using taskId is more reliable than taskName
158
+ - You can set custom field values using the custom_fields parameter (array of {id, value} objects)`,
132
159
  inputSchema: {
133
160
  type: "object",
134
161
  properties: {
@@ -168,9 +195,29 @@ Notes:
168
195
  dueDate: {
169
196
  type: "string",
170
197
  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'."
198
+ },
199
+ startDate: {
200
+ type: "string",
201
+ description: "New start date. Supports both Unix timestamps (in milliseconds) and natural language expressions."
202
+ },
203
+ custom_fields: {
204
+ type: "array",
205
+ items: {
206
+ type: "object",
207
+ properties: {
208
+ id: {
209
+ type: "string",
210
+ description: "ID of the custom field"
211
+ },
212
+ value: {
213
+ description: "Value for the custom field. Type depends on the field type."
214
+ }
215
+ },
216
+ required: ["id", "value"]
217
+ },
218
+ description: "Optional array of custom field values to set on the task. Each object must have an 'id' and 'value' property."
171
219
  }
172
- },
173
- required: []
220
+ }
174
221
  }
175
222
  };
176
223
  /**
@@ -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,10 +96,25 @@ 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
120
  * Validate bulk operation tasks array
@@ -142,69 +160,6 @@ export function isCustomTaskId(id) {
142
160
  const customIdPattern = /^[A-Z]+-\d+$/;
143
161
  return customIdPattern.test(id);
144
162
  }
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
163
  /**
209
164
  * Resolves a list ID from either direct ID or name
210
165
  * Handles validation and throws appropriate errors
@@ -216,9 +171,7 @@ export async function resolveListIdWithValidation(listId, listName) {
216
171
  if (listId)
217
172
  return listId;
218
173
  // 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');
174
+ const listInfo = await findListIDByName(workspaceService, listName);
222
175
  if (!listInfo) {
223
176
  throw new Error(`List "${listName}" not found`);
224
177
  }
@@ -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';