@hauptsache.net/clickup-mcp 1.0.4 → 1.1.0
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 +174 -31
- package/dist/cli.js +6 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -408
- package/dist/shared/config.d.ts +10 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +31 -0
- package/dist/shared/types.d.ts +13 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/utils.d.ts +30 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +185 -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 +278 -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 +182 -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 +61 -0
- package/dist/tools/task-tools.d.ts +9 -0
- package/dist/tools/task-tools.d.ts.map +1 -0
- package/dist/tools/task-tools.js +610 -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 +251 -0
- package/package.json +1 -1
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerTaskToolsRead = registerTaskToolsRead;
|
|
4
|
+
exports.registerTaskToolsWrite = registerTaskToolsWrite;
|
|
5
|
+
exports.generateTaskMetadata = generateTaskMetadata;
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
const clickup_text_1 = require("../clickup-text");
|
|
8
|
+
const config_1 = require("../shared/config");
|
|
9
|
+
const utils_1 = require("../shared/utils");
|
|
10
|
+
// Shared schemas for task parameters
|
|
11
|
+
const taskNameSchema = zod_1.z.string().min(1).describe("The name/title of the task");
|
|
12
|
+
const taskDescriptionSchema = zod_1.z.string().optional().describe("Optional description for the task");
|
|
13
|
+
const taskPrioritySchema = zod_1.z.enum(["urgent", "high", "normal", "low"]).optional().describe("Optional priority level");
|
|
14
|
+
const taskDueDateSchema = zod_1.z.string().optional().describe("Optional due date as ISO date string (e.g., '2024-10-06T23:59:59+02:00')");
|
|
15
|
+
const taskStartDateSchema = zod_1.z.string().optional().describe("Optional start date as ISO date string (e.g., '2024-10-06T09:00:00+02:00')");
|
|
16
|
+
const taskTimeEstimateSchema = zod_1.z.number().optional().describe("Optional time estimate in hours (will be converted to milliseconds)");
|
|
17
|
+
const taskTagsSchema = zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of tag names");
|
|
18
|
+
// Shared utility functions
|
|
19
|
+
function createAssigneeDescription(userData) {
|
|
20
|
+
const user = userData.user;
|
|
21
|
+
return `Optional array of user IDs to assign to the task (defaults to current user: ${user.username} - ${user.email} (${user.id}))`;
|
|
22
|
+
}
|
|
23
|
+
function convertPriorityToNumber(priority) {
|
|
24
|
+
switch (priority) {
|
|
25
|
+
case "urgent": return 1;
|
|
26
|
+
case "high": return 2;
|
|
27
|
+
case "normal": return 3;
|
|
28
|
+
case "low": return 4;
|
|
29
|
+
default: return 3;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function convertPriorityToString(priority) {
|
|
33
|
+
const priorityMap = { 1: 'urgent', 2: 'high', 3: 'normal', 4: 'low' };
|
|
34
|
+
return priorityMap[priority] || 'unknown';
|
|
35
|
+
}
|
|
36
|
+
function formatTimeEstimate(hours) {
|
|
37
|
+
const displayHours = Math.floor(hours);
|
|
38
|
+
const displayMinutes = Math.round((hours - displayHours) * 60);
|
|
39
|
+
return displayHours > 0 ? `${displayHours}h ${displayMinutes}m` : `${displayMinutes}m`;
|
|
40
|
+
}
|
|
41
|
+
function buildTaskRequestBody(params, currentUserId) {
|
|
42
|
+
const requestBody = {};
|
|
43
|
+
if (params.name !== undefined) {
|
|
44
|
+
requestBody.name = params.name;
|
|
45
|
+
}
|
|
46
|
+
if (params.priority !== undefined) {
|
|
47
|
+
requestBody.priority = convertPriorityToNumber(params.priority);
|
|
48
|
+
}
|
|
49
|
+
if (params.due_date !== undefined) {
|
|
50
|
+
requestBody.due_date = new Date(params.due_date).getTime();
|
|
51
|
+
}
|
|
52
|
+
if (params.start_date !== undefined) {
|
|
53
|
+
requestBody.start_date = new Date(params.start_date).getTime();
|
|
54
|
+
}
|
|
55
|
+
if (params.time_estimate !== undefined) {
|
|
56
|
+
requestBody.time_estimate = Math.round(params.time_estimate * 60 * 60 * 1000);
|
|
57
|
+
}
|
|
58
|
+
if (params.tags !== undefined && params.tags.length > 0) {
|
|
59
|
+
requestBody.tags = params.tags;
|
|
60
|
+
}
|
|
61
|
+
if (params.assignees !== undefined) {
|
|
62
|
+
requestBody.assignees = params.assignees;
|
|
63
|
+
}
|
|
64
|
+
else if (currentUserId) {
|
|
65
|
+
requestBody.assignees = [currentUserId];
|
|
66
|
+
}
|
|
67
|
+
if (params.parent !== undefined) {
|
|
68
|
+
requestBody.parent = params.parent;
|
|
69
|
+
}
|
|
70
|
+
return requestBody;
|
|
71
|
+
}
|
|
72
|
+
function formatTaskResponse(task, operation, params, userData) {
|
|
73
|
+
const responseLines = [
|
|
74
|
+
`Task ${operation} successfully!`,
|
|
75
|
+
`task_id: ${task.id}`,
|
|
76
|
+
`name: ${task.name}`,
|
|
77
|
+
...(operation === 'created' ? [`url: ${task.url}`] : []),
|
|
78
|
+
`status: ${task.status?.status || 'Unknown'}`,
|
|
79
|
+
`assignees: ${task.assignees?.map((a) => `${a.username} (${a.id})`).join(', ') || 'None'}`,
|
|
80
|
+
...(operation === 'created' && params.list_id ? [`list_id: ${params.list_id}`] : []),
|
|
81
|
+
...(operation === 'updated' ? [
|
|
82
|
+
`updated_by: ${userData.user.username}`,
|
|
83
|
+
`updated_at: ${timestampToIso(Date.now())}`
|
|
84
|
+
] : [])
|
|
85
|
+
];
|
|
86
|
+
if (params.priority !== undefined || task.priority) {
|
|
87
|
+
const priority = task.priority ? convertPriorityToString(task.priority.priority) :
|
|
88
|
+
params.priority ? params.priority : 'unknown';
|
|
89
|
+
responseLines.push(`priority: ${priority}`);
|
|
90
|
+
}
|
|
91
|
+
if (params.due_date !== undefined) {
|
|
92
|
+
responseLines.push(`due_date: ${params.due_date}`);
|
|
93
|
+
}
|
|
94
|
+
if (params.start_date !== undefined) {
|
|
95
|
+
responseLines.push(`start_date: ${params.start_date}`);
|
|
96
|
+
}
|
|
97
|
+
if (params.time_estimate !== undefined) {
|
|
98
|
+
responseLines.push(`time_estimate: ${formatTimeEstimate(params.time_estimate)}`);
|
|
99
|
+
}
|
|
100
|
+
if (params.tags !== undefined && params.tags.length > 0) {
|
|
101
|
+
responseLines.push(`tags: ${params.tags.join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
if (params.parent !== undefined) {
|
|
104
|
+
responseLines.push(`parent_task: ${params.parent}`);
|
|
105
|
+
}
|
|
106
|
+
return responseLines;
|
|
107
|
+
}
|
|
108
|
+
function registerTaskToolsRead(server, userData) {
|
|
109
|
+
server.tool("getTaskById", "Get a Clickup task with images and comments by ID", {
|
|
110
|
+
id: zod_1.z
|
|
111
|
+
.string()
|
|
112
|
+
.min(6)
|
|
113
|
+
.max(9)
|
|
114
|
+
.refine(val => (0, utils_1.isTaskId)(val), {
|
|
115
|
+
message: "Task ID must be 6-9 alphanumeric characters only"
|
|
116
|
+
})
|
|
117
|
+
.describe(`The 6-9 character ID of the task to get without a prefix like "#", "CU-" or "https://app.clickup.com/t/"`),
|
|
118
|
+
}, async ({ id }) => {
|
|
119
|
+
// 1. Load base task content, comment events, and status change events in parallel
|
|
120
|
+
const [taskDetailContentBlocks, commentEvents, statusChangeEvents] = await Promise.all([
|
|
121
|
+
loadTaskContent(id), // Returns Promise<ContentBlock[]>
|
|
122
|
+
loadTaskComments(id), // Returns Promise<DatedContentEvent[]>
|
|
123
|
+
loadTimeInStatusHistory(id), // Returns Promise<DatedContentEvent[]>
|
|
124
|
+
]);
|
|
125
|
+
// 2. Combine comment and status change events
|
|
126
|
+
const allDatedEvents = [...commentEvents, ...statusChangeEvents];
|
|
127
|
+
// 3. Sort all dated events chronologically
|
|
128
|
+
allDatedEvents.sort((a, b) => {
|
|
129
|
+
const dateA = a.date ? parseInt(a.date) : 0;
|
|
130
|
+
const dateB = b.date ? parseInt(b.date) : 0;
|
|
131
|
+
return dateA - dateB;
|
|
132
|
+
});
|
|
133
|
+
// 4. Flatten sorted events into a single ContentBlock stream
|
|
134
|
+
let processedEventBlocks = [];
|
|
135
|
+
for (const event of allDatedEvents) {
|
|
136
|
+
processedEventBlocks.push(...event.contentBlocks);
|
|
137
|
+
}
|
|
138
|
+
// 5. Combine task details with processed event blocks
|
|
139
|
+
const allContentBlocks = [...taskDetailContentBlocks, ...processedEventBlocks];
|
|
140
|
+
// 6. Limit images
|
|
141
|
+
const limitedContent = (0, utils_1.limitImages)(allContentBlocks, config_1.CONFIG.maxImages);
|
|
142
|
+
return {
|
|
143
|
+
content: limitedContent,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function registerTaskToolsWrite(server, userData) {
|
|
148
|
+
server.tool("addComment", [
|
|
149
|
+
"Adds a comment to a specific task.",
|
|
150
|
+
"BEST PRACTICES: Include progress information and current status updates.",
|
|
151
|
+
"If external links are provided, verify they are publicly accessible and incorporate relevant information from those links into the comment.",
|
|
152
|
+
"Check the task's current status - if it's in 'backlog' or similar inactive states, suggest moving it to an active status like 'in progress' when work is being done."
|
|
153
|
+
].join("\n"), {
|
|
154
|
+
task_id: zod_1.z.string().min(6).max(9).describe("The 6-9 character task ID to comment on"),
|
|
155
|
+
comment: zod_1.z.string().min(1).describe("The comment text to add to the task"),
|
|
156
|
+
}, async ({ task_id, comment }) => {
|
|
157
|
+
try {
|
|
158
|
+
const requestBody = {
|
|
159
|
+
comment_text: comment,
|
|
160
|
+
notify_all: true
|
|
161
|
+
};
|
|
162
|
+
const response = await fetch(`https://api.clickup.com/api/v2/task/${task_id}/comment`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: {
|
|
165
|
+
Authorization: config_1.CONFIG.apiKey,
|
|
166
|
+
'Content-Type': 'application/json'
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify(requestBody)
|
|
169
|
+
});
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
const errorData = await response.json().catch(() => ({}));
|
|
172
|
+
throw new Error(`Error adding comment: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
173
|
+
}
|
|
174
|
+
const commentData = await response.json();
|
|
175
|
+
return {
|
|
176
|
+
content: [
|
|
177
|
+
{
|
|
178
|
+
type: "text",
|
|
179
|
+
text: [
|
|
180
|
+
`Comment added successfully!`,
|
|
181
|
+
`comment_id: ${commentData.id || 'N/A'}`,
|
|
182
|
+
`task_id: ${task_id}`,
|
|
183
|
+
`comment: ${comment}`,
|
|
184
|
+
`date: ${timestampToIso(commentData.date || Date.now())}`,
|
|
185
|
+
`user: ${commentData.user?.username || 'Current user'}`,
|
|
186
|
+
].join('\n')
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
console.error('Error adding comment:', error);
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: `Error adding comment: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
server.tool("updateTask", [
|
|
204
|
+
"Updates various aspects of an existing task.",
|
|
205
|
+
"Use getListInfo first to see valid status options.",
|
|
206
|
+
"SAFETY FEATURE: Description updates are APPEND-ONLY to prevent data loss - existing content is preserved.",
|
|
207
|
+
"IMPORTANT: When updating tasks (especially when booking time or adding progress), ensure the status makes sense for the work being done - tasks in 'backlog' or 'closed' states usually shouldn't have active work.",
|
|
208
|
+
"Suggest appropriate status transitions."
|
|
209
|
+
].join("\n"), {
|
|
210
|
+
task_id: zod_1.z.string().min(6).max(9).describe("The 6-9 character task ID to update"),
|
|
211
|
+
name: taskNameSchema.optional(),
|
|
212
|
+
append_description: zod_1.z.string().optional().describe("Optional markdown content to APPEND to existing task description (preserves existing content for safety)"),
|
|
213
|
+
status: zod_1.z.string().optional().describe("Optional new status name - use getListInfo to see valid options"),
|
|
214
|
+
priority: taskPrioritySchema,
|
|
215
|
+
due_date: taskDueDateSchema,
|
|
216
|
+
start_date: taskStartDateSchema,
|
|
217
|
+
time_estimate: taskTimeEstimateSchema,
|
|
218
|
+
tags: taskTagsSchema.describe("Optional array of tag names (will replace existing tags)"),
|
|
219
|
+
assignees: zod_1.z.array(zod_1.z.string()).optional().describe(createAssigneeDescription(userData))
|
|
220
|
+
}, async ({ task_id, name, append_description, status, priority, due_date, start_date, time_estimate, tags, assignees }) => {
|
|
221
|
+
try {
|
|
222
|
+
const userData = await (0, utils_1.getCurrentUser)();
|
|
223
|
+
// Get task details including current markdown description
|
|
224
|
+
const taskResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}?include_markdown_description=true`, {
|
|
225
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
226
|
+
});
|
|
227
|
+
if (!taskResponse.ok) {
|
|
228
|
+
throw new Error(`Error fetching task: ${taskResponse.status} ${taskResponse.statusText}`);
|
|
229
|
+
}
|
|
230
|
+
const taskData = await taskResponse.json();
|
|
231
|
+
// Handle append-only description update with markdown support
|
|
232
|
+
let finalDescription;
|
|
233
|
+
if (append_description) {
|
|
234
|
+
const currentDescription = taskData.markdown_description || "";
|
|
235
|
+
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
236
|
+
const separator = currentDescription.trim() ? "\n\n---\n" : "";
|
|
237
|
+
finalDescription = currentDescription + separator + `**Edit (${timestamp}):** ${append_description}`;
|
|
238
|
+
}
|
|
239
|
+
// Build update body using shared utility (without description since we handle it separately)
|
|
240
|
+
const updateBody = buildTaskRequestBody({
|
|
241
|
+
name, priority, due_date, start_date, time_estimate, tags, assignees
|
|
242
|
+
});
|
|
243
|
+
// Add markdown description if we have content to append
|
|
244
|
+
if (finalDescription !== undefined) {
|
|
245
|
+
updateBody.markdown_description = finalDescription;
|
|
246
|
+
}
|
|
247
|
+
// Add status field (not handled by buildTaskRequestBody since it's update-specific)
|
|
248
|
+
if (status !== undefined) {
|
|
249
|
+
updateBody.status = status;
|
|
250
|
+
}
|
|
251
|
+
// Handle assignees for updates (different from creates)
|
|
252
|
+
if (assignees !== undefined) {
|
|
253
|
+
updateBody.assignees = { add: assignees, rem: [] }; // Add new assignees, remove none
|
|
254
|
+
}
|
|
255
|
+
// Check if there's anything to update
|
|
256
|
+
if (Object.keys(updateBody).length === 0) {
|
|
257
|
+
return {
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: "No updates provided. Please specify at least one field to update.",
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// Update the task
|
|
267
|
+
const updateResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}`, {
|
|
268
|
+
method: 'PUT',
|
|
269
|
+
headers: {
|
|
270
|
+
Authorization: config_1.CONFIG.apiKey,
|
|
271
|
+
'Content-Type': 'application/json'
|
|
272
|
+
},
|
|
273
|
+
body: JSON.stringify(updateBody)
|
|
274
|
+
});
|
|
275
|
+
if (!updateResponse.ok) {
|
|
276
|
+
const errorData = await updateResponse.json().catch(() => ({}));
|
|
277
|
+
throw new Error(`Error updating task: ${updateResponse.status} ${updateResponse.statusText} - ${JSON.stringify(errorData)}`);
|
|
278
|
+
}
|
|
279
|
+
const updatedTask = await updateResponse.json();
|
|
280
|
+
const responseLines = formatTaskResponse(updatedTask, 'updated', {
|
|
281
|
+
name, append_description, status, priority, due_date, start_date, time_estimate, tags, assignees
|
|
282
|
+
}, userData);
|
|
283
|
+
return {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: "text",
|
|
287
|
+
text: responseLines.join('\n')
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
console.error('Error updating task:', error);
|
|
294
|
+
return {
|
|
295
|
+
content: [
|
|
296
|
+
{
|
|
297
|
+
type: "text",
|
|
298
|
+
text: `Error updating task: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
server.tool("createTask", [
|
|
305
|
+
"Creates a new task in a specific list and assigns it to specified users (defaults to current user).",
|
|
306
|
+
"IMPORTANT: Before creating, always search for similar existing tasks first using searchTasks to avoid duplicates - ask the user if they want to use an existing task instead.",
|
|
307
|
+
"If related tasks exist, reference them with links in the description (format: https://app.clickup.com/t/TASK_ID).",
|
|
308
|
+
"Use getListInfo first to understand the list context and available statuses.",
|
|
309
|
+
"Task descriptions support full markdown formatting including **bold**, *italic*, lists, links, and code blocks."
|
|
310
|
+
].join("\n"), {
|
|
311
|
+
list_id: zod_1.z.string().min(1).describe("The ID of the list where the task will be created"),
|
|
312
|
+
name: taskNameSchema,
|
|
313
|
+
description: zod_1.z.string().optional().describe("Optional markdown description for the task - supports full markdown formatting"),
|
|
314
|
+
priority: taskPrioritySchema,
|
|
315
|
+
due_date: taskDueDateSchema,
|
|
316
|
+
start_date: taskStartDateSchema,
|
|
317
|
+
time_estimate: taskTimeEstimateSchema,
|
|
318
|
+
tags: taskTagsSchema,
|
|
319
|
+
parent: zod_1.z.string().optional().describe("Optional parent task ID to create this as a subtask"),
|
|
320
|
+
assignees: zod_1.z.array(zod_1.z.string()).optional().describe(createAssigneeDescription(userData))
|
|
321
|
+
}, async ({ list_id, name, description, priority, due_date, start_date, time_estimate, tags, parent, assignees }) => {
|
|
322
|
+
try {
|
|
323
|
+
const userData = await (0, utils_1.getCurrentUser)();
|
|
324
|
+
const currentUserId = userData.user.id;
|
|
325
|
+
const requestBody = buildTaskRequestBody({
|
|
326
|
+
name, priority, due_date, start_date, time_estimate, tags, assignees, parent
|
|
327
|
+
}, currentUserId);
|
|
328
|
+
// Add markdown description if provided
|
|
329
|
+
if (description) {
|
|
330
|
+
requestBody.markdown_description = description;
|
|
331
|
+
}
|
|
332
|
+
const response = await fetch(`https://api.clickup.com/api/v2/list/${list_id}/task`, {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
headers: {
|
|
335
|
+
Authorization: config_1.CONFIG.apiKey,
|
|
336
|
+
'Content-Type': 'application/json'
|
|
337
|
+
},
|
|
338
|
+
body: JSON.stringify(requestBody)
|
|
339
|
+
});
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
const errorData = await response.json().catch(() => ({}));
|
|
342
|
+
throw new Error(`Error creating task: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
343
|
+
}
|
|
344
|
+
const createdTask = await response.json();
|
|
345
|
+
const responseLines = formatTaskResponse(createdTask, 'created', {
|
|
346
|
+
list_id, name, description, priority, due_date, start_date, time_estimate, tags, parent, assignees
|
|
347
|
+
}, userData);
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: "text",
|
|
352
|
+
text: responseLines.join('\n')
|
|
353
|
+
}
|
|
354
|
+
],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
console.error('Error creating task:', error);
|
|
359
|
+
return {
|
|
360
|
+
content: [
|
|
361
|
+
{
|
|
362
|
+
type: "text",
|
|
363
|
+
text: `Error creating task: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async function loadTaskContent(taskId) {
|
|
371
|
+
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 } });
|
|
372
|
+
const task = await response.json();
|
|
373
|
+
const content = await (0, clickup_text_1.processClickUpMarkdown)(task.markdown_description || "", task.attachments);
|
|
374
|
+
// Create the task metadata block using the helper function
|
|
375
|
+
const taskMetadata = await generateTaskMetadata(task);
|
|
376
|
+
return [taskMetadata, ...content];
|
|
377
|
+
}
|
|
378
|
+
async function loadTaskComments(id) {
|
|
379
|
+
const response = await fetch(`https://api.clickup.com/api/v2/task/${id}/comment?start_date=0`, // Ensure all comments are fetched
|
|
380
|
+
{ headers: { Authorization: config_1.CONFIG.apiKey } });
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
console.error(`Error fetching comments for task ${id}: ${response.status} ${response.statusText}`);
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
const commentsData = await response.json();
|
|
386
|
+
if (!commentsData.comments || !Array.isArray(commentsData.comments)) {
|
|
387
|
+
console.error(`Unexpected comment data structure for task ${id}`);
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
const commentEvents = await Promise.all(commentsData.comments.map(async (comment) => {
|
|
391
|
+
const headerBlock = {
|
|
392
|
+
type: "text",
|
|
393
|
+
text: `Comment by ${comment.user.username} on ${timestampToIso(comment.date)}:`,
|
|
394
|
+
};
|
|
395
|
+
const commentBodyBlocks = await (0, clickup_text_1.processClickUpText)(comment.comment);
|
|
396
|
+
return {
|
|
397
|
+
date: comment.date, // String timestamp from ClickUp for sorting
|
|
398
|
+
contentBlocks: [headerBlock, ...commentBodyBlocks],
|
|
399
|
+
};
|
|
400
|
+
}));
|
|
401
|
+
return commentEvents;
|
|
402
|
+
}
|
|
403
|
+
async function loadTimeInStatusHistory(taskId) {
|
|
404
|
+
const url = `https://api.clickup.com/api/v2/task/${taskId}/time_in_status`;
|
|
405
|
+
try {
|
|
406
|
+
const response = await fetch(url, { headers: { Authorization: config_1.CONFIG.apiKey } });
|
|
407
|
+
if (!response.ok) {
|
|
408
|
+
console.error(`Error fetching time in status for task ${taskId}: ${response.status} ${response.statusText}`);
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
// Using 'any' for less strict typing as per user preference, but keeping structure for clarity
|
|
412
|
+
const data = await response.json();
|
|
413
|
+
const events = [];
|
|
414
|
+
const processStatusEntry = (entry) => {
|
|
415
|
+
if (!entry || !entry.total_time || !entry.total_time.since || !entry.status)
|
|
416
|
+
return null;
|
|
417
|
+
return {
|
|
418
|
+
date: entry.total_time.since,
|
|
419
|
+
contentBlocks: [{
|
|
420
|
+
type: "text",
|
|
421
|
+
text: `Status set to '${entry.status}' on ${timestampToIso(entry.total_time.since)}`,
|
|
422
|
+
}],
|
|
423
|
+
};
|
|
424
|
+
};
|
|
425
|
+
if (data.status_history && Array.isArray(data.status_history)) {
|
|
426
|
+
data.status_history.forEach((historyEntry) => {
|
|
427
|
+
const event = processStatusEntry(historyEntry);
|
|
428
|
+
if (event)
|
|
429
|
+
events.push(event);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
if (data.current_status) {
|
|
433
|
+
const event = processStatusEntry(data.current_status);
|
|
434
|
+
// Ensure current_status is only added if it's distinct or more recent than the last history item.
|
|
435
|
+
// The deduplication logic below handles if it's the same as the last history entry.
|
|
436
|
+
if (event)
|
|
437
|
+
events.push(event);
|
|
438
|
+
}
|
|
439
|
+
// Deduplicate events based on date and status name to avoid adding current_status if it's identical to the last history entry
|
|
440
|
+
const uniqueEvents = Array.from(new Map(events.map(event => [`${event.date}-${event.contentBlocks[0]?.text}`, event] // Keying by date and text content of first block
|
|
441
|
+
)).values());
|
|
442
|
+
return uniqueEvents;
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
console.error(`Exception fetching time in status for task ${taskId}:`, error);
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Helper function to fetch time entries for a task
|
|
451
|
+
*/
|
|
452
|
+
async function getTaskTimeEntries(taskId) {
|
|
453
|
+
try {
|
|
454
|
+
const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries?task_id=${taskId}`, {
|
|
455
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
456
|
+
});
|
|
457
|
+
if (!response.ok) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const data = await response.json();
|
|
461
|
+
if (!data.data || !Array.isArray(data.data) || data.data.length === 0) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
// Group time entries by user
|
|
465
|
+
const timeByUser = new Map();
|
|
466
|
+
data.data.forEach((entry) => {
|
|
467
|
+
const username = entry.user?.username || 'Unknown User';
|
|
468
|
+
const currentTime = timeByUser.get(username) || 0;
|
|
469
|
+
const entryDurationMs = parseInt(entry.duration) || 0;
|
|
470
|
+
timeByUser.set(username, currentTime + entryDurationMs);
|
|
471
|
+
});
|
|
472
|
+
// Format results
|
|
473
|
+
const userTimeEntries = [];
|
|
474
|
+
let totalTimeMs = 0;
|
|
475
|
+
for (const [username, totalMs] of timeByUser.entries()) {
|
|
476
|
+
const hours = totalMs / (1000 * 60 * 60);
|
|
477
|
+
const displayHours = Math.floor(hours);
|
|
478
|
+
const displayMinutes = Math.round((hours - displayHours) * 60);
|
|
479
|
+
const timeDisplay = displayHours > 0 ?
|
|
480
|
+
`${displayHours}h ${displayMinutes}m` :
|
|
481
|
+
`${displayMinutes}m`;
|
|
482
|
+
userTimeEntries.push(`${username}: ${timeDisplay}`);
|
|
483
|
+
totalTimeMs += totalMs;
|
|
484
|
+
}
|
|
485
|
+
if (userTimeEntries.length === 0) {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
return userTimeEntries.join(', ');
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
console.error(`Error fetching time entries for task ${taskId}:`, error);
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Formats timestamp to ISO string with local timezone (not UTC)
|
|
497
|
+
*/
|
|
498
|
+
function timestampToIso(timestamp) {
|
|
499
|
+
const date = new Date(+timestamp);
|
|
500
|
+
const year = date.getFullYear();
|
|
501
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
502
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
503
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
504
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
505
|
+
// Calculate timezone offset
|
|
506
|
+
const offset = date.getTimezoneOffset();
|
|
507
|
+
const offsetHours = Math.floor(Math.abs(offset) / 60);
|
|
508
|
+
const offsetMinutes = Math.abs(offset) % 60;
|
|
509
|
+
const sign = offset <= 0 ? '+' : '-';
|
|
510
|
+
const timezoneOffset = sign + String(offsetHours).padStart(2, '0') + ':' + String(offsetMinutes).padStart(2, '0');
|
|
511
|
+
return `${year}-${month}-${day}T${hours}:${minutes}${timezoneOffset}`;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Helper function to generate consistent task metadata
|
|
515
|
+
*/
|
|
516
|
+
async function generateTaskMetadata(task) {
|
|
517
|
+
let spaceName = task.space?.name || 'Unknown Space';
|
|
518
|
+
let spaceIdForDisplay = task.space?.id || 'N/A';
|
|
519
|
+
if (spaceName === 'Unknown Space' && task.space?.id) {
|
|
520
|
+
const spaceDetails = await (0, utils_1.getSpaceDetails)(task.space.id);
|
|
521
|
+
if (spaceDetails && spaceDetails.name) {
|
|
522
|
+
spaceName = spaceDetails.name;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const metadataLines = [
|
|
526
|
+
`task_id: ${task.id}`,
|
|
527
|
+
`name: ${task.name}`,
|
|
528
|
+
`status: ${task.status.status}`,
|
|
529
|
+
`date_created: ${timestampToIso(task.date_created)}`,
|
|
530
|
+
`date_updated: ${timestampToIso(task.date_updated)}`,
|
|
531
|
+
`creator: ${task.creator.username} (${task.creator.id})`,
|
|
532
|
+
`assignee: ${task.assignees.map((a) => `${a.username} (${a.id})`).join(', ')}`,
|
|
533
|
+
`list: ${task.list.name} (${task.list.id})`,
|
|
534
|
+
`space: ${spaceName} (${spaceIdForDisplay})`,
|
|
535
|
+
];
|
|
536
|
+
// Add priority if it exists
|
|
537
|
+
if (task.priority !== undefined && task.priority !== null) {
|
|
538
|
+
const priorityName = task.priority.priority || 'none';
|
|
539
|
+
metadataLines.push(`priority: ${priorityName}`);
|
|
540
|
+
}
|
|
541
|
+
// Add due date if it exists
|
|
542
|
+
if (task.due_date) {
|
|
543
|
+
metadataLines.push(`due_date: ${timestampToIso(task.due_date)}`);
|
|
544
|
+
}
|
|
545
|
+
// Add start date if it exists
|
|
546
|
+
if (task.start_date) {
|
|
547
|
+
metadataLines.push(`start_date: ${timestampToIso(task.start_date)}`);
|
|
548
|
+
}
|
|
549
|
+
// Add time estimate if it exists
|
|
550
|
+
if (task.time_estimate) {
|
|
551
|
+
const hours = Math.floor(task.time_estimate / 3600000);
|
|
552
|
+
const minutes = Math.floor((task.time_estimate % 3600000) / 60000);
|
|
553
|
+
metadataLines.push(`time_estimate: ${hours}h ${minutes}m`);
|
|
554
|
+
}
|
|
555
|
+
// Add time booked (tracked time entries)
|
|
556
|
+
const timeBooked = await getTaskTimeEntries(task.id);
|
|
557
|
+
if (timeBooked) {
|
|
558
|
+
metadataLines.push(`time_booked: ${timeBooked}`);
|
|
559
|
+
}
|
|
560
|
+
// Add tags if they exist
|
|
561
|
+
if (task.tags && task.tags.length > 0) {
|
|
562
|
+
metadataLines.push(`tags: ${task.tags.map((t) => t.name).join(', ')}`);
|
|
563
|
+
}
|
|
564
|
+
// Add watchers if they exist
|
|
565
|
+
if (task.watchers && task.watchers.length > 0) {
|
|
566
|
+
metadataLines.push(`watchers: ${task.watchers.map((w) => w.username).join(', ')}`);
|
|
567
|
+
}
|
|
568
|
+
// Add parent task information if it exists
|
|
569
|
+
if (typeof task.parent === "string") {
|
|
570
|
+
metadataLines.push(`parent_task_id: ${task.parent}`);
|
|
571
|
+
}
|
|
572
|
+
// Add child task information if it exists
|
|
573
|
+
if (task.subtasks && task.subtasks.length > 0) {
|
|
574
|
+
metadataLines.push(`child_task_ids: ${task.subtasks.map((st) => st.id).join(', ')}`);
|
|
575
|
+
}
|
|
576
|
+
// Add task URL
|
|
577
|
+
metadataLines.push(`url: ${task.url}`);
|
|
578
|
+
// Add archived status if true
|
|
579
|
+
if (task.archived) {
|
|
580
|
+
metadataLines.push(`archived: true`);
|
|
581
|
+
}
|
|
582
|
+
// Add custom fields if they exist
|
|
583
|
+
if (task.custom_fields && task.custom_fields.length > 0) {
|
|
584
|
+
task.custom_fields.forEach((field) => {
|
|
585
|
+
if (field.value !== undefined && field.value !== null && field.value !== '') {
|
|
586
|
+
const fieldName = field.name.toLowerCase().replace(/\s+/g, '_');
|
|
587
|
+
let fieldValue = field.value;
|
|
588
|
+
// Handle different custom field types
|
|
589
|
+
if (field.type === 'drop_down' && typeof field.value === 'number') {
|
|
590
|
+
// For dropdown fields, find the selected option
|
|
591
|
+
const selectedOption = field.type_config?.options?.find((opt) => opt.orderindex === field.value);
|
|
592
|
+
fieldValue = selectedOption?.name || field.value;
|
|
593
|
+
}
|
|
594
|
+
else if (Array.isArray(field.value)) {
|
|
595
|
+
// For multi-select or array values
|
|
596
|
+
fieldValue = field.value.map((v) => v.name || v).join(', ');
|
|
597
|
+
}
|
|
598
|
+
else if (typeof field.value === 'object') {
|
|
599
|
+
// For object values (like users), extract meaningful data
|
|
600
|
+
fieldValue = field.value.username || field.value.name || JSON.stringify(field.value);
|
|
601
|
+
}
|
|
602
|
+
metadataLines.push(`custom_${fieldName}: ${fieldValue}`);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
type: "text",
|
|
608
|
+
text: metadataLines.join("\n"),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-tools.d.ts","sourceRoot":"","sources":["../../src/tools/time-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAmCpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAmLtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAmFvD"}
|