@sebastienheyd/clickup-mcp 1.8.0 → 1.9.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 +10 -7
- package/dist/cli.js +1 -1
- package/dist/clickup-text.js +17 -0
- package/dist/tools/list-tools.d.ts.map +1 -1
- package/dist/tools/list-tools.js +26 -9
- package/dist/tools/task-tools.d.ts.map +1 -1
- package/dist/tools/task-tools.js +4 -0
- package/dist/tools/task-write-tools.d.ts.map +1 -1
- package/dist/tools/task-write-tools.js +123 -33
- package/dist/tools/time-tools.d.ts.map +1 -1
- package/dist/tools/time-tools.js +166 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,10 +96,10 @@ Turn natural language into powerful ClickUp actions:
|
|
|
96
96
|
- Create, read, and update documents and pages
|
|
97
97
|
- Add comments and collaborate with team members
|
|
98
98
|
- Manage priorities, due dates, assignees, and tags
|
|
99
|
-
- Handle time estimates and custom field values
|
|
99
|
+
- Handle time estimates, sprint points and custom field values
|
|
100
100
|
|
|
101
101
|
### 🔒 **Safety Features**
|
|
102
|
-
- **Append-
|
|
102
|
+
- **Append-First Descriptions**: `append_description` adds a dated block under the existing description and is the default for notes. `replace_description` rewrites the whole description on purpose - the previous content is echoed back so a wrong replacement can be undone
|
|
103
103
|
- **Normal Field Updates**: Status, priority, assignees, tags, and dates can be updated normally (easily revertible through ClickUp's history)
|
|
104
104
|
|
|
105
105
|
## Installation
|
|
@@ -191,14 +191,17 @@ The ClickUp MCP supports three operational modes to balance functionality, secur
|
|
|
191
191
|
| `getTaskById` | ✅ | ✅ | ✅ | Get complete task details including comments, images, and metadata |
|
|
192
192
|
| `addComment` | ❌ | ❌ | ✅ | Add comments to tasks for collaboration |
|
|
193
193
|
| `editComment` | ❌ | ❌ | ✅ | Correct your own comment within 24h instead of posting a follow-up |
|
|
194
|
-
| `
|
|
194
|
+
| `deleteComment` | ❌ | ❌ | ✅ | Delete your own comment within 24h (deleted text is echoed back) |
|
|
195
|
+
| `updateTask` | ❌ | ❌ | ✅ | Update tasks (status, priority, assignees, etc.); append to or replace descriptions |
|
|
195
196
|
| `createTask` | ❌ | ❌ | ✅ | Create new tasks with full markdown support |
|
|
196
197
|
| `searchTasks` | ✅ | ✅ | ✅ | Find tasks by content, keywords, assignees, or project context |
|
|
197
198
|
| `searchSpaces` | ❌ | ✅ | ✅ | Browse workspace structure, project organization, and documents |
|
|
198
199
|
| `getListInfo` | ❌ | ✅ | ✅ | Get list details and available statuses for task creation |
|
|
199
|
-
| `updateListInfo` | ❌ | ❌ | ✅ |
|
|
200
|
+
| `updateListInfo` | ❌ | ❌ | ✅ | Append to or replace list descriptions (previous content echoed back on replace) |
|
|
200
201
|
| `getTimeEntries` | ❌ | ✅ | ✅ | View time entries and analyze time spent across projects |
|
|
201
202
|
| `createTimeEntry` | ❌ | ❌ | ✅ | Log time entries for task tracking |
|
|
203
|
+
| `updateTimeEntry` | ❌ | ❌ | ✅ | Adjust your own time entries (duration, start, description, task) |
|
|
204
|
+
| `deleteTimeEntry` | ❌ | ❌ | ✅ | Delete your own time entries (irreversible, values are echoed back) |
|
|
202
205
|
| `readDocument` | ❌ | ✅ | ✅ | Get document details, page structure, and content with navigation |
|
|
203
206
|
| `searchDocuments` | ❌ | ✅ | ✅ | Search documents by name and space with fuzzy matching and space filtering |
|
|
204
207
|
| `updateDocumentPage` | ❌ | ❌ | ✅ | Update existing page content or name with replace/append modes |
|
|
@@ -277,8 +280,8 @@ Create a task called "API Integration" with description:
|
|
|
277
280
|
See related task: https://app.clickup.com/t/abc123
|
|
278
281
|
```
|
|
279
282
|
|
|
280
|
-
**Append
|
|
281
|
-
|
|
283
|
+
**Append or Replace Descriptions:**
|
|
284
|
+
By default (`append_description`), content is appended under the existing description:
|
|
282
285
|
```markdown
|
|
283
286
|
[Existing task description content]
|
|
284
287
|
|
|
@@ -288,7 +291,7 @@ When updating task descriptions, content is safely appended:
|
|
|
288
291
|
- Performance requirement: < 2s load time
|
|
289
292
|
```
|
|
290
293
|
|
|
291
|
-
This
|
|
294
|
+
This keeps existing content and a clear audit trail. To correct or restructure a description, `replace_description` rewrites it entirely; the tool answers with the previous description so nothing is silently lost. The same two parameters exist on `updateListInfo`.
|
|
292
295
|
|
|
293
296
|
## Writing Images Into Tickets
|
|
294
297
|
|
package/dist/cli.js
CHANGED
|
@@ -124,7 +124,7 @@ async function main() {
|
|
|
124
124
|
// Try to parse as JSON if it looks like a JSON value
|
|
125
125
|
try {
|
|
126
126
|
if (value.startsWith('{') || value.startsWith('[') ||
|
|
127
|
-
value === 'true' || value === 'false' ||
|
|
127
|
+
value === 'true' || value === 'false' || value === 'null' ||
|
|
128
128
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
129
129
|
(!isNaN(Number(value)) && !key.includes('id') && !value.startsWith('"'))) {
|
|
130
130
|
params[key] = JSON.parse(value);
|
package/dist/clickup-text.js
CHANGED
|
@@ -639,7 +639,24 @@ function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0, attachmentsByS
|
|
|
639
639
|
}
|
|
640
640
|
break;
|
|
641
641
|
}
|
|
642
|
+
// Keep the blank line the source puts between two blocks: remark drops it, and
|
|
643
|
+
// ClickUp would otherwise glue paragraphs and lists into one dense block
|
|
644
|
+
if (hasBlankLineBefore(nodes[i + 1], node)) {
|
|
645
|
+
blocks.push({ text: '\n', attributes: {} });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Whether the source has at least one blank line between `previous` and `next`.
|
|
651
|
+
* Several blank lines count as one, as in rendered markdown.
|
|
652
|
+
*/
|
|
653
|
+
function hasBlankLineBefore(next, previous) {
|
|
654
|
+
const previousEnd = previous.position?.end.line;
|
|
655
|
+
const nextStart = next?.position?.start.line;
|
|
656
|
+
if (previousEnd === undefined || nextStart === undefined) {
|
|
657
|
+
return false;
|
|
642
658
|
}
|
|
659
|
+
return nextStart - previousEnd > 1;
|
|
643
660
|
}
|
|
644
661
|
/**
|
|
645
662
|
* Recursively walk phrasing content (inline nodes) and build ClickUp blocks
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-tools.d.ts","sourceRoot":"","sources":["../../src/tools/list-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAqHtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"list-tools.d.ts","sourceRoot":"","sources":["../../src/tools/list-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAqHtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAkGvD"}
|
package/dist/tools/list-tools.js
CHANGED
|
@@ -112,21 +112,27 @@ function registerListToolsWrite(server) {
|
|
|
112
112
|
// Workaround: SDK 1.27+ dual Zod v3/v4 type causes TS2589 on server.tool() generics
|
|
113
113
|
const tool = server.tool.bind(server);
|
|
114
114
|
tool("updateListInfo", [
|
|
115
|
-
"Appends
|
|
115
|
+
"Appends to or replaces a list's description.",
|
|
116
116
|
"ALWAYS reference the list URL (https://app.clickup.com/v/l/LIST_ID) when updating or discussing lists.",
|
|
117
|
-
"
|
|
117
|
+
"`append_description` adds a dated block under the existing description and is the safe default. `replace_description` rewrites the whole description - read it with getListInfo first and repeat everything worth keeping; the previous description is echoed back so it can be restored.",
|
|
118
118
|
"Use this to add project context, requirements, or guidelines that LLMs should consider when working with tasks in this list.",
|
|
119
119
|
"Include links to related tasks, spaces, or external resources in the appended content.",
|
|
120
120
|
"Content is appended in markdown format with timestamp for tracking changes."
|
|
121
121
|
].join("\n"), {
|
|
122
122
|
list_id: zod_1.z.string().min(1).describe("The list ID to update"),
|
|
123
|
-
append_description: zod_1.z.string().min(1).describe("Markdown content to APPEND
|
|
123
|
+
append_description: zod_1.z.string().min(1).optional().describe("Markdown content to APPEND under the existing list description as a dated block (preserves existing content). Mutually exclusive with replace_description"),
|
|
124
|
+
replace_description: zod_1.z.string().optional().describe("Markdown content that REPLACES the whole list description. Anything not repeated here is lost (the previous description is echoed back). Mutually exclusive with append_description")
|
|
124
125
|
}, {
|
|
125
126
|
readOnlyHint: false,
|
|
126
|
-
destructiveHint:
|
|
127
|
+
destructiveHint: true,
|
|
127
128
|
idempotentHint: false,
|
|
128
|
-
}, async ({ list_id, append_description }) => {
|
|
129
|
+
}, async ({ list_id, append_description, replace_description }) => {
|
|
129
130
|
try {
|
|
131
|
+
if ((append_description === undefined) === (replace_description === undefined)) {
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: "text", text: "Pass either append_description or replace_description (exactly one). The list was NOT updated." }],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
130
136
|
// Get current list info including description (try to get markdown content)
|
|
131
137
|
const listResponse = await fetch(`https://api.clickup.com/api/v2/list/${list_id}?include_markdown_description=true`, {
|
|
132
138
|
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
@@ -135,11 +141,17 @@ function registerListToolsWrite(server) {
|
|
|
135
141
|
throw new Error(`Error fetching list: ${listResponse.status} ${listResponse.statusText}`);
|
|
136
142
|
}
|
|
137
143
|
const listData = await listResponse.json();
|
|
138
|
-
//
|
|
144
|
+
// Append adds a dated block under the current description; replace sends the new text as is
|
|
139
145
|
const currentDescription = listData.markdown_description || listData.markdown_content || listData.content || "";
|
|
140
146
|
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
let finalDescription;
|
|
148
|
+
if (replace_description !== undefined) {
|
|
149
|
+
finalDescription = replace_description;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const separator = currentDescription.trim() ? "\n\n---\n" : "";
|
|
153
|
+
finalDescription = currentDescription + separator + `**Edit (${timestamp}):** ${append_description}`;
|
|
154
|
+
}
|
|
143
155
|
// Update the list description using markdown_content
|
|
144
156
|
const updateResponse = await fetch(`https://api.clickup.com/api/v2/list/${list_id}`, {
|
|
145
157
|
method: 'PUT',
|
|
@@ -159,7 +171,12 @@ function registerListToolsWrite(server) {
|
|
|
159
171
|
content: [
|
|
160
172
|
{
|
|
161
173
|
type: "text",
|
|
162
|
-
text:
|
|
174
|
+
text: replace_description !== undefined
|
|
175
|
+
? [
|
|
176
|
+
`Successfully replaced the description of list "${listData.name}" (list_id: ${list_id}).`,
|
|
177
|
+
`previous_description: ${currentDescription.trim() ? currentDescription : '(empty)'}`,
|
|
178
|
+
].join('\n')
|
|
179
|
+
: `Successfully appended content to list "${listData.name}" (list_id: ${list_id}). The new content has been added with timestamp (${timestamp}) while preserving existing description.`,
|
|
163
180
|
},
|
|
164
181
|
],
|
|
165
182
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAyC,MAAM,iBAAiB,CAAC;AAOtF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAgErE;AA+MD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,CAAC,EAAE,GAAG,EAAE,EAAE,YAAY,GAAE,OAAe,GAAG,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"task-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAyC,MAAM,iBAAiB,CAAC;AAOtF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAgErE;AA+MD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,CAAC,EAAE,GAAG,EAAE,EAAE,YAAY,GAAE,OAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAwH/H"}
|
package/dist/tools/task-tools.js
CHANGED
|
@@ -273,6 +273,10 @@ async function generateTaskMetadata(task, timeEntries, isDetailView = false) {
|
|
|
273
273
|
const minutes = Math.floor((task.time_estimate % 3600000) / 60000);
|
|
274
274
|
metadataLines.push(`time_estimate: ${hours}h ${minutes}m`);
|
|
275
275
|
}
|
|
276
|
+
// Add sprint points if they exist
|
|
277
|
+
if (task.points !== undefined && task.points !== null) {
|
|
278
|
+
metadataLines.push(`points: ${task.points}`);
|
|
279
|
+
}
|
|
276
280
|
// Add time booked (tracked time entries) - only if timeEntries provided
|
|
277
281
|
if (timeEntries) {
|
|
278
282
|
const timeBooked = filterTaskTimeEntries(task.id, timeEntries);
|
|
@@ -1 +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;
|
|
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;AAwIpE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QA4sBtE"}
|
|
@@ -93,7 +93,9 @@ const taskPrioritySchema = zod_1.z.enum(["urgent", "high", "normal", "low"]).opt
|
|
|
93
93
|
const taskDueDateSchema = zod_1.z.string().optional().describe("Optional due date as ISO date string (e.g., '2024-10-06T23:59:59+02:00')");
|
|
94
94
|
const taskStartDateSchema = zod_1.z.string().optional().describe("Optional start date as ISO date string (e.g., '2024-10-06T09:00:00+02:00')");
|
|
95
95
|
const taskTimeEstimateSchema = zod_1.z.number().optional().describe("Optional time estimate in hours (will be converted to milliseconds)");
|
|
96
|
+
const taskPointsSchema = zod_1.z.number().nonnegative().optional().describe("Optional sprint points - requires the Sprint Points ClickApp on the space. Valid values depend on the points scale configured in that workspace: ClickUp rejects any other value with 'not a valid points selection'");
|
|
96
97
|
const taskTagsSchema = zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of tag names");
|
|
98
|
+
const CLEAR_HINT = " - pass null to clear the current value";
|
|
97
99
|
function registerTaskToolsWrite(server, userData) {
|
|
98
100
|
// Workaround: SDK 1.27+ dual Zod v3/v4 type causes TS2589 on server.tool() generics
|
|
99
101
|
const tool = server.tool.bind(server);
|
|
@@ -269,12 +271,68 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
269
271
|
};
|
|
270
272
|
}
|
|
271
273
|
});
|
|
274
|
+
tool("deleteComment", [
|
|
275
|
+
"Deletes a task comment. This cannot be undone - the deleted text is echoed back so it can be re-posted with addComment if needed.",
|
|
276
|
+
`GUARDRAILS: same as editComment - only comments written by the API token's own user can be deleted, and only within ${config_1.CONFIG.commentEditWindowHours} hours of their creation. Replies inside a thread cannot be deleted.`,
|
|
277
|
+
].join("\n"), {
|
|
278
|
+
task_id: zod_1.z.string().min(1).refine(val => (0, utils_1.isTaskId)(val) || (0, utils_1.isCustomTaskId)(val), {
|
|
279
|
+
message: "Must be an internal task ID (6+ alphanumeric characters) or a custom task ID (e.g. SOI-4422)"
|
|
280
|
+
}).describe("The ID of the task the comment belongs to - needed to locate the comment. Internal ID (e.g. \"869c4za0g\") or custom ID (e.g. \"SOI-4422\")"),
|
|
281
|
+
comment_id: zod_1.z.string().min(1).describe("The ID of the comment to delete, as returned by addComment or getTaskById"),
|
|
282
|
+
}, {
|
|
283
|
+
readOnlyHint: false,
|
|
284
|
+
destructiveHint: true,
|
|
285
|
+
idempotentHint: false,
|
|
286
|
+
}, async ({ task_id, comment_id }) => {
|
|
287
|
+
try {
|
|
288
|
+
const resolved_task_id = await (0, utils_1.resolveTaskId)(task_id);
|
|
289
|
+
const [existing, userData] = await Promise.all([
|
|
290
|
+
findTaskComment(resolved_task_id, comment_id),
|
|
291
|
+
(0, utils_1.getCurrentUser)(),
|
|
292
|
+
]);
|
|
293
|
+
assertCommentIsEditable(existing, userData.user.id, 'deleted');
|
|
294
|
+
const response = await fetch(`https://api.clickup.com/api/v2/comment/${comment_id}`, {
|
|
295
|
+
method: 'DELETE',
|
|
296
|
+
headers: { Authorization: config_1.CONFIG.apiKey }
|
|
297
|
+
});
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
const errorData = await response.json().catch(() => ({}));
|
|
300
|
+
throw new Error(`Error deleting comment: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "text",
|
|
306
|
+
text: [
|
|
307
|
+
`Comment deleted successfully!`,
|
|
308
|
+
`comment_id: ${comment_id}`,
|
|
309
|
+
`task_id: ${resolved_task_id}`,
|
|
310
|
+
`task_url: https://app.clickup.com/t/${resolved_task_id}`,
|
|
311
|
+
`created: ${timestampToIso(existing.date)}`,
|
|
312
|
+
`deleted_text: ${existing.comment_text || '(no plain text available)'}`,
|
|
313
|
+
].join('\n')
|
|
314
|
+
}
|
|
315
|
+
],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
console.error('Error deleting comment:', error);
|
|
320
|
+
return {
|
|
321
|
+
content: [
|
|
322
|
+
{
|
|
323
|
+
type: "text",
|
|
324
|
+
text: `Error deleting comment: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
});
|
|
272
330
|
tool("updateTask", (() => {
|
|
273
331
|
const descriptionBase = [
|
|
274
332
|
"Updates various aspects of an existing task including dependencies and relationships.",
|
|
275
333
|
"ALWAYS include the task URL (https://app.clickup.com/t/TASK_ID) when updating or referencing tasks.",
|
|
276
334
|
"Use getListInfo first to see valid status options.",
|
|
277
|
-
"
|
|
335
|
+
"DESCRIPTION: `append_description` adds a dated block under the existing description and is the safe default for notes. `replace_description` rewrites the whole description - use it to correct or restructure the specification. Read the current description with getTaskById first and repeat everything worth keeping; the previous description is echoed back so it can be restored with another call.",
|
|
278
336
|
"STATUS UPDATES: Use the `addComment` tool for progress reports, work logs, and status updates rather than the task description.",
|
|
279
337
|
IMAGE_SUPPORT_HINT,
|
|
280
338
|
"Task descriptions should contain requirements, specifications, and core task information.",
|
|
@@ -291,15 +349,18 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
291
349
|
message: "Must be an internal task ID (6+ alphanumeric characters) or a custom task ID (e.g. SOI-4422)"
|
|
292
350
|
}).describe("The task ID to update: internal ID (e.g. \"869c4za0g\") or custom ID (e.g. \"SOI-4422\")"),
|
|
293
351
|
name: taskNameSchema.optional(),
|
|
294
|
-
append_description: zod_1.z.string().optional().describe("Optional markdown content to APPEND
|
|
352
|
+
append_description: zod_1.z.string().optional().describe("Optional markdown content to APPEND under the existing task description as a dated block (preserves existing content). Mutually exclusive with replace_description"),
|
|
353
|
+
replace_description: zod_1.z.string().optional().describe("Optional markdown content that REPLACES the whole task description. Anything not repeated here is lost (the previous description is echoed back in the response). Mutually exclusive with append_description"),
|
|
295
354
|
status: zod_1.z.string().optional().describe("Optional new status name - use getListInfo to see valid options"),
|
|
296
355
|
priority: taskPrioritySchema,
|
|
297
|
-
due_date: taskDueDateSchema,
|
|
298
|
-
start_date: taskStartDateSchema,
|
|
299
|
-
time_estimate: taskTimeEstimateSchema,
|
|
356
|
+
due_date: taskDueDateSchema.nullable().describe(taskDueDateSchema.description + CLEAR_HINT),
|
|
357
|
+
start_date: taskStartDateSchema.nullable().describe(taskStartDateSchema.description + CLEAR_HINT),
|
|
358
|
+
time_estimate: taskTimeEstimateSchema.nullable().describe(taskTimeEstimateSchema.description + CLEAR_HINT),
|
|
359
|
+
points: taskPointsSchema.nullable().describe(taskPointsSchema.description + CLEAR_HINT),
|
|
300
360
|
tags: taskTagsSchema.describe("Optional array of tag names (will replace existing tags)"),
|
|
301
361
|
parent_task_id: zod_1.z.string().optional().describe("Optional parent task ID to change parent/child relationships"),
|
|
302
|
-
assignees: zod_1.z.array(zod_1.z.string()).optional().describe(
|
|
362
|
+
assignees: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of user IDs to add as assignees - existing assignees are kept"),
|
|
363
|
+
remove_assignees: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of user IDs to unassign from the task. Combine with assignees to replace one assignee by another."),
|
|
303
364
|
waiting_on: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of task IDs that this task should wait on (will replace existing waiting_on relationships)"),
|
|
304
365
|
blocking: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of task IDs that this task should block. Note: This creates dependencies FROM those tasks TO this task (those tasks will wait on this one)"),
|
|
305
366
|
linked_tasks: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of task IDs to link as related tasks without blocking (will replace existing linked tasks)")
|
|
@@ -308,8 +369,13 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
308
369
|
destructiveHint: true,
|
|
309
370
|
idempotentHint: false,
|
|
310
371
|
openWorldHint: true
|
|
311
|
-
}, async ({ task_id, name, append_description, status, priority, due_date, start_date, time_estimate, tags, parent_task_id, assignees, blocking, waiting_on, linked_tasks }) => {
|
|
372
|
+
}, async ({ task_id, name, append_description, replace_description, status, priority, due_date, start_date, time_estimate, points, tags, parent_task_id, assignees, remove_assignees, blocking, waiting_on, linked_tasks }) => {
|
|
312
373
|
try {
|
|
374
|
+
if (append_description !== undefined && replace_description !== undefined) {
|
|
375
|
+
return {
|
|
376
|
+
content: [{ type: "text", text: "Pass either append_description or replace_description, not both. The task was NOT updated." }],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
313
379
|
// Resolve custom task ID to internal ID if needed
|
|
314
380
|
const resolved_task_id = await (0, utils_1.resolveTaskId)(task_id);
|
|
315
381
|
const userData = await (0, utils_1.getCurrentUser)();
|
|
@@ -324,15 +390,18 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
324
390
|
// Resolve and upload description images FIRST - an image problem must
|
|
325
391
|
// abort before dependencies, tags or the task itself are touched, so the
|
|
326
392
|
// caller can fix the markdown and retry the whole call cleanly.
|
|
327
|
-
|
|
393
|
+
// Both description modes go through the same pipeline: only what is done
|
|
394
|
+
// with the prepared markdown differs (appended block vs whole description).
|
|
395
|
+
let preparedDescription;
|
|
328
396
|
let uploadedImages = [];
|
|
329
|
-
|
|
397
|
+
const descriptionInput = replace_description !== undefined ? replace_description : append_description;
|
|
398
|
+
if (descriptionInput) {
|
|
330
399
|
const abortNotice = "the task was NOT updated";
|
|
331
|
-
const prepared = await resolveImagesOrAbort(
|
|
400
|
+
const prepared = await resolveImagesOrAbort(descriptionInput, abortNotice);
|
|
332
401
|
uploadedImages = await uploadImagesOrAbort(resolved_task_id, prepared.images, abortNotice);
|
|
333
402
|
// Descriptions render plain markdown, so no image fragments are involved
|
|
334
403
|
// here - the local paths are simply swapped for the CDN URLs.
|
|
335
|
-
|
|
404
|
+
preparedDescription = (0, clickup_text_1.rewriteMarkdownImageUrls)(prepared.markdown, (0, attachments_1.toAttachmentMap)(uploadedImages));
|
|
336
405
|
}
|
|
337
406
|
// Handle dependencies separately since they need individual API calls
|
|
338
407
|
let dependencyUpdateResults = [];
|
|
@@ -382,25 +451,29 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
382
451
|
}
|
|
383
452
|
}
|
|
384
453
|
}
|
|
385
|
-
//
|
|
454
|
+
// Append adds a dated block under the current description; replace sends
|
|
455
|
+
// the prepared markdown as is. An empty replace_description clears it.
|
|
386
456
|
let finalDescription;
|
|
387
|
-
|
|
388
|
-
|
|
457
|
+
const currentDescription = taskData.markdown_description || "";
|
|
458
|
+
if (replace_description !== undefined) {
|
|
459
|
+
finalDescription = preparedDescription ?? "";
|
|
460
|
+
}
|
|
461
|
+
else if (preparedDescription !== undefined) {
|
|
389
462
|
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
390
463
|
const separator = currentDescription.trim() ? "\n\n---\n" : "";
|
|
391
|
-
finalDescription = currentDescription + separator + `**Edit (${timestamp}):** ${
|
|
464
|
+
finalDescription = currentDescription + separator + `**Edit (${timestamp}):** ${preparedDescription}`;
|
|
392
465
|
}
|
|
393
466
|
// Build update body without tags (they're handled separately)
|
|
394
467
|
const updateBody = buildTaskRequestBody({
|
|
395
|
-
name, status, priority, due_date, start_date, time_estimate, parent_task_id, assignees
|
|
468
|
+
name, status, priority, due_date, start_date, time_estimate, points, parent_task_id, assignees
|
|
396
469
|
});
|
|
397
470
|
// Add markdown description if we have content to append
|
|
398
471
|
if (finalDescription !== undefined) {
|
|
399
472
|
updateBody.markdown_description = finalDescription;
|
|
400
473
|
}
|
|
401
|
-
// Handle assignees for updates (different from creates)
|
|
402
|
-
if (assignees !== undefined) {
|
|
403
|
-
updateBody.assignees = { add: assignees, rem: [] };
|
|
474
|
+
// Handle assignees for updates (different from creates) - ClickUp requires both add and rem
|
|
475
|
+
if (assignees !== undefined || remove_assignees !== undefined) {
|
|
476
|
+
updateBody.assignees = { add: assignees ?? [], rem: remove_assignees ?? [] };
|
|
404
477
|
}
|
|
405
478
|
// Check if there's anything to update (including tags and dependencies which were handled separately)
|
|
406
479
|
if (Object.keys(updateBody).length === 0 && tags === undefined && blocking === undefined && waiting_on === undefined && linked_tasks === undefined) {
|
|
@@ -440,8 +513,12 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
440
513
|
}
|
|
441
514
|
}
|
|
442
515
|
const responseLines = formatTaskResponse(updatedTask, 'updated', {
|
|
443
|
-
name, append_description, status, priority, due_date, start_date, time_estimate, tags, parent_task_id, assignees, blocking, waiting_on, linked_tasks
|
|
516
|
+
name, append_description, status, priority, due_date, start_date, time_estimate, points, tags, parent_task_id, assignees, remove_assignees, blocking, waiting_on, linked_tasks
|
|
444
517
|
}, userData);
|
|
518
|
+
// Echo what was overwritten so a wrong replacement can be undone with another call
|
|
519
|
+
if (replace_description !== undefined) {
|
|
520
|
+
responseLines.push(`previous_description: ${currentDescription.trim() ? currentDescription : '(empty)'}`);
|
|
521
|
+
}
|
|
445
522
|
// Add dependency update results if any
|
|
446
523
|
if (dependencyUpdateResults.length > 0) {
|
|
447
524
|
responseLines.push('dependency_warnings: ' + dependencyUpdateResults.join('; '));
|
|
@@ -498,6 +575,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
498
575
|
due_date: taskDueDateSchema,
|
|
499
576
|
start_date: taskStartDateSchema,
|
|
500
577
|
time_estimate: taskTimeEstimateSchema,
|
|
578
|
+
points: taskPointsSchema,
|
|
501
579
|
tags: taskTagsSchema,
|
|
502
580
|
parent_task_id: zod_1.z.string().optional().describe("Optional parent task ID to create this as a subtask"),
|
|
503
581
|
assignees: zod_1.z.array(zod_1.z.string()).optional().describe(createAssigneeDescription(userData))
|
|
@@ -506,7 +584,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
506
584
|
destructiveHint: false,
|
|
507
585
|
idempotentHint: false,
|
|
508
586
|
openWorldHint: true
|
|
509
|
-
}, async ({ list_id, name, description, status, priority, due_date, start_date, time_estimate, tags, parent_task_id, assignees }) => {
|
|
587
|
+
}, async ({ list_id, name, description, status, priority, due_date, start_date, time_estimate, points, tags, parent_task_id, assignees }) => {
|
|
510
588
|
try {
|
|
511
589
|
// Resolve description images BEFORE creating the task: a broken reference
|
|
512
590
|
// (missing file, dead URL, non-image) must not leave a half-finished task
|
|
@@ -516,7 +594,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
516
594
|
const userData = await (0, utils_1.getCurrentUser)();
|
|
517
595
|
const currentUserId = userData.user.id;
|
|
518
596
|
const requestBody = buildTaskRequestBody({
|
|
519
|
-
name, status, priority, due_date, start_date, time_estimate, tags, assignees, parent_task_id
|
|
597
|
+
name, status, priority, due_date, start_date, time_estimate, points, tags, assignees, parent_task_id
|
|
520
598
|
}, currentUserId);
|
|
521
599
|
// Add markdown description if provided
|
|
522
600
|
if (description) {
|
|
@@ -589,7 +667,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
589
667
|
}
|
|
590
668
|
}
|
|
591
669
|
const responseLines = formatTaskResponse(createdTask, 'created', {
|
|
592
|
-
list_id, name, description, status, priority, due_date, start_date, time_estimate, tags, parent_task_id, assignees
|
|
670
|
+
list_id, name, description, status, priority, due_date, start_date, time_estimate, points, tags, parent_task_id, assignees
|
|
593
671
|
}, userData);
|
|
594
672
|
responseLines.push(...formatAttachedImages(uploaded));
|
|
595
673
|
responseLines.push(...imageWarnings);
|
|
@@ -712,17 +790,18 @@ async function findTaskComment(taskId, commentId) {
|
|
|
712
790
|
* other people's comments safe, and the time window is what keeps the tool from
|
|
713
791
|
* rewriting history.
|
|
714
792
|
*/
|
|
715
|
-
function assertCommentIsEditable(comment, currentUserId) {
|
|
793
|
+
function assertCommentIsEditable(comment, currentUserId, action = 'edited') {
|
|
716
794
|
const windowHours = config_1.CONFIG.commentEditWindowHours;
|
|
795
|
+
const verb = action === 'deleted' ? 'Deleting' : 'Editing';
|
|
717
796
|
if (!(windowHours > 0)) {
|
|
718
|
-
throw new Error(
|
|
797
|
+
throw new Error(`${verb} comments is disabled (CLICKUP_COMMENT_EDIT_WINDOW_HOURS=${windowHours}). Add a new comment instead.`);
|
|
719
798
|
}
|
|
720
799
|
if (String(comment.user?.id ?? '') !== String(currentUserId)) {
|
|
721
|
-
throw new Error(`Comment ${comment.id} was written by ${comment.user?.username || 'someone else'} (user_id: ${comment.user?.id ?? 'unknown'}), not by the current user (user_id: ${currentUserId}). Only your own comments can be
|
|
800
|
+
throw new Error(`Comment ${comment.id} was written by ${comment.user?.username || 'someone else'} (user_id: ${comment.user?.id ?? 'unknown'}), not by the current user (user_id: ${currentUserId}). Only your own comments can be ${action} - reply with a new comment instead.`);
|
|
722
801
|
}
|
|
723
802
|
const ageHours = (Date.now() - Number(comment.date)) / (1000 * 60 * 60);
|
|
724
803
|
if (ageHours > windowHours) {
|
|
725
|
-
throw new Error(`Comment ${comment.id} was created ${ageHours.toFixed(1)} hours ago (${timestampToIso(comment.date)}), which is outside the ${windowHours} hour edit window.
|
|
804
|
+
throw new Error(`Comment ${comment.id} was created ${ageHours.toFixed(1)} hours ago (${timestampToIso(comment.date)}), which is outside the ${windowHours} hour edit window. Comments that old can no longer be ${action} - add a new comment instead.`);
|
|
726
805
|
}
|
|
727
806
|
}
|
|
728
807
|
/**
|
|
@@ -754,14 +833,19 @@ function buildTaskRequestBody(params, currentUserId) {
|
|
|
754
833
|
if (params.priority !== undefined) {
|
|
755
834
|
requestBody.priority = convertPriorityToNumber(params.priority);
|
|
756
835
|
}
|
|
836
|
+
// null clears the value - it must be sent as is, new Date(null) would set 1970-01-01.
|
|
837
|
+
// time_estimate is the exception: ClickUp accepts null but silently keeps the estimate, only 0 clears it.
|
|
757
838
|
if (params.due_date !== undefined) {
|
|
758
|
-
requestBody.due_date = new Date(params.due_date).getTime();
|
|
839
|
+
requestBody.due_date = params.due_date === null ? null : new Date(params.due_date).getTime();
|
|
759
840
|
}
|
|
760
841
|
if (params.start_date !== undefined) {
|
|
761
|
-
requestBody.start_date = new Date(params.start_date).getTime();
|
|
842
|
+
requestBody.start_date = params.start_date === null ? null : new Date(params.start_date).getTime();
|
|
762
843
|
}
|
|
763
844
|
if (params.time_estimate !== undefined) {
|
|
764
|
-
requestBody.time_estimate = Math.round(params.time_estimate * 60 * 60 * 1000);
|
|
845
|
+
requestBody.time_estimate = params.time_estimate === null ? 0 : Math.round(params.time_estimate * 60 * 60 * 1000);
|
|
846
|
+
}
|
|
847
|
+
if (params.points !== undefined) {
|
|
848
|
+
requestBody.points = params.points;
|
|
765
849
|
}
|
|
766
850
|
// Tags are handled separately via dedicated API endpoints
|
|
767
851
|
// Do not include in the update request body
|
|
@@ -891,14 +975,20 @@ function formatTaskResponse(task, operation, params, userData) {
|
|
|
891
975
|
params.priority ? params.priority : 'unknown';
|
|
892
976
|
responseLines.push(`priority: ${priority}`);
|
|
893
977
|
}
|
|
978
|
+
if (params.remove_assignees !== undefined && params.remove_assignees.length > 0) {
|
|
979
|
+
responseLines.push(`removed_assignees: ${params.remove_assignees.join(', ')}`);
|
|
980
|
+
}
|
|
894
981
|
if (params.due_date !== undefined) {
|
|
895
|
-
responseLines.push(`due_date: ${params.due_date}`);
|
|
982
|
+
responseLines.push(`due_date: ${params.due_date ?? 'cleared'}`);
|
|
896
983
|
}
|
|
897
984
|
if (params.start_date !== undefined) {
|
|
898
|
-
responseLines.push(`start_date: ${params.start_date}`);
|
|
985
|
+
responseLines.push(`start_date: ${params.start_date ?? 'cleared'}`);
|
|
899
986
|
}
|
|
900
987
|
if (params.time_estimate !== undefined) {
|
|
901
|
-
responseLines.push(`time_estimate: ${formatTimeEstimate(params.time_estimate)}`);
|
|
988
|
+
responseLines.push(`time_estimate: ${params.time_estimate === null ? 'cleared' : formatTimeEstimate(params.time_estimate)}`);
|
|
989
|
+
}
|
|
990
|
+
if (params.points !== undefined) {
|
|
991
|
+
responseLines.push(`points: ${params.points === null ? 'cleared' : task.points ?? params.points}`);
|
|
902
992
|
}
|
|
903
993
|
if (params.tags !== undefined && params.tags.length > 0) {
|
|
904
994
|
responseLines.push(`tags: ${params.tags.join(', ')}`);
|
|
@@ -1 +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;AAyDpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAqFtD;
|
|
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;AAyDpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAqFtD;AA6OD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAmPvD"}
|
package/dist/tools/time-tools.js
CHANGED
|
@@ -248,7 +248,8 @@ function processTimeEntriesData(data, task_id, start_date, end_date, include_all
|
|
|
248
248
|
entryDuration = formatDuration(rawDuration);
|
|
249
249
|
}
|
|
250
250
|
const entryDescription = entry.description ? ` | ${entry.description}` : '';
|
|
251
|
-
|
|
251
|
+
// The id is what updateTimeEntry/deleteTimeEntry need to target an entry
|
|
252
|
+
outputLines.push(`${entryPrefix} ${entryStart} - ${entryDuration} (entry_id: ${entry.id})${entryDescription}`);
|
|
252
253
|
});
|
|
253
254
|
}
|
|
254
255
|
}
|
|
@@ -264,9 +265,173 @@ function processTimeEntriesData(data, task_id, start_date, end_date, include_all
|
|
|
264
265
|
],
|
|
265
266
|
};
|
|
266
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Reads a single time entry. GET /time_entries/{id} answers `{"data": null}`
|
|
270
|
+
* with a 200 for an unknown or already deleted id, hence the explicit check.
|
|
271
|
+
*/
|
|
272
|
+
async function fetchTimeEntry(entryId) {
|
|
273
|
+
const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries/${entryId}`, {
|
|
274
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
275
|
+
});
|
|
276
|
+
if (!response.ok) {
|
|
277
|
+
throw new Error(`Error fetching time entry ${entryId}: ${response.status} ${response.statusText}`);
|
|
278
|
+
}
|
|
279
|
+
const data = await response.json();
|
|
280
|
+
if (!data.data) {
|
|
281
|
+
throw new Error(`Time entry ${entryId} was not found. Use getTimeEntries to list existing entries and their entry_id.`);
|
|
282
|
+
}
|
|
283
|
+
return data.data;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* The safety model of updateTimeEntry/deleteTimeEntry: getTimeEntries can list
|
|
287
|
+
* other members' entries (include_all_users), so the owner check is what keeps
|
|
288
|
+
* a wrong entry_id from touching a colleague's time. A running timer is refused
|
|
289
|
+
* too - its duration is negative and its end does not exist yet.
|
|
290
|
+
*/
|
|
291
|
+
function assertTimeEntryIsOwn(entry, currentUserId, action) {
|
|
292
|
+
if (String(entry.user?.id ?? '') !== String(currentUserId)) {
|
|
293
|
+
throw new Error(`Time entry ${entry.id} belongs to ${entry.user?.username || 'someone else'} (user_id: ${entry.user?.id ?? 'unknown'}), not to the current user (user_id: ${currentUserId}). Only your own time entries can be ${action}.`);
|
|
294
|
+
}
|
|
295
|
+
if (parseInt(entry.duration) < 0) {
|
|
296
|
+
throw new Error(`Time entry ${entry.id} is a running timer. Stop it in ClickUp before trying to ${action === 'deleted' ? 'delete' : 'update'} it.`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function formatTimeEntryLines(entry) {
|
|
300
|
+
return [
|
|
301
|
+
`entry_id: ${entry.id}`,
|
|
302
|
+
`task: ${entry.task?.name || 'No Task'} (task_id: ${entry.task?.id || 'none'})`,
|
|
303
|
+
`start_time: ${timestampToIso(parseInt(entry.start))}`,
|
|
304
|
+
`duration: ${formatDuration(parseInt(entry.duration) || 0)}`,
|
|
305
|
+
...(entry.description ? [`description: ${entry.description}`] : []),
|
|
306
|
+
`user: ${entry.user?.username || 'Unknown User'} (user_id: ${entry.user?.id ?? 'unknown'})`,
|
|
307
|
+
];
|
|
308
|
+
}
|
|
267
309
|
function registerTimeToolsWrite(server) {
|
|
268
310
|
// Workaround: SDK 1.27+ dual Zod v3/v4 type causes TS2589 on server.tool() generics
|
|
269
311
|
const tool = server.tool.bind(server);
|
|
312
|
+
tool("updateTimeEntry", [
|
|
313
|
+
"Updates an existing time entry: duration, start time, description or the task it is booked on.",
|
|
314
|
+
"Use getTimeEntries first to find the entry_id. Only fields provided are changed.",
|
|
315
|
+
"GUARDRAILS: only time entries of the API token's own user can be updated, and running timers are refused.",
|
|
316
|
+
"Use decimal hours for the duration (e.g., 0.25 for 15 minutes, 1.5 for 1h 30min)."
|
|
317
|
+
].join("\n"), {
|
|
318
|
+
entry_id: zod_1.z.string().min(1).describe("The time entry ID (entry_id shown by getTimeEntries)"),
|
|
319
|
+
hours: zod_1.z.number().min(0.01).max(24).optional().describe("New duration in decimal hours (e.g., 0.25 = 15min, 1.5 = 1h 30min)"),
|
|
320
|
+
start_time: zod_1.z.string().optional().describe("New start time as ISO date string (e.g., '2024-10-06T09:00:00+02:00'); the end is recomputed from the duration"),
|
|
321
|
+
description: zod_1.z.string().optional().describe("New description (pass an empty string to clear it)"),
|
|
322
|
+
task_id: zod_1.z.string().min(1).refine(val => (0, utils_1.isTaskId)(val) || (0, utils_1.isCustomTaskId)(val), {
|
|
323
|
+
message: "Must be an internal task ID (6+ alphanumeric characters) or a custom task ID (e.g. SOI-4422)"
|
|
324
|
+
}).optional().describe("Move the entry to another task: internal ID (e.g. \"869c4za0g\") or custom ID (e.g. \"SOI-4422\")")
|
|
325
|
+
}, {
|
|
326
|
+
readOnlyHint: false,
|
|
327
|
+
destructiveHint: true,
|
|
328
|
+
idempotentHint: true,
|
|
329
|
+
}, async ({ entry_id, hours, start_time, description, task_id }) => {
|
|
330
|
+
try {
|
|
331
|
+
if (hours === undefined && start_time === undefined && description === undefined && task_id === undefined) {
|
|
332
|
+
return {
|
|
333
|
+
content: [{ type: "text", text: "No updates provided. Please specify at least one of hours, start_time, description or task_id." }],
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const userData = await (0, utils_1.getCurrentUser)();
|
|
337
|
+
const existing = await fetchTimeEntry(entry_id);
|
|
338
|
+
assertTimeEntryIsOwn(existing, userData.user.id, 'updated');
|
|
339
|
+
// ClickUp leaves `end` untouched when only `start` is sent, which makes the
|
|
340
|
+
// entry inconsistent - so start, end and duration are always sent together.
|
|
341
|
+
const startMs = start_time !== undefined ? isoToTimestamp(start_time) : parseInt(existing.start);
|
|
342
|
+
const durationMs = hours !== undefined ? Math.round(hours * 60 * 60 * 1000) : parseInt(existing.duration);
|
|
343
|
+
const requestBody = {
|
|
344
|
+
start: startMs,
|
|
345
|
+
end: startMs + durationMs,
|
|
346
|
+
duration: durationMs,
|
|
347
|
+
...(description !== undefined && { description }),
|
|
348
|
+
...(task_id !== undefined && { tid: await (0, utils_1.resolveTaskId)(task_id) }),
|
|
349
|
+
};
|
|
350
|
+
const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries/${entry_id}`, {
|
|
351
|
+
method: 'PUT',
|
|
352
|
+
headers: {
|
|
353
|
+
Authorization: config_1.CONFIG.apiKey,
|
|
354
|
+
'Content-Type': 'application/json'
|
|
355
|
+
},
|
|
356
|
+
body: JSON.stringify(requestBody)
|
|
357
|
+
});
|
|
358
|
+
if (!response.ok) {
|
|
359
|
+
const errorData = await response.json().catch(() => ({}));
|
|
360
|
+
throw new Error(`Error updating time entry: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
361
|
+
}
|
|
362
|
+
// PUT answers `{"data": [entry]}` - fall back to what was sent if the shape differs
|
|
363
|
+
const responseData = await response.json();
|
|
364
|
+
const updated = Array.isArray(responseData.data) ? responseData.data[0] : responseData.data;
|
|
365
|
+
const finalEntry = { ...existing, ...requestBody, ...(updated || {}), task: updated?.task || existing.task };
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: "text",
|
|
370
|
+
text: [`Time entry updated successfully!`, ...formatTimeEntryLines(finalEntry)].join('\n')
|
|
371
|
+
}
|
|
372
|
+
],
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
console.error('Error updating time entry:', error);
|
|
377
|
+
return {
|
|
378
|
+
content: [
|
|
379
|
+
{
|
|
380
|
+
type: "text",
|
|
381
|
+
text: `Error updating time entry: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
tool("deleteTimeEntry", [
|
|
388
|
+
"Deletes a time entry. This cannot be undone - check the entry with getTimeEntries first.",
|
|
389
|
+
"GUARDRAILS: only time entries of the API token's own user can be deleted, and running timers are refused.",
|
|
390
|
+
"The response echoes the deleted values so the entry can be recreated with createTimeEntry if needed."
|
|
391
|
+
].join("\n"), {
|
|
392
|
+
entry_id: zod_1.z.string().min(1).describe("The time entry ID (entry_id shown by getTimeEntries)")
|
|
393
|
+
}, {
|
|
394
|
+
readOnlyHint: false,
|
|
395
|
+
destructiveHint: true,
|
|
396
|
+
idempotentHint: false,
|
|
397
|
+
}, async ({ entry_id }) => {
|
|
398
|
+
try {
|
|
399
|
+
const userData = await (0, utils_1.getCurrentUser)();
|
|
400
|
+
const existing = await fetchTimeEntry(entry_id);
|
|
401
|
+
assertTimeEntryIsOwn(existing, userData.user.id, 'deleted');
|
|
402
|
+
const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries/${entry_id}`, {
|
|
403
|
+
method: 'DELETE',
|
|
404
|
+
headers: {
|
|
405
|
+
Authorization: config_1.CONFIG.apiKey,
|
|
406
|
+
'Content-Type': 'application/json'
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
if (!response.ok) {
|
|
410
|
+
const errorData = await response.json().catch(() => ({}));
|
|
411
|
+
throw new Error(`Error deleting time entry: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
412
|
+
}
|
|
413
|
+
// DELETE answers an empty object, so the values come from the entry read beforehand
|
|
414
|
+
return {
|
|
415
|
+
content: [
|
|
416
|
+
{
|
|
417
|
+
type: "text",
|
|
418
|
+
text: [`Time entry deleted successfully!`, ...formatTimeEntryLines(existing)].join('\n')
|
|
419
|
+
}
|
|
420
|
+
],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
console.error('Error deleting time entry:', error);
|
|
425
|
+
return {
|
|
426
|
+
content: [
|
|
427
|
+
{
|
|
428
|
+
type: "text",
|
|
429
|
+
text: `Error deleting time entry: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
});
|
|
270
435
|
tool("createTimeEntry", [
|
|
271
436
|
"Creates a time entry (books time) on a task for the current user.",
|
|
272
437
|
"Use decimal hours (e.g., 0.25 for 15 minutes, 0.5 for 30 minutes, 2.5 for 2.5 hours).",
|
package/package.json
CHANGED