@sebastienheyd/clickup-mcp 1.6.1
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/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +184 -0
- package/dist/clickup-text.d.ts +83 -0
- package/dist/clickup-text.d.ts.map +1 -0
- package/dist/clickup-text.js +563 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +133 -0
- package/dist/resources/space-resources.d.ts +6 -0
- package/dist/resources/space-resources.d.ts.map +1 -0
- package/dist/resources/space-resources.js +95 -0
- package/dist/shared/config.d.ts +11 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +61 -0
- package/dist/shared/data-uri.d.ts +14 -0
- package/dist/shared/data-uri.d.ts.map +1 -0
- package/dist/shared/data-uri.js +34 -0
- package/dist/shared/image-processing.d.ts +13 -0
- package/dist/shared/image-processing.d.ts.map +1 -0
- package/dist/shared/image-processing.js +199 -0
- package/dist/shared/types.d.ts +21 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/utils.d.ts +95 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +630 -0
- package/dist/test-utils.d.ts +23 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +44 -0
- package/dist/tools/doc-tools.d.ts +4 -0
- package/dist/tools/doc-tools.d.ts.map +1 -0
- package/dist/tools/doc-tools.js +436 -0
- package/dist/tools/list-tools.d.ts +4 -0
- package/dist/tools/list-tools.d.ts.map +1 -0
- package/dist/tools/list-tools.js +175 -0
- package/dist/tools/search-tools.d.ts +3 -0
- package/dist/tools/search-tools.d.ts.map +1 -0
- package/dist/tools/search-tools.js +188 -0
- package/dist/tools/space-tools.d.ts +3 -0
- package/dist/tools/space-tools.d.ts.map +1 -0
- package/dist/tools/space-tools.js +144 -0
- package/dist/tools/task-tools.d.ts +8 -0
- package/dist/tools/task-tools.d.ts.map +1 -0
- package/dist/tools/task-tools.js +325 -0
- package/dist/tools/task-write-tools.d.ts +3 -0
- package/dist/tools/task-write-tools.d.ts.map +1 -0
- package/dist/tools/task-write-tools.js +575 -0
- package/dist/tools/time-tools.d.ts +4 -0
- package/dist/tools/time-tools.d.ts.map +1 -0
- package/dist/tools/time-tools.js +338 -0
- package/package.json +72 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerTaskToolsRead = registerTaskToolsRead;
|
|
4
|
+
exports.generateTaskMetadata = generateTaskMetadata;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const clickup_text_1 = require("../clickup-text");
|
|
7
|
+
const config_1 = require("../shared/config");
|
|
8
|
+
const utils_1 = require("../shared/utils");
|
|
9
|
+
const image_processing_1 = require("../shared/image-processing");
|
|
10
|
+
// Read-specific utility functions
|
|
11
|
+
function registerTaskToolsRead(server, userData) {
|
|
12
|
+
server.tool("getTaskById", [
|
|
13
|
+
"Get a ClickUp task with images and comments by ID.",
|
|
14
|
+
"Always use this URL when referencing tasks in conversations or sharing with others.",
|
|
15
|
+
"The response provides complete context including task details, comments, and status history."
|
|
16
|
+
].join("\n"), {
|
|
17
|
+
id: zod_1.z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.refine(val => (0, utils_1.isTaskId)(val) || (0, utils_1.isCustomTaskId)(val), {
|
|
21
|
+
message: "Must be an internal task ID (6-9 alphanumeric characters) or a custom task ID (e.g. SOI-4422)"
|
|
22
|
+
})
|
|
23
|
+
.describe(`The task ID: either an internal ID (6-9 alphanumeric characters like "869c4za0g") or a custom task ID (e.g. "SOI-4422"). Do not include prefixes like "#", "CU-" or URLs.`),
|
|
24
|
+
}, {
|
|
25
|
+
readOnlyHint: true
|
|
26
|
+
}, async ({ id }) => {
|
|
27
|
+
// Resolve custom task ID to internal ID if needed
|
|
28
|
+
const resolvedId = await (0, utils_1.resolveTaskId)(id);
|
|
29
|
+
// 1. Load base task content, comment events, and status change events in parallel
|
|
30
|
+
const [taskDetailContentBlocks, commentEvents, statusChangeEvents] = await Promise.all([
|
|
31
|
+
loadTaskContent(resolvedId), // Returns Promise<ContentBlock[]>
|
|
32
|
+
loadTaskComments(resolvedId), // Returns Promise<DatedContentEvent[]>
|
|
33
|
+
loadTimeInStatusHistory(resolvedId), // Returns Promise<DatedContentEvent[]>
|
|
34
|
+
]);
|
|
35
|
+
// 2. Combine comment and status change events
|
|
36
|
+
const allDatedEvents = [...commentEvents, ...statusChangeEvents];
|
|
37
|
+
// 3. Sort all dated events chronologically
|
|
38
|
+
allDatedEvents.sort((a, b) => {
|
|
39
|
+
const dateA = a.date ? parseInt(a.date) : 0;
|
|
40
|
+
const dateB = b.date ? parseInt(b.date) : 0;
|
|
41
|
+
return dateA - dateB;
|
|
42
|
+
});
|
|
43
|
+
// 4. Flatten sorted events into a single ContentBlock stream
|
|
44
|
+
let processedEventBlocks = [];
|
|
45
|
+
for (const event of allDatedEvents) {
|
|
46
|
+
processedEventBlocks.push(...event.contentBlocks);
|
|
47
|
+
}
|
|
48
|
+
// 5. Combine task details with processed event blocks
|
|
49
|
+
const allContentBlocks = [...taskDetailContentBlocks, ...processedEventBlocks];
|
|
50
|
+
// 6. Download images with smart size limiting
|
|
51
|
+
const limitedContent = await (0, image_processing_1.downloadImages)(allContentBlocks);
|
|
52
|
+
return {
|
|
53
|
+
content: limitedContent,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Fetch time entries for a specific task (all time, not date-limited for detail view)
|
|
59
|
+
*/
|
|
60
|
+
async function fetchTaskTimeEntries(taskId) {
|
|
61
|
+
try {
|
|
62
|
+
// Get all team members for assignee filter
|
|
63
|
+
const teamMembers = await (0, utils_1.getAllTeamMembers)();
|
|
64
|
+
const params = new URLSearchParams({
|
|
65
|
+
task_id: taskId,
|
|
66
|
+
include_location_names: 'true',
|
|
67
|
+
start_date: '0', // overwrite the default 30 days
|
|
68
|
+
});
|
|
69
|
+
if (teamMembers.length > 0) {
|
|
70
|
+
params.append('assignee', teamMembers.join(','));
|
|
71
|
+
}
|
|
72
|
+
const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries?${params}`, {
|
|
73
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
console.error(`Error fetching time entries for task ${taskId}: ${response.status} ${response.statusText}`);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const data = await response.json();
|
|
80
|
+
return data.data || [];
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error('Error fetching task time entries:', error);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function loadTaskContent(taskId) {
|
|
88
|
+
const response = await fetch(`https://api.clickup.com/api/v2/task/${taskId}?include_markdown_description=true&include_subtasks=true`, { headers: { Authorization: config_1.CONFIG.apiKey } });
|
|
89
|
+
const task = await response.json();
|
|
90
|
+
const [taskMetadata, content] = await Promise.all([
|
|
91
|
+
// Create the task metadata block using the helper functions
|
|
92
|
+
(async () => {
|
|
93
|
+
const timeEntries = await fetchTaskTimeEntries(task.id);
|
|
94
|
+
return await generateTaskMetadata(task, timeEntries, true);
|
|
95
|
+
})(),
|
|
96
|
+
// process markdown and download images
|
|
97
|
+
(0, clickup_text_1.convertMarkdownToToolCallResult)(task.markdown_description || "", task.attachments || []),
|
|
98
|
+
]);
|
|
99
|
+
return [taskMetadata, ...content];
|
|
100
|
+
}
|
|
101
|
+
async function loadTaskComments(id) {
|
|
102
|
+
const response = await fetch(`https://api.clickup.com/api/v2/task/${id}/comment?start_date=0`, // Ensure all comments are fetched
|
|
103
|
+
{ headers: { Authorization: config_1.CONFIG.apiKey } });
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
console.error(`Error fetching comments for task ${id}: ${response.status} ${response.statusText}`);
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const commentsData = await response.json();
|
|
109
|
+
if (!commentsData.comments || !Array.isArray(commentsData.comments)) {
|
|
110
|
+
console.error(`Unexpected comment data structure for task ${id}`);
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const commentEvents = await Promise.all(commentsData.comments.map(async (comment) => {
|
|
114
|
+
const headerBlock = {
|
|
115
|
+
type: "text",
|
|
116
|
+
text: `Comment by ${comment.user.username} on ${timestampToIso(comment.date)}:`,
|
|
117
|
+
};
|
|
118
|
+
const commentBodyBlocks = await (0, clickup_text_1.convertClickUpTextItemsToToolCallResult)(comment.comment);
|
|
119
|
+
return {
|
|
120
|
+
date: comment.date, // String timestamp from ClickUp for sorting
|
|
121
|
+
contentBlocks: [headerBlock, ...commentBodyBlocks],
|
|
122
|
+
};
|
|
123
|
+
}));
|
|
124
|
+
return commentEvents;
|
|
125
|
+
}
|
|
126
|
+
async function loadTimeInStatusHistory(taskId) {
|
|
127
|
+
const url = `https://api.clickup.com/api/v2/task/${taskId}/time_in_status`;
|
|
128
|
+
try {
|
|
129
|
+
const response = await fetch(url, { headers: { Authorization: config_1.CONFIG.apiKey } });
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
console.error(`Error fetching time in status for task ${taskId}: ${response.status} ${response.statusText}`);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
// Using 'any' for less strict typing as per user preference, but keeping structure for clarity
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
const events = [];
|
|
137
|
+
const processStatusEntry = (entry) => {
|
|
138
|
+
if (!entry || !entry.total_time || !entry.total_time.since || !entry.status)
|
|
139
|
+
return null;
|
|
140
|
+
return {
|
|
141
|
+
date: entry.total_time.since,
|
|
142
|
+
contentBlocks: [{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: `Status set to '${entry.status}' on ${timestampToIso(entry.total_time.since)}`,
|
|
145
|
+
}],
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
if (data.status_history && Array.isArray(data.status_history)) {
|
|
149
|
+
data.status_history.forEach((historyEntry) => {
|
|
150
|
+
const event = processStatusEntry(historyEntry);
|
|
151
|
+
if (event)
|
|
152
|
+
events.push(event);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (data.current_status) {
|
|
156
|
+
const event = processStatusEntry(data.current_status);
|
|
157
|
+
// Ensure current_status is only added if it's distinct or more recent than the last history item.
|
|
158
|
+
// The deduplication logic below handles if it's the same as the last history entry.
|
|
159
|
+
if (event)
|
|
160
|
+
events.push(event);
|
|
161
|
+
}
|
|
162
|
+
// Deduplicate events based on date and status name to avoid adding current_status if it's identical to the last history entry
|
|
163
|
+
const uniqueEvents = Array.from(new Map(events.map(event => {
|
|
164
|
+
const firstBlock = event.contentBlocks[0];
|
|
165
|
+
const textKey = firstBlock && 'text' in firstBlock ? firstBlock.text : 'unknown';
|
|
166
|
+
return [`${event.date}-${textKey}`, event];
|
|
167
|
+
})).values());
|
|
168
|
+
return uniqueEvents;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
console.error(`Exception fetching time in status for task ${taskId}:`, error);
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Formats timestamp to ISO string with local timezone (not UTC)
|
|
177
|
+
*/
|
|
178
|
+
function timestampToIso(timestamp) {
|
|
179
|
+
const date = new Date(+timestamp);
|
|
180
|
+
const year = date.getFullYear();
|
|
181
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
182
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
183
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
184
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
185
|
+
// Calculate timezone offset
|
|
186
|
+
const offset = date.getTimezoneOffset();
|
|
187
|
+
const offsetHours = Math.floor(Math.abs(offset) / 60);
|
|
188
|
+
const offsetMinutes = Math.abs(offset) % 60;
|
|
189
|
+
const sign = offset <= 0 ? '+' : '-';
|
|
190
|
+
const timezoneOffset = sign + String(offsetHours).padStart(2, '0') + ':' + String(offsetMinutes).padStart(2, '0');
|
|
191
|
+
return `${year}-${month}-${day}T${hours}:${minutes}${timezoneOffset}`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Helper function to filter and format time entries for a specific task
|
|
195
|
+
*/
|
|
196
|
+
function filterTaskTimeEntries(taskId, timeEntries) {
|
|
197
|
+
if (!timeEntries || timeEntries.length === 0) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
// Filter entries for this specific task
|
|
201
|
+
const taskEntries = timeEntries.filter((entry) => entry.task?.id === taskId);
|
|
202
|
+
if (taskEntries.length === 0) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
// Group time entries by user (same logic as original getTaskTimeEntries)
|
|
206
|
+
const timeByUser = new Map();
|
|
207
|
+
taskEntries.forEach((entry) => {
|
|
208
|
+
const username = entry.user?.username || 'Unknown User';
|
|
209
|
+
const currentTime = timeByUser.get(username) || 0;
|
|
210
|
+
const entryDurationMs = parseInt(entry.duration) || 0;
|
|
211
|
+
timeByUser.set(username, currentTime + entryDurationMs);
|
|
212
|
+
});
|
|
213
|
+
// Format results (same logic as original)
|
|
214
|
+
const userTimeEntries = [];
|
|
215
|
+
for (const [username, totalMs] of timeByUser.entries()) {
|
|
216
|
+
const hours = totalMs / (1000 * 60 * 60);
|
|
217
|
+
const displayHours = Math.floor(hours);
|
|
218
|
+
const displayMinutes = Math.round((hours - displayHours) * 60);
|
|
219
|
+
const timeDisplay = displayHours > 0 ?
|
|
220
|
+
`${displayHours}h ${displayMinutes}m` :
|
|
221
|
+
`${displayMinutes}m`;
|
|
222
|
+
userTimeEntries.push(`${username}: ${timeDisplay}`);
|
|
223
|
+
}
|
|
224
|
+
return userTimeEntries.length > 0 ? userTimeEntries.join(', ') : null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Helper function to generate consistent task metadata
|
|
228
|
+
*/
|
|
229
|
+
async function generateTaskMetadata(task, timeEntries, isDetailView = false) {
|
|
230
|
+
let spaceName = task.space?.name || 'Unknown Space';
|
|
231
|
+
let spaceIdForDisplay = task.space?.id || 'N/A';
|
|
232
|
+
if (spaceName === 'Unknown Space' && task.space?.id) {
|
|
233
|
+
const spaceDetails = await (0, utils_1.getSpaceDetails)(task.space.id);
|
|
234
|
+
if (spaceDetails && spaceDetails.name) {
|
|
235
|
+
spaceName = spaceDetails.name;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const metadataLines = [
|
|
239
|
+
`task_id: ${task.id}`,
|
|
240
|
+
`task_url: ${task.url}`,
|
|
241
|
+
`name: ${task.name}`,
|
|
242
|
+
`status: ${task.status.status}`,
|
|
243
|
+
`date_created: ${timestampToIso(task.date_created)}`,
|
|
244
|
+
`date_updated: ${timestampToIso(task.date_updated)}`,
|
|
245
|
+
`creator: ${task.creator.username} (${task.creator.id})`,
|
|
246
|
+
`assignee: ${task.assignees.map((a) => `${a.username} (${a.id})`).join(', ')}`,
|
|
247
|
+
`list: ${task.list.name} (${task.list.id})`,
|
|
248
|
+
`space: ${spaceName} (${spaceIdForDisplay})`,
|
|
249
|
+
];
|
|
250
|
+
// Add priority if it exists
|
|
251
|
+
if (task.priority !== undefined && task.priority !== null) {
|
|
252
|
+
const priorityName = task.priority.priority || 'none';
|
|
253
|
+
metadataLines.push(`priority: ${priorityName}`);
|
|
254
|
+
}
|
|
255
|
+
// Add due date if it exists
|
|
256
|
+
if (task.due_date) {
|
|
257
|
+
metadataLines.push(`due_date: ${timestampToIso(task.due_date)}`);
|
|
258
|
+
}
|
|
259
|
+
// Add start date if it exists
|
|
260
|
+
if (task.start_date) {
|
|
261
|
+
metadataLines.push(`start_date: ${timestampToIso(task.start_date)}`);
|
|
262
|
+
}
|
|
263
|
+
// Add time estimate if it exists
|
|
264
|
+
if (task.time_estimate) {
|
|
265
|
+
const hours = Math.floor(task.time_estimate / 3600000);
|
|
266
|
+
const minutes = Math.floor((task.time_estimate % 3600000) / 60000);
|
|
267
|
+
metadataLines.push(`time_estimate: ${hours}h ${minutes}m`);
|
|
268
|
+
}
|
|
269
|
+
// Add time booked (tracked time entries) - only if timeEntries provided
|
|
270
|
+
if (timeEntries) {
|
|
271
|
+
const timeBooked = filterTaskTimeEntries(task.id, timeEntries);
|
|
272
|
+
if (timeBooked) {
|
|
273
|
+
const disclaimer = isDetailView ? "" : " (last 30 days)";
|
|
274
|
+
metadataLines.push(`time_booked${disclaimer}: ${timeBooked}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Add tags if they exist
|
|
278
|
+
if (task.tags && task.tags.length > 0) {
|
|
279
|
+
metadataLines.push(`tags: ${task.tags.map((t) => t.name).join(', ')}`);
|
|
280
|
+
}
|
|
281
|
+
// Add watchers if they exist
|
|
282
|
+
if (task.watchers && task.watchers.length > 0) {
|
|
283
|
+
metadataLines.push(`watchers: ${task.watchers.map((w) => w.username).join(', ')}`);
|
|
284
|
+
}
|
|
285
|
+
// Add parent task information if it exists
|
|
286
|
+
if (typeof task.parent === "string") {
|
|
287
|
+
metadataLines.push(`parent_task_id: ${task.parent}`);
|
|
288
|
+
}
|
|
289
|
+
// Add child task information if it exists
|
|
290
|
+
if (task.subtasks && task.subtasks.length > 0) {
|
|
291
|
+
metadataLines.push(`child_task_ids: ${task.subtasks.map((st) => st.id).join(', ')}`);
|
|
292
|
+
}
|
|
293
|
+
// Add archived status if true
|
|
294
|
+
if (task.archived) {
|
|
295
|
+
metadataLines.push(`archived: true`);
|
|
296
|
+
}
|
|
297
|
+
// Add custom fields if they exist
|
|
298
|
+
if (task.custom_fields && task.custom_fields.length > 0) {
|
|
299
|
+
task.custom_fields.forEach((field) => {
|
|
300
|
+
if (field.value !== undefined && field.value !== null && field.value !== '') {
|
|
301
|
+
const fieldName = field.name.toLowerCase().replace(/\s+/g, '_');
|
|
302
|
+
let fieldValue = field.value;
|
|
303
|
+
// Handle different custom field types
|
|
304
|
+
if (field.type === 'drop_down' && typeof field.value === 'number') {
|
|
305
|
+
// For dropdown fields, find the selected option
|
|
306
|
+
const selectedOption = field.type_config?.options?.find((opt) => opt.orderindex === field.value);
|
|
307
|
+
fieldValue = selectedOption?.name || field.value;
|
|
308
|
+
}
|
|
309
|
+
else if (Array.isArray(field.value)) {
|
|
310
|
+
// For multi-select or array values
|
|
311
|
+
fieldValue = field.value.map((v) => v.name || v).join(', ');
|
|
312
|
+
}
|
|
313
|
+
else if (typeof field.value === 'object') {
|
|
314
|
+
// For object values (like users), extract meaningful data
|
|
315
|
+
fieldValue = field.value.username || field.value.name || JSON.stringify(field.value);
|
|
316
|
+
}
|
|
317
|
+
metadataLines.push(`custom_${fieldName}: ${fieldValue}`);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
type: "text",
|
|
323
|
+
text: metadataLines.join("\n"),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-write-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-write-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAcpE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAgatE"}
|