@sebastienheyd/clickup-mcp 1.7.4 → 1.8.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 +37 -1
- package/dist/cli.js +3 -1
- package/dist/clickup-text.d.ts +82 -2
- package/dist/clickup-text.d.ts.map +1 -1
- package/dist/clickup-text.js +192 -21
- package/dist/shared/attachments.d.ts +112 -0
- package/dist/shared/attachments.d.ts.map +1 -0
- package/dist/shared/attachments.js +268 -0
- package/dist/shared/config.d.ts +2 -0
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +23 -0
- package/dist/shared/image-processing.d.ts +5 -0
- package/dist/shared/image-processing.d.ts.map +1 -1
- package/dist/shared/image-processing.js +1 -0
- package/dist/tools/task-write-tools.d.ts.map +1 -1
- package/dist/tools/task-write-tools.js +323 -4
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Model Context Protocol (MCP) server enabling AI assistants to interact with Clic
|
|
|
13
13
|
| **Setup** | Local npm/npx install | Remote MCP (no install) |
|
|
14
14
|
| **Authentication** | API key only | OAuth only |
|
|
15
15
|
| **Task Context** | Complete with comments, status history, inline images | Requires mutiple tool calls for full contxt |
|
|
16
|
-
| **Image Support** |
|
|
16
|
+
| **Image Support** | Read and write: inline images with smart size budgeting, and `` uploads automatically | Upload via separate tool calls; base64 capped at ~200KB |
|
|
17
17
|
| **Search** | Fuzzy search on recent tasks (limited scope) | Full ClickUp search database |
|
|
18
18
|
| **Documents** | CRUD operations | CRUD + document search |
|
|
19
19
|
| **Time Tracking** | View and create entries | Timers and entries |
|
|
@@ -24,6 +24,7 @@ Model Context Protocol (MCP) server enabling AI assistants to interact with Clic
|
|
|
24
24
|
|
|
25
25
|
**Choose this MCP when:**
|
|
26
26
|
- You need rich task context with inline images for AI coding tools
|
|
27
|
+
- You want to write screenshots into tickets by local file path (running locally, it reads the file itself instead of taking base64)
|
|
27
28
|
- You need API key authentication for automation or CI/CD pipelines
|
|
28
29
|
- You want the `read-minimal` mode optimized for development workflows
|
|
29
30
|
|
|
@@ -189,6 +190,7 @@ The ClickUp MCP supports three operational modes to balance functionality, secur
|
|
|
189
190
|
|------------------------|:------------:|:----:|:-----:|-----------------------------------------------------------------------------------------|
|
|
190
191
|
| `getTaskById` | ✅ | ✅ | ✅ | Get complete task details including comments, images, and metadata |
|
|
191
192
|
| `addComment` | ❌ | ❌ | ✅ | Add comments to tasks for collaboration |
|
|
193
|
+
| `editComment` | ❌ | ❌ | ✅ | Correct your own comment within 24h instead of posting a follow-up |
|
|
192
194
|
| `updateTask` | ❌ | ❌ | ✅ | Update tasks (status, priority, assignees, etc.) with **SAFE APPEND-ONLY** descriptions |
|
|
193
195
|
| `createTask` | ❌ | ❌ | ✅ | Create new tasks with full markdown support |
|
|
194
196
|
| `searchTasks` | ✅ | ✅ | ✅ | Find tasks by content, keywords, assignees, or project context |
|
|
@@ -231,6 +233,8 @@ This MCP server can be configured using environment variables:
|
|
|
231
233
|
- `CLICKUP_MCP_MODE`: (Optional) Controls which tools are available. Options: `read-minimal`, `read`, `write` (default).
|
|
232
234
|
- `MAX_IMAGES`: (Optional) The maximum number of images to return for a task in `getTaskById`. Defaults to 4.
|
|
233
235
|
- `MAX_RESPONSE_SIZE_MB`: (Optional) The maximum response size in megabytes for `getTaskById`. Uses intelligent size budgeting to fit the most important images within the limit. Defaults to 1.
|
|
236
|
+
- `MAX_UPLOAD_SIZE_MB`: (Optional) The maximum size of a single image uploaded when writing comments or descriptions. Defaults to 10.
|
|
237
|
+
- `CLICKUP_COMMENT_EDIT_WINDOW_HOURS`: (Optional) How long after creation `editComment` may still rewrite a comment. Defaults to 24. Set to `0` to disable comment editing entirely.
|
|
234
238
|
- `CLICKUP_PRIMARY_LANGUAGE`: (Optional) A hint for the primary language used in your ClickUp tasks (e.g., "de" for German, "en" for English). This helps the `searchTask` tool provide more tailored guidance in its description for multilingual searches.
|
|
235
239
|
- `LANG`: (Optional) If `CLICKUP_PRIMARY_LANGUAGE` is not set, the MCP will check this standard environment variable (e.g., "en_US.UTF-8", "de_DE") as a fallback to infer the primary language.
|
|
236
240
|
|
|
@@ -286,6 +290,38 @@ When updating task descriptions, content is safely appended:
|
|
|
286
290
|
|
|
287
291
|
This ensures no existing content is ever lost while maintaining a clear audit trail.
|
|
288
292
|
|
|
293
|
+
## Writing Images Into Tickets
|
|
294
|
+
|
|
295
|
+
`addComment`, `editComment`, `createTask` and `updateTask` accept images as ordinary markdown. Because
|
|
296
|
+
this server runs locally, it reads the file itself - so a **local path is enough**:
|
|
297
|
+
|
|
298
|
+
```markdown
|
|
299
|
+
Ist umgesetzt. So sieht es aus:
|
|
300
|
+
|
|
301
|
+
**1. Login öffnen** – der Kunde gibt nur seine E-Mail-Adresse ein.
|
|
302
|
+
|
|
303
|
+

|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Accepted sources: local file paths, `data:` URIs, http(s) URLs (downloaded, then
|
|
307
|
+
re-uploaded), and existing ClickUp attachment URLs (embedded without re-uploading).
|
|
308
|
+
|
|
309
|
+
Notes:
|
|
310
|
+
|
|
311
|
+
- **Prefer paths over base64.** A path costs a few tokens; the same screenshot as a
|
|
312
|
+
`data:` URI costs roughly 4/3 of its file size in the request.
|
|
313
|
+
- **The caption becomes the attachment filename**, and that filename is what ClickUp
|
|
314
|
+
displays beneath the image - so write a caption that reads well.
|
|
315
|
+
- **An image inside a numbered list breaks ClickUp's numbering.** Write walkthrough
|
|
316
|
+
steps as bold lines with the image between them, as above.
|
|
317
|
+
- Only real PNG/JPEG/GIF/WebP files are uploaded - the content is checked, not the
|
|
318
|
+
extension. A file that fails **aborts the write**: `addComment`, `editComment` and
|
|
319
|
+
`updateTask` report every broken reference and change nothing, so the markdown can be
|
|
320
|
+
fixed and the call retried without creating duplicates. `createTask` validates its
|
|
321
|
+
images before creating the task; only an upload failing afterwards is reported as a
|
|
322
|
+
warning, since the task already exists at that point.
|
|
323
|
+
- Attachments always belong to a task, so document pages cannot embed uploads this way.
|
|
324
|
+
|
|
289
325
|
## Performance & Limitations
|
|
290
326
|
|
|
291
327
|
**Optimized for AI Workflows:**
|
package/dist/cli.js
CHANGED
|
@@ -116,7 +116,9 @@ async function main() {
|
|
|
116
116
|
// Parse parameters
|
|
117
117
|
for (let i = 1; i < args.length; i++) {
|
|
118
118
|
const arg = args[i];
|
|
119
|
-
|
|
119
|
+
// The `s` flag matters: without it `.` stops at a newline and multi-line values
|
|
120
|
+
// (markdown descriptions, comments with images) are silently skipped entirely.
|
|
121
|
+
const match = arg.match(/^([^=]+)=(.*)$/s);
|
|
120
122
|
if (match) {
|
|
121
123
|
const [, key, value] = match;
|
|
122
124
|
// Try to parse as JSON if it looks like a JSON value
|
package/dist/clickup-text.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ import { ImageMetadataBlock } from "./shared/types";
|
|
|
6
6
|
export interface ClickUpTextItem {
|
|
7
7
|
text?: string;
|
|
8
8
|
type?: string;
|
|
9
|
+
task_mention?: {
|
|
10
|
+
task_id?: string;
|
|
11
|
+
};
|
|
9
12
|
image?: {
|
|
10
13
|
id?: string;
|
|
11
14
|
name?: string;
|
|
@@ -67,17 +70,94 @@ export interface ClickUpCommentBlock {
|
|
|
67
70
|
};
|
|
68
71
|
indent?: number;
|
|
69
72
|
'block-id'?: string;
|
|
73
|
+
alt?: string;
|
|
70
74
|
};
|
|
71
75
|
list?: {
|
|
72
76
|
list: 'bullet' | 'ordered' | 'unchecked' | 'checked';
|
|
73
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* Present on task mention fragments. ClickUp renders these as live task references
|
|
80
|
+
* showing the current task name, status and assignee.
|
|
81
|
+
*/
|
|
82
|
+
task_mention?: {
|
|
83
|
+
task_id: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Present on image fragments. ClickUp only renders a preview when this holds the
|
|
87
|
+
* complete attachment object from the upload response - a bare URL string produces
|
|
88
|
+
* an empty placeholder tile.
|
|
89
|
+
*/
|
|
90
|
+
image?: {
|
|
91
|
+
id?: string;
|
|
92
|
+
name?: string;
|
|
93
|
+
title?: string;
|
|
94
|
+
extension?: string;
|
|
95
|
+
url: string;
|
|
96
|
+
thumbnail_small?: string;
|
|
97
|
+
thumbnail_medium?: string;
|
|
98
|
+
thumbnail_large?: string;
|
|
99
|
+
width?: number;
|
|
100
|
+
height?: number;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Minimal shape needed to embed an already-uploaded attachment as an image fragment
|
|
105
|
+
*/
|
|
106
|
+
export interface EmbeddableAttachment {
|
|
107
|
+
id?: string;
|
|
108
|
+
name?: string;
|
|
109
|
+
title?: string;
|
|
110
|
+
extension?: string;
|
|
111
|
+
url: string;
|
|
112
|
+
thumbnail_small?: string;
|
|
113
|
+
thumbnail_medium?: string;
|
|
114
|
+
thumbnail_large?: string;
|
|
115
|
+
width?: number;
|
|
116
|
+
height?: number;
|
|
117
|
+
[key: string]: any;
|
|
74
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Build the image fragment ClickUp needs to render an inline image in a comment.
|
|
121
|
+
* `title`/`text` carry the caption; the rest is copied straight from the upload response.
|
|
122
|
+
*/
|
|
123
|
+
export declare function buildImageFragment(attachment: EmbeddableAttachment, caption: string): ClickUpCommentBlock;
|
|
124
|
+
/**
|
|
125
|
+
* Extract the task ID from a ClickUp task URL, or null if it is not one.
|
|
126
|
+
*/
|
|
127
|
+
export declare function parseClickUpTaskUrl(url: string): string | null;
|
|
128
|
+
/**
|
|
129
|
+
* Wrap image destinations that contain spaces in angle brackets.
|
|
130
|
+
*
|
|
131
|
+
* CommonMark rejects a bare destination with spaces, so ``
|
|
132
|
+
* is not an image at all - it would silently stay literal text and never be uploaded.
|
|
133
|
+
* Screenshot filenames have spaces constantly ("Screenshot 2026-07-27 at 14.30.png"),
|
|
134
|
+
* so normalising to the `<...>` form is what makes the obvious thing work.
|
|
135
|
+
*/
|
|
136
|
+
export declare function normalizeImageDestinations(markdown: string): string;
|
|
137
|
+
/**
|
|
138
|
+
* Collect every image reference in a markdown document, in document order.
|
|
139
|
+
* Callers use this to know what needs uploading before converting.
|
|
140
|
+
*/
|
|
141
|
+
export declare function collectMarkdownImageSources(markdown: string): {
|
|
142
|
+
src: string;
|
|
143
|
+
alt: string;
|
|
144
|
+
}[];
|
|
145
|
+
/**
|
|
146
|
+
* Replace image sources in markdown with their uploaded ClickUp URLs.
|
|
147
|
+
*
|
|
148
|
+
* Used for task descriptions: `markdown_description` renders `` directly,
|
|
149
|
+
* so descriptions need no fragment handling - only the URL has to be swapped.
|
|
150
|
+
* Images without an upload keep their original source untouched.
|
|
151
|
+
*/
|
|
152
|
+
export declare function rewriteMarkdownImageUrls(markdown: string, attachmentsBySrc: Map<string, EmbeddableAttachment>): string;
|
|
75
153
|
/**
|
|
76
154
|
* Convert markdown text to ClickUp comment blocks format using remark
|
|
77
|
-
* Supports: headers, bold, italic, code, links, lists, blockquotes, code blocks
|
|
155
|
+
* Supports: headers, bold, italic, code, links, lists, blockquotes, code blocks, images
|
|
78
156
|
*
|
|
79
157
|
* @param markdown The markdown text to convert
|
|
158
|
+
* @param attachmentsBySrc Uploaded attachments keyed by the markdown `src` they came from.
|
|
159
|
+
* Images without an entry degrade to a link so their information is not lost.
|
|
80
160
|
* @returns Array of ClickUp comment blocks
|
|
81
161
|
*/
|
|
82
|
-
export declare function convertMarkdownToClickUpBlocks(markdown: string): ClickUpCommentBlock[];
|
|
162
|
+
export declare function convertMarkdownToClickUpBlocks(markdown: string, attachmentsBySrc?: Map<string, EmbeddableAttachment>): ClickUpCommentBlock[];
|
|
83
163
|
//# sourceMappingURL=clickup-text.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clickup-text.d.ts","sourceRoot":"","sources":["../src/clickup-text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAOpD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"clickup-text.d.ts","sourceRoot":"","sources":["../src/clickup-text.ts"],"names":[],"mappings":"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"}
|
package/dist/clickup-text.js
CHANGED
|
@@ -5,6 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.convertClickUpTextItemsToToolCallResult = convertClickUpTextItemsToToolCallResult;
|
|
7
7
|
exports.convertMarkdownToToolCallResult = convertMarkdownToToolCallResult;
|
|
8
|
+
exports.buildImageFragment = buildImageFragment;
|
|
9
|
+
exports.parseClickUpTaskUrl = parseClickUpTaskUrl;
|
|
10
|
+
exports.normalizeImageDestinations = normalizeImageDestinations;
|
|
11
|
+
exports.collectMarkdownImageSources = collectMarkdownImageSources;
|
|
12
|
+
exports.rewriteMarkdownImageUrls = rewriteMarkdownImageUrls;
|
|
8
13
|
exports.convertMarkdownToClickUpBlocks = convertMarkdownToClickUpBlocks;
|
|
9
14
|
const data_uri_1 = require("./shared/data-uri");
|
|
10
15
|
const unified_1 = require("unified");
|
|
@@ -31,6 +36,18 @@ function extractThumbnailsFromDataAttachment(attributes) {
|
|
|
31
36
|
return {};
|
|
32
37
|
}
|
|
33
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Render an image reference as markdown, escaping whatever would break the syntax.
|
|
41
|
+
*
|
|
42
|
+
* Reading and writing use the same markdown here on purpose: it lets an agent feed a
|
|
43
|
+
* comment it just read straight back into editComment without losing the images.
|
|
44
|
+
*/
|
|
45
|
+
function toMarkdownImage(alt, url) {
|
|
46
|
+
const safeAlt = alt.replace(/[\[\]\r\n]/g, " ").trim();
|
|
47
|
+
// Angle brackets let a URL with spaces or parentheses survive the round trip
|
|
48
|
+
const safeUrl = /[\s()]/.test(url) ? `<${url}>` : url;
|
|
49
|
+
return ``;
|
|
50
|
+
}
|
|
34
51
|
/**
|
|
35
52
|
* Process an array of ClickUp text items into a structured content format
|
|
36
53
|
* that includes both text and images in their original sequence
|
|
@@ -80,8 +97,10 @@ async function convertClickUpTextItemsToToolCallResult(textItems) {
|
|
|
80
97
|
}
|
|
81
98
|
continue;
|
|
82
99
|
}
|
|
83
|
-
//
|
|
84
|
-
|
|
100
|
+
// Reference the image in the same markdown syntax the write tools accept, so a
|
|
101
|
+
// comment read here can be handed back to editComment unchanged and keep its
|
|
102
|
+
// images - an existing ClickUp attachment URL is re-embedded without re-uploading.
|
|
103
|
+
currentTextBlock += `\n${toMarkdownImage(altText, imageUrl)}`;
|
|
85
104
|
// Get working thumbnail URLs from data-attachment if available
|
|
86
105
|
const extractedThumbnails = extractThumbnailsFromDataAttachment(item.attributes);
|
|
87
106
|
// Determine best thumbnail URLs (prefer extracted over API thumbnails)
|
|
@@ -215,6 +234,11 @@ async function convertClickUpTextItemsToToolCallResult(textItems) {
|
|
|
215
234
|
currentLine += formattedText;
|
|
216
235
|
}
|
|
217
236
|
}
|
|
237
|
+
// Task mentions render as the task URL so the reference survives the round trip:
|
|
238
|
+
// writing that URL back through addComment/editComment regenerates the mention.
|
|
239
|
+
else if (item.type === "task_mention" && item.task_mention?.task_id) {
|
|
240
|
+
currentLine += `https://app.clickup.com/t/${item.task_mention.task_id}`;
|
|
241
|
+
}
|
|
218
242
|
// Handle other types of items like bookmarks or whatever clickup can think of
|
|
219
243
|
else {
|
|
220
244
|
currentTextBlock += JSON.stringify(item);
|
|
@@ -288,9 +312,9 @@ function convertMarkdownToToolCallResult(markdownText, attachments) {
|
|
|
288
312
|
// Check if this image URL exists in our attachments
|
|
289
313
|
const attachment = attachmentMap.get(imageUrl);
|
|
290
314
|
if (attachment) {
|
|
291
|
-
//
|
|
315
|
+
// Keep the markdown syntax, so the reference stays usable in a write call
|
|
292
316
|
const imageFileName = altText || "image";
|
|
293
|
-
currentTextBlock += `\
|
|
317
|
+
currentTextBlock += `\n${toMarkdownImage(imageFileName, imageUrl)}`;
|
|
294
318
|
// Only create image_metadata if we have at least one thumbnail (never use original image)
|
|
295
319
|
if (attachment.thumbnail_large || attachment.thumbnail_medium || attachment.thumbnail_small) {
|
|
296
320
|
// Push accumulated text (including image URL) as a text block
|
|
@@ -380,14 +404,132 @@ function extractFileTypeFromUrl(url) {
|
|
|
380
404
|
return null;
|
|
381
405
|
return filename.substring(lastDot + 1);
|
|
382
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Build the image fragment ClickUp needs to render an inline image in a comment.
|
|
409
|
+
* `title`/`text` carry the caption; the rest is copied straight from the upload response.
|
|
410
|
+
*/
|
|
411
|
+
function buildImageFragment(attachment, caption) {
|
|
412
|
+
const label = caption || attachment.name || 'image';
|
|
413
|
+
const fragment = {
|
|
414
|
+
type: 'image',
|
|
415
|
+
text: label,
|
|
416
|
+
image: {
|
|
417
|
+
id: attachment.id,
|
|
418
|
+
name: attachment.name,
|
|
419
|
+
title: label,
|
|
420
|
+
extension: attachment.extension,
|
|
421
|
+
url: attachment.url,
|
|
422
|
+
thumbnail_small: attachment.thumbnail_small,
|
|
423
|
+
thumbnail_medium: attachment.thumbnail_medium,
|
|
424
|
+
thumbnail_large: attachment.thumbnail_large,
|
|
425
|
+
width: attachment.width,
|
|
426
|
+
height: attachment.height,
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
if (caption) {
|
|
430
|
+
fragment.attributes = { alt: caption };
|
|
431
|
+
}
|
|
432
|
+
return fragment;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Matches a plain ClickUp task URL, with or without the team segment:
|
|
436
|
+
* https://app.clickup.com/t/86cb3t6t2 or https://app.clickup.com/t/4500611/86cb3t6t2
|
|
437
|
+
*
|
|
438
|
+
* Deliberately narrow: custom task IDs (PREFIX-123) and URLs carrying a query or
|
|
439
|
+
* fragment (e.g. ?comment=... deep links) do NOT match, because a mention would
|
|
440
|
+
* either not resolve or lose the anchor - those stay ordinary links.
|
|
441
|
+
*/
|
|
442
|
+
const CLICKUP_TASK_URL_PATTERN = /^https?:\/\/app\.clickup\.com\/t\/(?:\d+\/)?([a-z0-9]{6,12})\/?$/;
|
|
443
|
+
/**
|
|
444
|
+
* Extract the task ID from a ClickUp task URL, or null if it is not one.
|
|
445
|
+
*/
|
|
446
|
+
function parseClickUpTaskUrl(url) {
|
|
447
|
+
const match = url.match(CLICKUP_TASK_URL_PATTERN);
|
|
448
|
+
return match ? match[1] : null;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Wrap image destinations that contain spaces in angle brackets.
|
|
452
|
+
*
|
|
453
|
+
* CommonMark rejects a bare destination with spaces, so ``
|
|
454
|
+
* is not an image at all - it would silently stay literal text and never be uploaded.
|
|
455
|
+
* Screenshot filenames have spaces constantly ("Screenshot 2026-07-27 at 14.30.png"),
|
|
456
|
+
* so normalising to the `<...>` form is what makes the obvious thing work.
|
|
457
|
+
*/
|
|
458
|
+
function normalizeImageDestinations(markdown) {
|
|
459
|
+
return markdown.replace(/!\[([^\]]*)\]\(([^)\n]*)\)/g, (match, alt, inner) => {
|
|
460
|
+
const trimmed = inner.trim();
|
|
461
|
+
// Already bracketed, or nothing to fix
|
|
462
|
+
if (trimmed.startsWith('<') || trimmed.includes('>')) {
|
|
463
|
+
return match;
|
|
464
|
+
}
|
|
465
|
+
// Split off an optional markdown title: dest "title" / 'title'
|
|
466
|
+
const titleMatch = trimmed.match(/^(.*?)(\s+(?:"[^"]*"|'[^']*'))$/s);
|
|
467
|
+
const dest = titleMatch ? titleMatch[1] : trimmed;
|
|
468
|
+
const title = titleMatch ? titleMatch[2] : '';
|
|
469
|
+
if (!dest || !/\s/.test(dest)) {
|
|
470
|
+
return match;
|
|
471
|
+
}
|
|
472
|
+
return ``;
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Collect every image reference in a markdown document, in document order.
|
|
477
|
+
* Callers use this to know what needs uploading before converting.
|
|
478
|
+
*/
|
|
479
|
+
function collectMarkdownImageSources(markdown) {
|
|
480
|
+
const images = [];
|
|
481
|
+
try {
|
|
482
|
+
const tree = (0, unified_1.unified)()
|
|
483
|
+
.use(remark_parse_1.default)
|
|
484
|
+
.use(remark_gfm_1.default)
|
|
485
|
+
.parse(markdown);
|
|
486
|
+
const visit = (nodes) => {
|
|
487
|
+
for (const node of nodes) {
|
|
488
|
+
if (node.type === 'image' && typeof node.url === 'string') {
|
|
489
|
+
images.push({ src: node.url, alt: typeof node.alt === 'string' ? node.alt : '' });
|
|
490
|
+
}
|
|
491
|
+
else if (Array.isArray(node.children)) {
|
|
492
|
+
visit(node.children);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
visit(tree.children);
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
console.error('Failed to collect markdown images:', error);
|
|
500
|
+
}
|
|
501
|
+
return images;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Replace image sources in markdown with their uploaded ClickUp URLs.
|
|
505
|
+
*
|
|
506
|
+
* Used for task descriptions: `markdown_description` renders `` directly,
|
|
507
|
+
* so descriptions need no fragment handling - only the URL has to be swapped.
|
|
508
|
+
* Images without an upload keep their original source untouched.
|
|
509
|
+
*/
|
|
510
|
+
function rewriteMarkdownImageUrls(markdown, attachmentsBySrc) {
|
|
511
|
+
if (attachmentsBySrc.size === 0) {
|
|
512
|
+
return markdown;
|
|
513
|
+
}
|
|
514
|
+
return markdown.replace(/!\[([^\]]*)\]\(\s*(<[^>]*>|[^)\s]+)([^)]*)\)/g, (match, alt, rawSrc, trailing) => {
|
|
515
|
+
const src = rawSrc.startsWith('<') && rawSrc.endsWith('>') ? rawSrc.slice(1, -1) : rawSrc;
|
|
516
|
+
const attachment = attachmentsBySrc.get(src);
|
|
517
|
+
if (!attachment) {
|
|
518
|
+
return match;
|
|
519
|
+
}
|
|
520
|
+
return ``;
|
|
521
|
+
});
|
|
522
|
+
}
|
|
383
523
|
/**
|
|
384
524
|
* Convert markdown text to ClickUp comment blocks format using remark
|
|
385
|
-
* Supports: headers, bold, italic, code, links, lists, blockquotes, code blocks
|
|
525
|
+
* Supports: headers, bold, italic, code, links, lists, blockquotes, code blocks, images
|
|
386
526
|
*
|
|
387
527
|
* @param markdown The markdown text to convert
|
|
528
|
+
* @param attachmentsBySrc Uploaded attachments keyed by the markdown `src` they came from.
|
|
529
|
+
* Images without an entry degrade to a link so their information is not lost.
|
|
388
530
|
* @returns Array of ClickUp comment blocks
|
|
389
531
|
*/
|
|
390
|
-
function convertMarkdownToClickUpBlocks(markdown) {
|
|
532
|
+
function convertMarkdownToClickUpBlocks(markdown, attachmentsBySrc) {
|
|
391
533
|
const blocks = [];
|
|
392
534
|
try {
|
|
393
535
|
// Parse the entire markdown document using remark with GFM support (for task lists)
|
|
@@ -396,7 +538,7 @@ function convertMarkdownToClickUpBlocks(markdown) {
|
|
|
396
538
|
.use(remark_gfm_1.default)
|
|
397
539
|
.parse(markdown);
|
|
398
540
|
// Walk the tree recursively
|
|
399
|
-
walkMdastNodes(tree.children, {}, blocks);
|
|
541
|
+
walkMdastNodes(tree.children, {}, blocks, 0, attachmentsBySrc);
|
|
400
542
|
}
|
|
401
543
|
catch (error) {
|
|
402
544
|
console.error('Failed to parse markdown:', error);
|
|
@@ -412,20 +554,20 @@ function convertMarkdownToClickUpBlocks(markdown) {
|
|
|
412
554
|
* @param blocks Output array to append ClickUp blocks to
|
|
413
555
|
* @param depth Nesting depth for lists (0 = top level, 1 = first nest, etc.)
|
|
414
556
|
*/
|
|
415
|
-
function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0) {
|
|
557
|
+
function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0, attachmentsBySrc) {
|
|
416
558
|
for (let i = 0; i < nodes.length; i++) {
|
|
417
559
|
const node = nodes[i];
|
|
418
560
|
const currentAttrs = { ...inheritedAttrs };
|
|
419
561
|
switch (node.type) {
|
|
420
562
|
case 'heading':
|
|
421
563
|
// Process heading content with inline formatting
|
|
422
|
-
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
564
|
+
walkPhrasingContent(node.children, currentAttrs, blocks, attachmentsBySrc);
|
|
423
565
|
// Add newline with header attribute
|
|
424
566
|
blocks.push({ text: '\n', attributes: { header: node.depth } });
|
|
425
567
|
break;
|
|
426
568
|
case 'paragraph':
|
|
427
569
|
// Process paragraph content with inline formatting
|
|
428
|
-
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
570
|
+
walkPhrasingContent(node.children, currentAttrs, blocks, attachmentsBySrc);
|
|
429
571
|
// Add newline unless it's the last node
|
|
430
572
|
if (i < nodes.length - 1) {
|
|
431
573
|
blocks.push({ text: '\n', attributes: {} });
|
|
@@ -437,7 +579,7 @@ function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0) {
|
|
|
437
579
|
const blockquoteChildren = node.children;
|
|
438
580
|
for (const child of blockquoteChildren) {
|
|
439
581
|
if (child.type === 'paragraph') {
|
|
440
|
-
walkPhrasingContent(child.children, currentAttrs, blocks);
|
|
582
|
+
walkPhrasingContent(child.children, currentAttrs, blocks, attachmentsBySrc);
|
|
441
583
|
blocks.push({ text: '\n', attributes: { blockquote: {} } });
|
|
442
584
|
}
|
|
443
585
|
// Note: Other child types (heading, list) are not supported by ClickUp blockquotes
|
|
@@ -457,7 +599,7 @@ function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0) {
|
|
|
457
599
|
for (const itemChild of listItem.children) {
|
|
458
600
|
if (itemChild.type === 'paragraph') {
|
|
459
601
|
// Process paragraph content with inline formatting
|
|
460
|
-
walkPhrasingContent(itemChild.children, currentAttrs, blocks);
|
|
602
|
+
walkPhrasingContent(itemChild.children, currentAttrs, blocks, attachmentsBySrc);
|
|
461
603
|
// Add newline with list formatting and optional indent
|
|
462
604
|
const listAttrs = {
|
|
463
605
|
list: { list: finalListType }
|
|
@@ -470,7 +612,7 @@ function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0) {
|
|
|
470
612
|
}
|
|
471
613
|
else if (itemChild.type === 'list') {
|
|
472
614
|
// Nested list - recursively process with increased depth
|
|
473
|
-
walkMdastNodes([itemChild], currentAttrs, blocks, depth + 1);
|
|
615
|
+
walkMdastNodes([itemChild], currentAttrs, blocks, depth + 1, attachmentsBySrc);
|
|
474
616
|
}
|
|
475
617
|
}
|
|
476
618
|
}
|
|
@@ -493,7 +635,7 @@ function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0) {
|
|
|
493
635
|
default:
|
|
494
636
|
// For any other block-level nodes, try to process children
|
|
495
637
|
if ('children' in node && Array.isArray(node.children)) {
|
|
496
|
-
walkMdastNodes(node.children, currentAttrs, blocks, depth);
|
|
638
|
+
walkMdastNodes(node.children, currentAttrs, blocks, depth, attachmentsBySrc);
|
|
497
639
|
}
|
|
498
640
|
break;
|
|
499
641
|
}
|
|
@@ -503,10 +645,30 @@ function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0) {
|
|
|
503
645
|
* Recursively walk phrasing content (inline nodes) and build ClickUp blocks
|
|
504
646
|
* Accumulates formatting attributes from parent nodes
|
|
505
647
|
*/
|
|
506
|
-
function walkPhrasingContent(nodes, inheritedAttrs, blocks) {
|
|
648
|
+
function walkPhrasingContent(nodes, inheritedAttrs, blocks, attachmentsBySrc) {
|
|
507
649
|
for (const node of nodes) {
|
|
508
650
|
const currentAttrs = { ...inheritedAttrs };
|
|
509
651
|
switch (node.type) {
|
|
652
|
+
case 'image': {
|
|
653
|
+
// An image node has neither `value` nor `children`, so without this case it
|
|
654
|
+
// would fall through to `default` and vanish silently.
|
|
655
|
+
const attachment = attachmentsBySrc?.get(node.url);
|
|
656
|
+
const caption = node.alt || '';
|
|
657
|
+
if (attachment) {
|
|
658
|
+
blocks.push(buildImageFragment(attachment, caption));
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
// Nothing was uploaded for this source - degrade to a link rather than
|
|
662
|
+
// dropping the reference, so the information survives.
|
|
663
|
+
const label = caption || node.url;
|
|
664
|
+
const isEmbeddable = /^https?:\/\//i.test(node.url);
|
|
665
|
+
blocks.push({
|
|
666
|
+
text: label,
|
|
667
|
+
attributes: isEmbeddable ? { ...currentAttrs, link: node.url } : currentAttrs,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
510
672
|
case 'text':
|
|
511
673
|
// Plain text node
|
|
512
674
|
if (node.value) {
|
|
@@ -519,12 +681,12 @@ function walkPhrasingContent(nodes, inheritedAttrs, blocks) {
|
|
|
519
681
|
case 'strong':
|
|
520
682
|
// Bold text - recurse with bold attribute
|
|
521
683
|
currentAttrs.bold = true;
|
|
522
|
-
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
684
|
+
walkPhrasingContent(node.children, currentAttrs, blocks, attachmentsBySrc);
|
|
523
685
|
break;
|
|
524
686
|
case 'emphasis':
|
|
525
687
|
// Italic text - recurse with italic attribute
|
|
526
688
|
currentAttrs.italic = true;
|
|
527
|
-
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
689
|
+
walkPhrasingContent(node.children, currentAttrs, blocks, attachmentsBySrc);
|
|
528
690
|
break;
|
|
529
691
|
case 'inlineCode':
|
|
530
692
|
// Inline code
|
|
@@ -536,11 +698,20 @@ function walkPhrasingContent(nodes, inheritedAttrs, blocks) {
|
|
|
536
698
|
});
|
|
537
699
|
}
|
|
538
700
|
break;
|
|
539
|
-
case 'link':
|
|
540
|
-
//
|
|
701
|
+
case 'link': {
|
|
702
|
+
// A link to a ClickUp task becomes a real task mention, matching what the
|
|
703
|
+
// ClickUp UI does when a task URL is pasted. The mention renders the live
|
|
704
|
+
// task name, so any custom link text is intentionally replaced by it.
|
|
705
|
+
const mentionedTaskId = parseClickUpTaskUrl(node.url);
|
|
706
|
+
if (mentionedTaskId) {
|
|
707
|
+
blocks.push({ type: 'task_mention', task_mention: { task_id: mentionedTaskId } });
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
// Ordinary link - recurse with link attribute
|
|
541
711
|
currentAttrs.link = node.url;
|
|
542
|
-
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
712
|
+
walkPhrasingContent(node.children, currentAttrs, blocks, attachmentsBySrc);
|
|
543
713
|
break;
|
|
714
|
+
}
|
|
544
715
|
case 'break':
|
|
545
716
|
// Line break - add as plain text
|
|
546
717
|
blocks.push({ text: '\n', attributes: {} });
|
|
@@ -555,7 +726,7 @@ function walkPhrasingContent(nodes, inheritedAttrs, blocks) {
|
|
|
555
726
|
}
|
|
556
727
|
else if ('children' in node && Array.isArray(node.children)) {
|
|
557
728
|
// Recurse into children for other container nodes
|
|
558
|
-
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
729
|
+
walkPhrasingContent(node.children, currentAttrs, blocks, attachmentsBySrc);
|
|
559
730
|
}
|
|
560
731
|
break;
|
|
561
732
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
2
|
+
/**
|
|
3
|
+
* Attachment object as returned by POST /api/v2/task/{task_id}/attachment.
|
|
4
|
+
* ClickUp only renders an image inside a comment when the fragment carries this
|
|
5
|
+
* whole object - a bare URL string renders as an empty placeholder tile.
|
|
6
|
+
*/
|
|
7
|
+
export interface ClickUpUploadedAttachment {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
extension?: string;
|
|
12
|
+
url: string;
|
|
13
|
+
thumbnail_small?: string;
|
|
14
|
+
thumbnail_medium?: string;
|
|
15
|
+
thumbnail_large?: string;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
}
|
|
20
|
+
/** Image bytes ready to be uploaded */
|
|
21
|
+
interface ResolvedBytes {
|
|
22
|
+
kind: "bytes";
|
|
23
|
+
bytes: Buffer;
|
|
24
|
+
mimeType: string;
|
|
25
|
+
suggestedName: string;
|
|
26
|
+
}
|
|
27
|
+
/** Already an attachment on ClickUp's CDN - reuse it instead of uploading again */
|
|
28
|
+
interface ResolvedExisting {
|
|
29
|
+
kind: "existing";
|
|
30
|
+
url: string;
|
|
31
|
+
}
|
|
32
|
+
export type ResolvedImageSource = ResolvedBytes | ResolvedExisting;
|
|
33
|
+
/**
|
|
34
|
+
* ClickUp serves attachments from *.clickup-attachments.com. Such a URL is
|
|
35
|
+
* already uploaded, so it can be embedded directly.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isClickUpAttachmentUrl(url: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Turn the `src` of a markdown image into something uploadable.
|
|
40
|
+
*
|
|
41
|
+
* Supported sources, in this order:
|
|
42
|
+
* - a ClickUp attachment URL -> reused as-is, no upload
|
|
43
|
+
* - a base64 data URI -> decoded
|
|
44
|
+
* - any other http(s) URL -> downloaded
|
|
45
|
+
* - anything else -> read from the local filesystem
|
|
46
|
+
*
|
|
47
|
+
* The local path case is the interesting one: this server runs next to the agent,
|
|
48
|
+
* so a screenshot can be referenced by path instead of being inlined as base64,
|
|
49
|
+
* which would otherwise cost a multiple of the file size in tokens.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveImageSource(src: string, baseDir?: string): Promise<ResolvedImageSource>;
|
|
52
|
+
/**
|
|
53
|
+
* Derive the upload filename from the markdown alt text.
|
|
54
|
+
*
|
|
55
|
+
* ClickUp shows the *attachment filename* underneath an image, not the fragment
|
|
56
|
+
* text - so naming the upload after the caption is what makes a readable caption
|
|
57
|
+
* appear in the ticket.
|
|
58
|
+
*/
|
|
59
|
+
export declare function captionToFilename(caption: string, fallbackName: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Upload a single image to a task and return the full attachment object.
|
|
62
|
+
*/
|
|
63
|
+
export declare function uploadTaskAttachment(taskId: string, filename: string, bytes: Buffer, mimeType: string): Promise<ClickUpUploadedAttachment>;
|
|
64
|
+
/** A markdown image whose source resolved to uploadable bytes or an existing attachment */
|
|
65
|
+
export interface ResolvedMarkdownImage {
|
|
66
|
+
/** The original `src` as written in the markdown */
|
|
67
|
+
src: string;
|
|
68
|
+
alt: string;
|
|
69
|
+
resolved: ResolvedImageSource;
|
|
70
|
+
}
|
|
71
|
+
/** One markdown image reference that could not be used, and why */
|
|
72
|
+
export interface ImageFailure {
|
|
73
|
+
src: string;
|
|
74
|
+
error: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Phase 1 of attaching images: resolve every source without writing anything.
|
|
78
|
+
*
|
|
79
|
+
* Reads local files, downloads http(s) URLs, decodes data URIs and validates
|
|
80
|
+
* magic bytes and size. Failures are collected instead of thrown so the caller
|
|
81
|
+
* can report every broken reference at once - and abort before anything is
|
|
82
|
+
* posted to ClickUp. Identical sources are resolved once.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveMarkdownImages(images: {
|
|
85
|
+
src: string;
|
|
86
|
+
alt: string;
|
|
87
|
+
}[], baseDir?: string): Promise<{
|
|
88
|
+
resolved: ResolvedMarkdownImage[];
|
|
89
|
+
failures: ImageFailure[];
|
|
90
|
+
}>;
|
|
91
|
+
/** A successfully uploaded (or reused) attachment for one markdown source */
|
|
92
|
+
export interface UploadedMarkdownImage {
|
|
93
|
+
src: string;
|
|
94
|
+
attachment: ClickUpUploadedAttachment;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Phase 2: upload the resolved images to the task.
|
|
98
|
+
*
|
|
99
|
+
* Uploads run sequentially: a typical comment has a handful of screenshots, and
|
|
100
|
+
* N uploads plus one write call stays well inside ClickUp's 100 calls/minute.
|
|
101
|
+
* Stops at the first upload error - an API failure is unlikely to heal mid-batch,
|
|
102
|
+
* and everything uploaded so far is returned so the caller can tell a retry to
|
|
103
|
+
* reference those CDN URLs directly instead of uploading again.
|
|
104
|
+
*/
|
|
105
|
+
export declare function uploadResolvedImages(taskId: string, images: ResolvedMarkdownImage[]): Promise<{
|
|
106
|
+
uploaded: UploadedMarkdownImage[];
|
|
107
|
+
failure: ImageFailure | null;
|
|
108
|
+
}>;
|
|
109
|
+
/** Map from markdown `src` to the attachment that should be embedded for it. */
|
|
110
|
+
export declare function toAttachmentMap(uploaded: UploadedMarkdownImage[]): Map<string, ClickUpUploadedAttachment>;
|
|
111
|
+
export {};
|
|
112
|
+
//# sourceMappingURL=attachments.d.ts.map
|