@sebastienheyd/clickup-mcp 1.10.1 → 1.10.2
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 +6 -2
- package/dist/shared/attachments.d.ts.map +1 -1
- package/dist/shared/attachments.js +9 -3
- package/dist/shared/dates.d.ts +25 -0
- package/dist/shared/dates.d.ts.map +1 -0
- package/dist/shared/dates.js +42 -0
- package/dist/tools/task-tools.d.ts.map +1 -1
- package/dist/tools/task-tools.js +2 -0
- package/dist/tools/task-write-tools.d.ts.map +1 -1
- package/dist/tools/task-write-tools.js +173 -23
- package/dist/tools/time-tools.d.ts.map +1 -1
- package/dist/tools/time-tools.js +14 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -313,7 +313,7 @@ This keeps existing content and a clear audit trail. To correct or restructure a
|
|
|
313
313
|
|
|
314
314
|
## Writing Images Into Tickets
|
|
315
315
|
|
|
316
|
-
`addComment`, `
|
|
316
|
+
`addComment`, `createTask` and `updateTask` accept images as ordinary markdown. Because
|
|
317
317
|
this server runs locally, it reads the file itself - so a **local path is enough**:
|
|
318
318
|
|
|
319
319
|
```markdown
|
|
@@ -336,12 +336,16 @@ Notes:
|
|
|
336
336
|
- **An image inside a numbered list breaks ClickUp's numbering.** Write walkthrough
|
|
337
337
|
steps as bold lines with the image between them, as above.
|
|
338
338
|
- Only real PNG/JPEG/GIF/WebP files are uploaded - the content is checked, not the
|
|
339
|
-
extension. A file that fails **aborts the write**: `addComment
|
|
339
|
+
extension. A file that fails **aborts the write**: `addComment` and
|
|
340
340
|
`updateTask` report every broken reference and change nothing, so the markdown can be
|
|
341
341
|
fixed and the call retried without creating duplicates. `createTask` validates its
|
|
342
342
|
images before creating the task; only an upload failing afterwards is reported as a
|
|
343
343
|
warning, since the task already exists at that point.
|
|
344
344
|
- Attachments always belong to a task, so document pages cannot embed uploads this way.
|
|
345
|
+
- **`editComment` can only keep the images already in the comment.** Pass the markdown
|
|
346
|
+
returned by `getTaskById` back and they stay; ClickUp refuses to add any other image to
|
|
347
|
+
an existing comment, so a new image reference is rejected before anything is uploaded.
|
|
348
|
+
Share a new image in a new comment with `addComment` instead.
|
|
345
349
|
|
|
346
350
|
## Performance & Limitations
|
|
347
351
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../../src/shared/attachments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAQhC;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,uCAAuC;AACvC,UAAU,aAAa;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,mFAAmF;AACnF,UAAU,gBAAgB;IACxB,IAAI,EAAE,UAAU,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,gBAAgB,CAAC;AAoCnE;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAM3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAsB,GAC9B,OAAO,CAAC,mBAAmB,CAAC,CA2C9B;AAcD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAS/E;AAmCD;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,yBAAyB,CAAC,CAwBpC;AAED,2FAA2F;AAC3F,MAAM,WAAW,qBAAqB;IACpC,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,mEAAmE;AACnE,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,EACtC,OAAO,GAAE,MAAsB,GAC9B,OAAO,CAAC;IAAE,QAAQ,EAAE,qBAAqB,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAAC,CAqB1E;AAED,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,yBAAyB,CAAC;CACvC;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,qBAAqB,EAAE,GAC9B,OAAO,CAAC;IAAE,QAAQ,EAAE,qBAAqB,EAAE,CAAC;IAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../../src/shared/attachments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAQhC;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,uCAAuC;AACvC,UAAU,aAAa;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,mFAAmF;AACnF,UAAU,gBAAgB;IACxB,IAAI,EAAE,UAAU,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,gBAAgB,CAAC;AAoCnE;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAM3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAsB,GAC9B,OAAO,CAAC,mBAAmB,CAAC,CA2C9B;AAcD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAS/E;AAmCD;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,yBAAyB,CAAC,CAwBpC;AAED,2FAA2F;AAC3F,MAAM,WAAW,qBAAqB;IACpC,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,mEAAmE;AACnE,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,EACtC,OAAO,GAAE,MAAsB,GAC9B,OAAO,CAAC;IAAE,QAAQ,EAAE,qBAAqB,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAAC,CAqB1E;AAED,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,yBAAyB,CAAC;CACvC;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,qBAAqB,EAAE,GAC9B,OAAO,CAAC;IAAE,QAAQ,EAAE,qBAAqB,EAAE,CAAC;IAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAAA;CAAE,CAAC,CA8C9E;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,qBAAqB,EAAE,GAChC,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAMxC"}
|
|
@@ -228,12 +228,18 @@ async function uploadResolvedImages(taskId, images) {
|
|
|
228
228
|
try {
|
|
229
229
|
if (resolved.kind === "existing") {
|
|
230
230
|
// Already on ClickUp's CDN - synthesise the minimal attachment shape so
|
|
231
|
-
// the fragment builder has something to work with.
|
|
232
|
-
|
|
231
|
+
// the fragment builder has something to work with. The real attachment id
|
|
232
|
+
// is `<uuid><ext>`, the uuid being the path segment before the filename
|
|
233
|
+
// (.../t123/<uuid>/<name>); ClickUp rejects a comment edit whose image
|
|
234
|
+
// fragment carries any other id (404 ACCESS_028, verified live).
|
|
235
|
+
const pathname = new URL(resolved.url).pathname;
|
|
236
|
+
const name = decodeURIComponent((0, path_1.basename)(pathname));
|
|
237
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
238
|
+
const uuid = segments.length >= 2 ? segments[segments.length - 2] : undefined;
|
|
233
239
|
uploaded.push({
|
|
234
240
|
src,
|
|
235
241
|
attachment: {
|
|
236
|
-
id: name,
|
|
242
|
+
id: uuid ? `${uuid}${(0, path_1.extname)(name)}` : name,
|
|
237
243
|
name,
|
|
238
244
|
title: alt || name,
|
|
239
245
|
extension: (0, path_1.extname)(name).replace(/^\./, "") || undefined,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date parameters as tools receive them: an ISO date with a time of day
|
|
3
|
+
* ("2026-10-02T18:00:00+02:00") or a bare date ("2026-10-02").
|
|
4
|
+
*/
|
|
5
|
+
export interface ParsedDateInput {
|
|
6
|
+
/** Milliseconds since the epoch */
|
|
7
|
+
timestamp: number;
|
|
8
|
+
/** false for a bare date (YYYY-MM-DD), which stands for a whole local day */
|
|
9
|
+
hasTime: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse a date parameter, or throw an error that names the field.
|
|
13
|
+
*
|
|
14
|
+
* Two silent failures are turned into errors on purpose:
|
|
15
|
+
* - an unparseable string gives NaN, which JSON.stringify sends as `null` - a
|
|
16
|
+
* due date would be cleared while the tool reports success;
|
|
17
|
+
* - JavaScript rolls impossible days over instead of rejecting them, with or
|
|
18
|
+
* without a time ("2026-02-31" and "2026-02-31T10:00" both become March 3rd).
|
|
19
|
+
*
|
|
20
|
+
* A bare date is a whole LOCAL day: `new Date("2026-10-02")` would be UTC
|
|
21
|
+
* midnight, i.e. the previous evening east of Greenwich. `bareDateAt` picks the
|
|
22
|
+
* start (00:00:00.000) or the end (23:59:59.999) of that day.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseDateInput(value: string, field: string, bareDateAt?: "start" | "end"): ParsedDateInput;
|
|
25
|
+
//# sourceMappingURL=dates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dates.d.ts","sourceRoot":"","sources":["../../src/shared/dates.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;CAClB;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,OAAO,GAAG,KAAe,GACpC,eAAe,CA8BjB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseDateInput = parseDateInput;
|
|
4
|
+
const LEADING_DATE = /^(\d{4})-(\d{2})-(\d{2})(.*)$/;
|
|
5
|
+
/**
|
|
6
|
+
* Parse a date parameter, or throw an error that names the field.
|
|
7
|
+
*
|
|
8
|
+
* Two silent failures are turned into errors on purpose:
|
|
9
|
+
* - an unparseable string gives NaN, which JSON.stringify sends as `null` - a
|
|
10
|
+
* due date would be cleared while the tool reports success;
|
|
11
|
+
* - JavaScript rolls impossible days over instead of rejecting them, with or
|
|
12
|
+
* without a time ("2026-02-31" and "2026-02-31T10:00" both become March 3rd).
|
|
13
|
+
*
|
|
14
|
+
* A bare date is a whole LOCAL day: `new Date("2026-10-02")` would be UTC
|
|
15
|
+
* midnight, i.e. the previous evening east of Greenwich. `bareDateAt` picks the
|
|
16
|
+
* start (00:00:00.000) or the end (23:59:59.999) of that day.
|
|
17
|
+
*/
|
|
18
|
+
function parseDateInput(value, field, bareDateAt = "start") {
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
const leading = LEADING_DATE.exec(trimmed);
|
|
21
|
+
if (leading) {
|
|
22
|
+
const [, yearText, monthText, dayText, rest] = leading;
|
|
23
|
+
const year = Number(yearText);
|
|
24
|
+
const month = Number(monthText);
|
|
25
|
+
const day = Number(dayText);
|
|
26
|
+
const daysInMonth = month >= 1 && month <= 12 ? new Date(year, month, 0).getDate() : 0;
|
|
27
|
+
if (day < 1 || day > daysInMonth) {
|
|
28
|
+
throw new Error(`Invalid ${field} "${value}": ${yearText}-${monthText}-${dayText} is not a calendar date.`);
|
|
29
|
+
}
|
|
30
|
+
if (rest === "") {
|
|
31
|
+
const timestamp = bareDateAt === "start"
|
|
32
|
+
? new Date(year, month - 1, day, 0, 0, 0, 0).getTime()
|
|
33
|
+
: new Date(year, month - 1, day, 23, 59, 59, 999).getTime();
|
|
34
|
+
return { timestamp, hasTime: false };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const timestamp = Date.parse(trimmed);
|
|
38
|
+
if (Number.isNaN(timestamp)) {
|
|
39
|
+
throw new Error(`Invalid ${field} "${value}". Expected an ISO date with a time of day (e.g. 2026-10-02T18:00:00+02:00) or a bare date (YYYY-MM-DD).`);
|
|
40
|
+
}
|
|
41
|
+
return { timestamp, hasTime: true };
|
|
42
|
+
}
|
|
@@ -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;AAQtF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAgErE;AAuPD;;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;AAQtF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAgErE;AAuPD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,CAAC,EAAE,GAAG,EAAE,EAAE,YAAY,GAAE,OAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAuJ/H"}
|
package/dist/tools/task-tools.js
CHANGED
|
@@ -280,6 +280,8 @@ async function generateTaskMetadata(task, timeEntries, isDetailView = false) {
|
|
|
280
280
|
}
|
|
281
281
|
const metadataLines = [
|
|
282
282
|
`task_id: ${task.id}`,
|
|
283
|
+
// The custom ID (e.g. "PQPS-1234") is what people quote; every tool accepts it
|
|
284
|
+
...(task.custom_id ? [`custom_id: ${task.custom_id}`] : []),
|
|
283
285
|
`task_url: ${task.url}`,
|
|
284
286
|
`name: ${task.name}`,
|
|
285
287
|
`status: ${task.status.status}`,
|
|
@@ -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;AA6JpE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAyxBtE"}
|
|
@@ -7,6 +7,7 @@ const utils_1 = require("../shared/utils");
|
|
|
7
7
|
const clickup_text_1 = require("../clickup-text");
|
|
8
8
|
const attachments_1 = require("../shared/attachments");
|
|
9
9
|
const comments_1 = require("../shared/comments");
|
|
10
|
+
const dates_1 = require("../shared/dates");
|
|
10
11
|
/**
|
|
11
12
|
* Shared wording for the image support of every markdown field in this file.
|
|
12
13
|
* Kept in one place so the tools stay consistent about what a client may pass.
|
|
@@ -190,7 +191,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
190
191
|
});
|
|
191
192
|
if (!response.ok) {
|
|
192
193
|
const errorData = await response.json().catch(() => ({}));
|
|
193
|
-
throw new Error(`
|
|
194
|
+
throw new Error(`ClickUp API ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
194
195
|
}
|
|
195
196
|
const commentData = await response.json();
|
|
196
197
|
return {
|
|
@@ -234,8 +235,8 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
234
235
|
"ClickUp shows no 'edited' marker, so people who already read the comment will not notice the change - for anything that changes meaning after a discussion has started, prefer a follow-up comment.",
|
|
235
236
|
"Editing does not reset the creation date, so the edit window does not get extended by editing.",
|
|
236
237
|
COMMENT_FORMATTING_HINT,
|
|
237
|
-
|
|
238
|
-
"
|
|
238
|
+
"IMAGES ON EDIT: an edit can only KEEP the images already in this comment. Reading the comment (getTaskById) returns them as markdown, and passing that markdown back keeps them; an image whose markdown you drop disappears.",
|
|
239
|
+
"ClickUp refuses to add any other image to an existing comment - a new file, a data URI, a web URL, or an image of another comment or of the task description - so such a reference is rejected before anything is uploaded. To share a new image, post a new comment with addComment.",
|
|
239
240
|
"Task URLs (https://app.clickup.com/t/TASK_ID) become live task references, and existing references are read back as such URLs - passing the text back keeps them.",
|
|
240
241
|
];
|
|
241
242
|
if (config_1.CONFIG.primaryLanguageHint && config_1.CONFIG.primaryLanguageHint.toLowerCase() !== 'en') {
|
|
@@ -259,14 +260,14 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
259
260
|
(0, utils_1.getCurrentUser)(),
|
|
260
261
|
]);
|
|
261
262
|
assertCommentIsEditable(existing, userData.user.id);
|
|
262
|
-
// Same
|
|
263
|
-
// accepted by PUT too, so formatting
|
|
264
|
-
//
|
|
265
|
-
//
|
|
263
|
+
// Same converter as addComment - the undocumented rich `comment` array is
|
|
264
|
+
// accepted by PUT too, so formatting survives an edit. Images are different:
|
|
265
|
+
// ClickUp only lets an edit keep the comment's OWN images, so they are
|
|
266
|
+
// matched against the stored fragments and nothing is ever uploaded here.
|
|
266
267
|
const abortNotice = "the comment was NOT changed";
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const commentBlocks = (0, clickup_text_1.convertMarkdownToClickUpBlocks)(markdown, (0, attachments_1.toAttachmentMap)(
|
|
268
|
+
const markdown = (0, clickup_text_1.normalizeImageDestinations)(comment);
|
|
269
|
+
const keptImages = keepOwnCommentImages(existing, markdown, abortNotice);
|
|
270
|
+
const commentBlocks = (0, clickup_text_1.convertMarkdownToClickUpBlocks)(markdown, (0, attachments_1.toAttachmentMap)(keptImages));
|
|
270
271
|
// Only `comment` is sent: sending `comment_text` alongside it appends that
|
|
271
272
|
// string to the blocks instead of being ignored.
|
|
272
273
|
const response = await fetch(`https://api.clickup.com/api/v2/comment/${comment_id}`, {
|
|
@@ -279,7 +280,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
279
280
|
});
|
|
280
281
|
if (!response.ok) {
|
|
281
282
|
const errorData = await response.json().catch(() => ({}));
|
|
282
|
-
throw new Error(`
|
|
283
|
+
throw new Error(`ClickUp API ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
283
284
|
}
|
|
284
285
|
return {
|
|
285
286
|
content: [
|
|
@@ -293,7 +294,10 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
293
294
|
`created: ${timestampToIso(existing.date)} (unchanged by the edit)`,
|
|
294
295
|
`previous_text: ${existing.comment_text || '(no plain text available)'}`,
|
|
295
296
|
`new_comment: ${summarizeMarkdownForEcho(comment)}`,
|
|
296
|
-
...
|
|
297
|
+
...(keptImages.length > 0 ? [
|
|
298
|
+
`images_kept: ${keptImages.length}`,
|
|
299
|
+
...keptImages.map((k) => ` - ${k.attachment.name} (${k.attachment.url})`),
|
|
300
|
+
] : []),
|
|
297
301
|
].join('\n')
|
|
298
302
|
}
|
|
299
303
|
],
|
|
@@ -335,7 +339,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
335
339
|
});
|
|
336
340
|
if (!response.ok) {
|
|
337
341
|
const errorData = await response.json().catch(() => ({}));
|
|
338
|
-
throw new Error(`
|
|
342
|
+
throw new Error(`ClickUp API ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
339
343
|
}
|
|
340
344
|
return {
|
|
341
345
|
content: [
|
|
@@ -395,7 +399,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
395
399
|
time_estimate: taskTimeEstimateSchema.nullable().describe(taskTimeEstimateSchema.description + CLEAR_HINT),
|
|
396
400
|
points: taskPointsSchema.nullable().describe(taskPointsSchema.description + CLEAR_HINT),
|
|
397
401
|
tags: taskTagsSchema.describe("Optional array of tag names (will replace existing tags)"),
|
|
398
|
-
parent_task_id:
|
|
402
|
+
parent_task_id: taskIdSchema.optional().describe("Optional parent task ID to move this task under, as a subtask: internal ID (e.g. \"869c4za0g\") or custom ID (e.g. \"SOI-4422\")"),
|
|
399
403
|
assignees: zod_1.z.array(zod_1.z.string()).optional().describe("Optional array of user IDs to add as assignees - existing assignees are kept"),
|
|
400
404
|
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."),
|
|
401
405
|
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)"),
|
|
@@ -418,8 +422,22 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
418
422
|
}],
|
|
419
423
|
};
|
|
420
424
|
}
|
|
425
|
+
// A malformed date must be refused before anything is written: it would
|
|
426
|
+
// otherwise be sent as null and clear the date (see parseDateInput).
|
|
427
|
+
assertValidTaskDates({ due_date, start_date });
|
|
421
428
|
// Resolve custom task IDs (e.g. "SOI-4422") to internal IDs
|
|
422
429
|
task_id = await (0, utils_1.resolveTaskId)(task_id);
|
|
430
|
+
// Current dependencies and links come back from ClickUp with internal IDs,
|
|
431
|
+
// so the requested lists must use them too: an unresolved "SOI-4422" would
|
|
432
|
+
// not match its own internal ID, and the existing relation would be removed
|
|
433
|
+
// as "no longer requested" while a duplicate is added. Resolved up front, so
|
|
434
|
+
// an unknown custom ID aborts the update before anything is written.
|
|
435
|
+
[blocking, waiting_on, linked_tasks] = await Promise.all([blocking, waiting_on, linked_tasks].map(resolveTaskIdList));
|
|
436
|
+
// ClickUp's `parent` field only takes an internal ID: a custom ID gets a bare
|
|
437
|
+
// 500 Internal Server Error (verified live), with nothing to tell what is wrong.
|
|
438
|
+
if (parent_task_id !== undefined) {
|
|
439
|
+
parent_task_id = await (0, utils_1.resolveTaskId)(parent_task_id);
|
|
440
|
+
}
|
|
423
441
|
const userData = await (0, utils_1.getCurrentUser)();
|
|
424
442
|
// Get task details including current markdown description
|
|
425
443
|
const taskResponse = await fetch(`https://api.clickup.com/api/v2/task/${task_id}?include_markdown_description=true`, {
|
|
@@ -543,7 +561,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
543
561
|
});
|
|
544
562
|
if (!updateResponse.ok) {
|
|
545
563
|
const errorData = await updateResponse.json().catch(() => ({}));
|
|
546
|
-
throw new Error(`
|
|
564
|
+
throw new Error(`ClickUp API ${updateResponse.status} ${updateResponse.statusText} - ${JSON.stringify(errorData)}`);
|
|
547
565
|
}
|
|
548
566
|
updatedTask = await updateResponse.json();
|
|
549
567
|
}
|
|
@@ -626,7 +644,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
626
644
|
time_estimate: taskTimeEstimateSchema,
|
|
627
645
|
points: taskPointsSchema,
|
|
628
646
|
tags: taskTagsSchema,
|
|
629
|
-
parent_task_id:
|
|
647
|
+
parent_task_id: taskIdSchema.optional().describe("Optional parent task ID to create this as a subtask: internal ID (e.g. \"869c4za0g\") or custom ID (e.g. \"SOI-4422\")"),
|
|
630
648
|
assignees: zod_1.z.array(zod_1.z.string()).optional().describe(createAssigneeDescription(userData))
|
|
631
649
|
}, {
|
|
632
650
|
readOnlyHint: false,
|
|
@@ -640,6 +658,12 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
640
658
|
// behind. Uploading has to wait until the task exists, though - ClickUp
|
|
641
659
|
// attachments always belong to a task.
|
|
642
660
|
const { markdown: normalizedDescription, images } = await resolveImagesOrAbort(description, "the task was NOT created");
|
|
661
|
+
// ClickUp's `parent` field only takes an internal ID: a custom ID gets a bare
|
|
662
|
+
// 500 Internal Server Error (verified live). Resolved before the task exists,
|
|
663
|
+
// so an unknown parent leaves nothing behind.
|
|
664
|
+
if (parent_task_id !== undefined) {
|
|
665
|
+
parent_task_id = await (0, utils_1.resolveTaskId)(parent_task_id);
|
|
666
|
+
}
|
|
643
667
|
const userData = await (0, utils_1.getCurrentUser)();
|
|
644
668
|
const currentUserId = userData.user.id;
|
|
645
669
|
const requestBody = buildTaskRequestBody({
|
|
@@ -659,7 +683,7 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
659
683
|
});
|
|
660
684
|
if (!response.ok) {
|
|
661
685
|
const errorData = await response.json().catch(() => ({}));
|
|
662
|
-
throw new Error(`
|
|
686
|
+
throw new Error(`ClickUp API ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
663
687
|
}
|
|
664
688
|
const createdTask = await response.json();
|
|
665
689
|
// Tags are omitted from the create body by buildTaskRequestBody because they
|
|
@@ -758,15 +782,85 @@ function convertPriorityToNumber(priority) {
|
|
|
758
782
|
default: return 3;
|
|
759
783
|
}
|
|
760
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* The task payload carries the priority NAME (`priority: { id: "4", priority: "low" }`),
|
|
787
|
+
* so a numeric-only lookup turned every real priority into "unknown". Accept both.
|
|
788
|
+
*/
|
|
761
789
|
function convertPriorityToString(priority) {
|
|
790
|
+
if (typeof priority === 'string' && ['urgent', 'high', 'normal', 'low'].includes(priority.toLowerCase())) {
|
|
791
|
+
return priority.toLowerCase();
|
|
792
|
+
}
|
|
762
793
|
const priorityMap = { 1: 'urgent', 2: 'high', 3: 'normal', 4: 'low' };
|
|
763
|
-
return priorityMap[priority] || 'unknown';
|
|
794
|
+
return priorityMap[Number(priority)] || 'unknown';
|
|
764
795
|
}
|
|
765
796
|
function formatTimeEstimate(hours) {
|
|
766
797
|
const displayHours = Math.floor(hours);
|
|
767
798
|
const displayMinutes = Math.round((hours - displayHours) * 60);
|
|
768
799
|
return displayHours > 0 ? `${displayHours}h ${displayMinutes}m` : `${displayMinutes}m`;
|
|
769
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* Match the image references of an edited comment against the images the comment
|
|
803
|
+
* already contains, and return the stored attachment object for each of them.
|
|
804
|
+
*
|
|
805
|
+
* Verified live on `PUT /comment/{id}`: an image fragment is only accepted when it
|
|
806
|
+
* carries the real attachment of an image that is ALREADY in that comment. Any
|
|
807
|
+
* other attachment - a fresh upload, the task description's image, an image of
|
|
808
|
+
* another comment - is refused with 401 ACCESS_610, and a fragment whose id is not
|
|
809
|
+
* a real attachment id fails with 404 ACCESS_028. Reusing the stored object (real
|
|
810
|
+
* id, dimensions included) is therefore the only working way to keep an image, and
|
|
811
|
+
* anything else is rejected here, before a request is made - uploading first would
|
|
812
|
+
* only leave an orphan attachment on the task behind the failed edit.
|
|
813
|
+
*/
|
|
814
|
+
function keepOwnCommentImages(existing, markdown, abortNotice) {
|
|
815
|
+
const sources = (0, clickup_text_1.collectMarkdownImageSources)(markdown);
|
|
816
|
+
if (sources.length === 0) {
|
|
817
|
+
return [];
|
|
818
|
+
}
|
|
819
|
+
const ownImages = new Map();
|
|
820
|
+
for (const fragment of existing.comment ?? []) {
|
|
821
|
+
if (fragment?.type === "image" && fragment.image?.url) {
|
|
822
|
+
ownImages.set(normalizeUrl(fragment.image.url), fragment.image);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const kept = [];
|
|
826
|
+
const refused = [];
|
|
827
|
+
const seen = new Set();
|
|
828
|
+
for (const { src } of sources) {
|
|
829
|
+
if (seen.has(src)) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
seen.add(src);
|
|
833
|
+
const stored = ownImages.get(normalizeUrl(src));
|
|
834
|
+
if (stored) {
|
|
835
|
+
kept.push({ src, attachment: stored });
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
refused.push(src);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (refused.length > 0) {
|
|
842
|
+
throw new Error([
|
|
843
|
+
`${refused.length} image reference(s) are not images of this comment, so ${abortNotice}:`,
|
|
844
|
+
...refused.map((src) => ` - ${summarizeImageSource(src)}`),
|
|
845
|
+
`ClickUp only lets an edit keep the images already in the comment - it refuses to add any other image.`,
|
|
846
|
+
`Remove these references, or post the new image in a new comment with addComment.`,
|
|
847
|
+
].join("\n"));
|
|
848
|
+
}
|
|
849
|
+
return kept;
|
|
850
|
+
}
|
|
851
|
+
/** Compare attachment URLs regardless of percent-encoding of the filename */
|
|
852
|
+
function normalizeUrl(url) {
|
|
853
|
+
try {
|
|
854
|
+
return decodeURI(url.trim());
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
return url.trim();
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/** Shorten a data URI so an error message never repeats a base64 payload */
|
|
861
|
+
function summarizeImageSource(src) {
|
|
862
|
+
return src.startsWith("data:") ? `${src.slice(0, 40)}... (data URI)` : src;
|
|
863
|
+
}
|
|
770
864
|
/**
|
|
771
865
|
* Load a single comment of a task.
|
|
772
866
|
*
|
|
@@ -800,6 +894,10 @@ async function findTaskComment(taskId, commentId) {
|
|
|
800
894
|
}
|
|
801
895
|
checked += page.length;
|
|
802
896
|
sawThreadedReplies || (sawThreadedReplies = page.some((entry) => (entry.reply_count ?? 0) > 0));
|
|
897
|
+
// A short page is the last one - requesting the next would only return [].
|
|
898
|
+
if (page.length < comments_1.COMMENTS_PER_PAGE) {
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
803
901
|
// Comments come back newest first, so once a page runs past the edit window
|
|
804
902
|
// there is nothing editable further back.
|
|
805
903
|
const oldest = page[page.length - 1];
|
|
@@ -853,6 +951,35 @@ function timestampToIso(timestamp) {
|
|
|
853
951
|
const timezoneOffset = sign + String(offsetHours).padStart(2, '0') + ':' + String(offsetMinutes).padStart(2, '0');
|
|
854
952
|
return `${year}-${month}-${day}T${hours}:${minutes}${timezoneOffset}`;
|
|
855
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Convert a due/start date parameter for the task API.
|
|
956
|
+
*
|
|
957
|
+
* ClickUp drops the time of day unless `due_date_time`/`start_date_time` is true
|
|
958
|
+
* (verified live: 18:00 came back as 04:00, its all-day placeholder), so report
|
|
959
|
+
* whether the caller gave a time. A bare date (YYYY-MM-DD) is an all-day value at
|
|
960
|
+
* local midnight. Invalid or impossible dates throw instead of being sent as null.
|
|
961
|
+
*/
|
|
962
|
+
function parseTaskDate(value, field) {
|
|
963
|
+
return (0, dates_1.parseDateInput)(value, field, "start");
|
|
964
|
+
}
|
|
965
|
+
/** Validate due/start dates up front so a bad value never reaches a write */
|
|
966
|
+
function assertValidTaskDates(dates) {
|
|
967
|
+
for (const field of ["due_date", "start_date"]) {
|
|
968
|
+
const value = dates[field];
|
|
969
|
+
if (typeof value === "string") {
|
|
970
|
+
parseTaskDate(value, field);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
/** Resolve every ID of a relation list to an internal ID, without duplicates */
|
|
975
|
+
async function resolveTaskIdList(ids) {
|
|
976
|
+
if (ids === undefined) {
|
|
977
|
+
return undefined;
|
|
978
|
+
}
|
|
979
|
+
// Deduplicate before resolving too: each custom ID costs one API call
|
|
980
|
+
const resolved = await Promise.all([...new Set(ids)].map((id) => (0, utils_1.resolveTaskId)(id)));
|
|
981
|
+
return [...new Set(resolved)];
|
|
982
|
+
}
|
|
856
983
|
function buildTaskRequestBody(params, currentUserId) {
|
|
857
984
|
const requestBody = {};
|
|
858
985
|
if (params.name !== undefined) {
|
|
@@ -867,10 +994,24 @@ function buildTaskRequestBody(params, currentUserId) {
|
|
|
867
994
|
// null clears the value - it must be sent as is, new Date(null) would set 1970-01-01.
|
|
868
995
|
// time_estimate is the exception: ClickUp accepts null but silently keeps the estimate, only 0 clears it.
|
|
869
996
|
if (params.due_date !== undefined) {
|
|
870
|
-
|
|
997
|
+
if (params.due_date === null) {
|
|
998
|
+
requestBody.due_date = null;
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
const due = parseTaskDate(params.due_date, 'due_date');
|
|
1002
|
+
requestBody.due_date = due.timestamp;
|
|
1003
|
+
requestBody.due_date_time = due.hasTime;
|
|
1004
|
+
}
|
|
871
1005
|
}
|
|
872
1006
|
if (params.start_date !== undefined) {
|
|
873
|
-
|
|
1007
|
+
if (params.start_date === null) {
|
|
1008
|
+
requestBody.start_date = null;
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
const start = parseTaskDate(params.start_date, 'start_date');
|
|
1012
|
+
requestBody.start_date = start.timestamp;
|
|
1013
|
+
requestBody.start_date_time = start.hasTime;
|
|
1014
|
+
}
|
|
874
1015
|
}
|
|
875
1016
|
if (params.time_estimate !== undefined) {
|
|
876
1017
|
requestBody.time_estimate = params.time_estimate === null ? 0 : Math.round(params.time_estimate * 60 * 60 * 1000);
|
|
@@ -894,9 +1035,18 @@ function buildTaskRequestBody(params, currentUserId) {
|
|
|
894
1035
|
// Helper function to manage task dependencies
|
|
895
1036
|
async function updateTaskDependencies(taskId, taskData, dependencies) {
|
|
896
1037
|
const errors = [];
|
|
897
|
-
// Get current dependencies
|
|
898
|
-
|
|
899
|
-
|
|
1038
|
+
// Get current dependencies. The task payload has no `blocking`/`waiting_on`
|
|
1039
|
+
// arrays (verified live): both directions live in one flat `dependencies` array
|
|
1040
|
+
// of `{ task_id, depends_on }` records, and this task can sit on either side.
|
|
1041
|
+
// Reading the missing fields made every current dependency look absent, so
|
|
1042
|
+
// `waiting_on: []`/`blocking: []` never removed anything and reported no error.
|
|
1043
|
+
const dependencyRecords = Array.isArray(taskData.dependencies) ? taskData.dependencies : [];
|
|
1044
|
+
const currentWaitingOn = dependencyRecords
|
|
1045
|
+
.filter((dep) => dep.task_id === taskId && dep.depends_on)
|
|
1046
|
+
.map((dep) => dep.depends_on);
|
|
1047
|
+
const currentBlocking = dependencyRecords
|
|
1048
|
+
.filter((dep) => dep.depends_on === taskId && dep.task_id)
|
|
1049
|
+
.map((dep) => dep.task_id);
|
|
900
1050
|
// `linked_tasks` entries are link records, not tasks: each has `task_id` and
|
|
901
1051
|
// `link_id` (the two ends of the link) and no `id` at all. Mapping `.id` here
|
|
902
1052
|
// produced `[undefined, ...]`, so every existing link looked like it was no
|
|
@@ -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;
|
|
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;AAoEpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAqFtD;AA6OD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAmPvD"}
|
package/dist/tools/time-tools.js
CHANGED
|
@@ -5,12 +5,22 @@ exports.registerTimeToolsWrite = registerTimeToolsWrite;
|
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const config_1 = require("../shared/config");
|
|
7
7
|
const utils_1 = require("../shared/utils");
|
|
8
|
+
const dates_1 = require("../shared/dates");
|
|
8
9
|
/**
|
|
9
10
|
* Converts ISO date string to Unix timestamp in milliseconds
|
|
10
11
|
*/
|
|
11
12
|
function isoToTimestamp(isoString) {
|
|
12
13
|
return new Date(isoString).getTime();
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Convert a date range filter bound. A bare date (YYYY-MM-DD) means the whole
|
|
17
|
+
* local day: `new Date("2026-10-01")` is UTC midnight, so as an end bound it used
|
|
18
|
+
* to exclude the very day the summary header announced ("2026-10-01 to 2026-10-01").
|
|
19
|
+
* Invalid or impossible dates throw instead of sending `NaN` to the API.
|
|
20
|
+
*/
|
|
21
|
+
function dateFilterToTimestamp(value, bound) {
|
|
22
|
+
return (0, dates_1.parseDateInput)(value, bound === "start" ? "start_date" : "end_date", bound).timestamp;
|
|
23
|
+
}
|
|
14
24
|
/**
|
|
15
25
|
* Formats timestamp to ISO string with local timezone (not UTC)
|
|
16
26
|
*/
|
|
@@ -57,8 +67,8 @@ function registerTimeToolsRead(server) {
|
|
|
57
67
|
task_id: zod_1.z.string().min(1).refine(val => (0, utils_1.isTaskId)(val) || (0, utils_1.isCustomTaskId)(val), {
|
|
58
68
|
message: "Must be an internal task ID (6-16 alphanumeric characters) or a custom task ID (e.g. SOI-4422)"
|
|
59
69
|
}).optional().describe("Optional task ID to filter entries: internal ID (e.g. \"869c4za0g\") or custom ID (e.g. \"SOI-4422\"). If not provided, returns all user's time entries."),
|
|
60
|
-
start_date: zod_1.z.string().optional().describe("Optional start date filter as ISO date string (e.g., '2024-10-06T00:00:00+02:00'). Defaults to 30 days ago."),
|
|
61
|
-
end_date: zod_1.z.string().optional().describe("Optional end date filter as ISO date string (e.g., '2024-10-06T23:59:59+02:00'). Defaults to current date."),
|
|
70
|
+
start_date: zod_1.z.string().optional().describe("Optional start date filter as ISO date string (e.g., '2024-10-06T00:00:00+02:00') or a bare date (e.g., '2024-10-06', from the start of that local day). Defaults to 30 days ago."),
|
|
71
|
+
end_date: zod_1.z.string().optional().describe("Optional end date filter as ISO date string (e.g., '2024-10-06T23:59:59+02:00') or a bare date (e.g., '2024-10-06', up to the end of that local day). Defaults to current date."),
|
|
62
72
|
list_id: zod_1.z.string().optional().describe("Optional single list ID to filter time entries by a specific list"),
|
|
63
73
|
space_id: zod_1.z.string().optional().describe("Optional single space ID to filter time entries by a specific space"),
|
|
64
74
|
include_all_users: zod_1.z.boolean().optional().describe("Optional flag to include time entries from all team members (default: false, only current user)")
|
|
@@ -72,10 +82,10 @@ function registerTimeToolsRead(server) {
|
|
|
72
82
|
params.append('task_id', await (0, utils_1.resolveTaskId)(task_id));
|
|
73
83
|
}
|
|
74
84
|
if (start_date) {
|
|
75
|
-
params.append('start_date',
|
|
85
|
+
params.append('start_date', dateFilterToTimestamp(start_date, "start").toString());
|
|
76
86
|
}
|
|
77
87
|
if (end_date) {
|
|
78
|
-
params.append('end_date',
|
|
88
|
+
params.append('end_date', dateFilterToTimestamp(end_date, "end").toString());
|
|
79
89
|
}
|
|
80
90
|
// Add single list_id or space_id filter (not both)
|
|
81
91
|
if (list_id) {
|
package/package.json
CHANGED