@sebastienheyd/clickup-mcp 1.9.0 → 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 CHANGED
@@ -4,6 +4,24 @@
4
4
 
5
5
  Model Context Protocol (MCP) server enabling AI assistants to interact with ClickUp workspaces. Get complete task context with comments and images, search across projects, create and update tasks, collaborate through comments, and track time - all through natural language.
6
6
 
7
+ ## Differences From the Upstream Project
8
+
9
+ This fork is published as [`@sebastienheyd/clickup-mcp`](https://www.npmjs.com/package/@sebastienheyd/clickup-mcp) and follows its own version numbers. Upstream history is merged (last sync: upstream 1.9.0, September 2026), so the two projects share their core; the fork keeps its own additions on top.
10
+
11
+ | Area | Only in this fork |
12
+ |------|-------------------|
13
+ | Task IDs | Custom task IDs (e.g. `SOI-4422`) accepted wherever a `task_id` is expected, time tools included |
14
+ | `searchTasks` | `include_closed` and `archived` filters |
15
+ | `searchSpaces` | `folder_id` to resolve a single folder with its lists and statuses |
16
+ | `createTask` / `updateTask` | Sprint `points`; `remove_assignees`; `null` clears `due_date`, `start_date`, `time_estimate` and `points` |
17
+ | Descriptions | `description` (full replacement) on `updateListInfo` too, and both tools echo the previous description |
18
+ | Comments | `deleteComment` (same guardrails as `editComment`) |
19
+ | Time tracking | `updateTimeEntry` and `deleteTimeEntry`; `getTimeEntries` shows each `entry_id` |
20
+ | Tooling | `Makefile` with the common commands; `npm run cli` understands `key=null` |
21
+ | Runtime | Node.js 20 or newer (upstream: 16) |
22
+
23
+ See [CHANGELOG.md](CHANGELOG.md) for this fork's history and the [upstream changelog](https://github.com/hauptsacheNet/clickup-mcp/blob/main/CHANGELOG.md) for theirs.
24
+
7
25
  ## This MCP vs Official ClickUp MCP
8
26
 
9
27
  > See also: [Official ClickUp MCP Documentation](https://developer.clickup.com/docs/connect-an-ai-assistant-to-clickups-mcp-server)
@@ -99,7 +117,7 @@ Turn natural language into powerful ClickUp actions:
99
117
  - Handle time estimates, sprint points and custom field values
100
118
 
101
119
  ### 🔒 **Safety Features**
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
120
+ - **Append-First Descriptions**: `append_description` adds a dated block under the existing description and is the default for notes. `description` rewrites the whole description on purpose - the previous content is echoed back so a wrong replacement can be undone
103
121
  - **Normal Field Updates**: Status, priority, assignees, tags, and dates can be updated normally (easily revertible through ClickUp's history)
104
122
 
105
123
  ## Installation
@@ -291,11 +309,11 @@ By default (`append_description`), content is appended under the existing descri
291
309
  - Performance requirement: < 2s load time
292
310
  ```
293
311
 
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`.
312
+ This keeps existing content and a clear audit trail. To correct or restructure a description, `description` rewrites it entirely; the tool answers with the previous description so nothing is silently lost. The same two parameters exist on `updateListInfo`.
295
313
 
296
314
  ## Writing Images Into Tickets
297
315
 
298
- `addComment`, `editComment`, `createTask` and `updateTask` accept images as ordinary markdown. Because
316
+ `addComment`, `createTask` and `updateTask` accept images as ordinary markdown. Because
299
317
  this server runs locally, it reads the file itself - so a **local path is enough**:
300
318
 
301
319
  ```markdown
@@ -318,12 +336,16 @@ Notes:
318
336
  - **An image inside a numbered list breaks ClickUp's numbering.** Write walkthrough
319
337
  steps as bold lines with the image between them, as above.
320
338
  - Only real PNG/JPEG/GIF/WebP files are uploaded - the content is checked, not the
321
- extension. A file that fails **aborts the write**: `addComment`, `editComment` and
339
+ extension. A file that fails **aborts the write**: `addComment` and
322
340
  `updateTask` report every broken reference and change nothing, so the markdown can be
323
341
  fixed and the call retried without creating duplicates. `createTask` validates its
324
342
  images before creating the task; only an upload failing afterwards is reported as a
325
343
  warning, since the task already exists at that point.
326
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.
327
349
 
328
350
  ## Performance & Limitations
329
351
 
@@ -57,6 +57,7 @@ export interface ClickUpCommentBlock {
57
57
  attributes?: {
58
58
  bold?: boolean;
59
59
  italic?: boolean;
60
+ strike?: boolean;
60
61
  code?: boolean;
61
62
  link?: string;
62
63
  'code-block'?: {
@@ -1 +1 @@
1
- {"version":3,"file":"clickup-text.d.ts","sourceRoot":"","sources":["../src/clickup-text.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"clickup-text.d.ts","sourceRoot":"","sources":["../src/clickup-text.ts"],"names":[],"mappings":"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"}
@@ -62,11 +62,24 @@ async function convertClickUpTextItemsToToolCallResult(textItems) {
62
62
  // Track current formatting state to avoid unnecessary close/reopen
63
63
  let activeBold = false;
64
64
  let activeItalic = false;
65
+ let activeStrike = false;
65
66
  let activeCode = false;
67
+ // ClickUp emits one '\n' fragment with a code-block attribute per code LINE;
68
+ // consecutive ones belong to the same fenced block when read back as markdown.
69
+ let inCodeBlock = false;
70
+ let currentFenceLang = '';
71
+ const closeCodeFence = () => {
72
+ if (inCodeBlock) {
73
+ currentTextBlock += '```\n';
74
+ inCodeBlock = false;
75
+ currentFenceLang = '';
76
+ }
77
+ };
66
78
  for (let i = 0; i < textItems.length; i++) {
67
79
  const item = textItems[i];
68
80
  // Handle image items
69
81
  if (item.type === "image" && item.image && item.image.url) {
82
+ closeCodeFence();
70
83
  const imageFileName = item.image.name || item.image.title || "image";
71
84
  const imageUrl = item.image.url;
72
85
  const altText = item.text || imageFileName;
@@ -133,6 +146,27 @@ async function convertClickUpTextItemsToToolCallResult(textItems) {
133
146
  else if (typeof item.text === "string") {
134
147
  // Check if this is a newline with block formatting (header, blockquote, list)
135
148
  if (item.text === '\n' && item.attributes) {
149
+ // Code block formatting: append this line to the open fence (or open one).
150
+ // The attribute value carries the language, either directly or nested as
151
+ // {'code-block': lang}; 'plain' means no language.
152
+ if (item.attributes['code-block']) {
153
+ const rawLang = item.attributes['code-block'];
154
+ const lang = typeof rawLang === 'string' ? rawLang : rawLang?.['code-block'] ?? '';
155
+ const fenceLang = lang === 'plain' ? '' : lang;
156
+ if (inCodeBlock && fenceLang !== currentFenceLang) {
157
+ closeCodeFence();
158
+ }
159
+ if (!inCodeBlock) {
160
+ currentTextBlock += '```' + fenceLang + '\n';
161
+ inCodeBlock = true;
162
+ currentFenceLang = fenceLang;
163
+ }
164
+ currentTextBlock += currentLine + '\n';
165
+ currentLine = "";
166
+ continue;
167
+ }
168
+ // Any other line terminator ends a code block
169
+ closeCodeFence();
136
170
  // Header formatting
137
171
  if (item.attributes.header) {
138
172
  const level = item.attributes.header;
@@ -163,17 +197,8 @@ async function convertClickUpTextItemsToToolCallResult(textItems) {
163
197
  break;
164
198
  }
165
199
  }
166
- // Code block formatting
167
- else if (item.attributes['code-block']) {
168
- // Wrap the current line in code block markers
169
- currentLine = '```\n' + currentLine + '\n```';
170
- }
171
200
  // Add formatted line to text block
172
- currentTextBlock += currentLine;
173
- // Add newline unless it's code block (already has newlines)
174
- if (!item.attributes['code-block']) {
175
- currentTextBlock += '\n';
176
- }
201
+ currentTextBlock += currentLine + '\n';
177
202
  currentLine = ""; // Reset for next line
178
203
  continue;
179
204
  }
@@ -182,15 +207,18 @@ async function convertClickUpTextItemsToToolCallResult(textItems) {
182
207
  // Determine current and next formatting state
183
208
  const hasBold = item.attributes?.bold === true;
184
209
  const hasItalic = item.attributes?.italic === true;
210
+ const hasStrike = item.attributes?.strike === true;
185
211
  const hasLink = item.attributes?.link;
186
212
  // Look ahead to next non-newline block
187
213
  let nextHasBold = false;
188
214
  let nextHasItalic = false;
215
+ let nextHasStrike = false;
189
216
  for (let j = i + 1; j < textItems.length; j++) {
190
217
  const nextItem = textItems[j];
191
218
  if (nextItem.text !== '\n' || !nextItem.attributes) {
192
219
  nextHasBold = nextItem.attributes?.bold === true;
193
220
  nextHasItalic = nextItem.attributes?.italic === true;
221
+ nextHasStrike = nextItem.attributes?.strike === true;
194
222
  break;
195
223
  }
196
224
  }
@@ -200,14 +228,20 @@ async function convertClickUpTextItemsToToolCallResult(textItems) {
200
228
  prefix += "**";
201
229
  if (hasItalic && !activeItalic)
202
230
  prefix += "*";
231
+ if (hasStrike && !activeStrike)
232
+ prefix += "~~";
203
233
  // Build suffix (close formatting that won't continue)
204
234
  let suffix = "";
235
+ if (hasStrike && !nextHasStrike)
236
+ suffix += "~~";
205
237
  if (hasItalic && !nextHasItalic)
206
238
  suffix += "*";
207
239
  if (hasBold && !nextHasBold)
208
240
  suffix += "**";
209
241
  // Close formatting that's active but not in this block
210
242
  let closingPrefix = "";
243
+ if (activeStrike && !hasStrike)
244
+ closingPrefix += "~~";
211
245
  if (activeBold && !hasBold)
212
246
  closingPrefix += "**";
213
247
  if (activeItalic && !hasItalic)
@@ -216,6 +250,7 @@ async function convertClickUpTextItemsToToolCallResult(textItems) {
216
250
  // Update state
217
251
  activeBold = hasBold && nextHasBold;
218
252
  activeItalic = hasItalic && nextHasItalic;
253
+ activeStrike = hasStrike && nextHasStrike;
219
254
  // Link formatting (wraps everything)
220
255
  if (hasLink) {
221
256
  formattedText = `[${formattedText}](${hasLink})`;
@@ -227,6 +262,7 @@ async function convertClickUpTextItemsToToolCallResult(textItems) {
227
262
  // Add to current line (not text block yet)
228
263
  if (item.text === '\n') {
229
264
  // Plain newline without formatting
265
+ closeCodeFence();
230
266
  currentTextBlock += currentLine + '\n';
231
267
  currentLine = "";
232
268
  }
@@ -245,6 +281,7 @@ async function convertClickUpTextItemsToToolCallResult(textItems) {
245
281
  }
246
282
  }
247
283
  // Add any remaining text
284
+ closeCodeFence();
248
285
  if (currentLine) {
249
286
  currentTextBlock += currentLine;
250
287
  }
@@ -439,7 +476,7 @@ function buildImageFragment(attachment, caption) {
439
476
  * fragment (e.g. ?comment=... deep links) do NOT match, because a mention would
440
477
  * either not resolve or lose the anchor - those stay ordinary links.
441
478
  */
442
- const CLICKUP_TASK_URL_PATTERN = /^https?:\/\/app\.clickup\.com\/t\/(?:\d+\/)?([a-z0-9]{6,12})\/?$/;
479
+ const CLICKUP_TASK_URL_PATTERN = /^https?:\/\/app\.clickup\.com\/t\/(?:\d+\/)?([a-z0-9]{6,16})\/?$/;
443
480
  /**
444
481
  * Extract the task ID from a ClickUp task URL, or null if it is not one.
445
482
  */
@@ -558,6 +595,14 @@ function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0, attachmentsByS
558
595
  for (let i = 0; i < nodes.length; i++) {
559
596
  const node = nodes[i];
560
597
  const currentAttrs = { ...inheritedAttrs };
598
+ // ClickUp has no paragraph margins: two paragraphs separated by a single '\n'
599
+ // render as consecutive lines. The UI stores a paragraph break as an extra
600
+ // empty '\n' fragment (what a user gets by pressing Enter twice), so emit one
601
+ // between adjacent flow blocks. Headings, code blocks and blockquotes bring
602
+ // their own spacing - a blank line next to them would double the gap.
603
+ if (i > 0 && needsBlankLineBetween(nodes[i - 1], node)) {
604
+ blocks.push({ text: '\n', attributes: {} });
605
+ }
561
606
  switch (node.type) {
562
607
  case 'heading':
563
608
  // Process heading content with inline formatting
@@ -621,42 +666,139 @@ function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0, attachmentsByS
621
666
  // Code block
622
667
  const codeNode = node;
623
668
  if (codeNode.value) {
624
- blocks.push({ text: codeNode.value, attributes: {} });
625
- blocks.push({
626
- text: '\n',
627
- attributes: { 'code-block': { 'code-block': codeNode.lang || 'plain' } }
628
- });
669
+ pushCodeBlockLines(blocks, codeNode.value, codeNode.lang || 'plain');
629
670
  }
630
671
  break;
631
672
  case 'thematicBreak':
632
673
  // Horizontal rule - just add a line break
633
674
  blocks.push({ text: '\n', attributes: {} });
634
675
  break;
676
+ case 'table': {
677
+ // ClickUp comments cannot render tables (there is no table fragment in the
678
+ // comment format - unknown attributes are stored but render as plain text).
679
+ // Re-render the table as an aligned pipe table inside a code block so the
680
+ // information survives and stays readable in monospace.
681
+ const tableText = serializeTableAsAlignedPipes(node);
682
+ if (tableText) {
683
+ pushCodeBlockLines(blocks, tableText, 'plain');
684
+ }
685
+ break;
686
+ }
635
687
  default:
636
688
  // For any other block-level nodes, try to process children
637
689
  if ('children' in node && Array.isArray(node.children)) {
638
690
  walkMdastNodes(node.children, currentAttrs, blocks, depth, attachmentsBySrc);
639
691
  }
692
+ else if ('value' in node && typeof node.value === 'string') {
693
+ // Last-resort safety net: never drop text content silently
694
+ blocks.push({ text: node.value, attributes: {} });
695
+ }
640
696
  break;
641
697
  }
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: {} });
698
+ }
699
+ }
700
+ /**
701
+ * Block types that render without their own vertical margin in ClickUp comments.
702
+ * Two of them in a row need an explicit empty line to read as separate blocks.
703
+ */
704
+ const FLOW_BLOCK_TYPES = new Set(['paragraph', 'list']);
705
+ /**
706
+ * Block types emitted as code-block lines (tables are rendered inside a code block).
707
+ * Quill fuses consecutive code-block lines into ONE block whatever the source looked
708
+ * like, so two of them in a row need an empty line in between - otherwise a command
709
+ * and its output, or a code block and a table, collapse into a single block, and the
710
+ * read side (which groups consecutive code-block lines into one fence) can no longer
711
+ * tell them apart.
712
+ */
713
+ const CODE_BLOCK_TYPES = new Set(['code', 'table']);
714
+ function needsBlankLineBetween(prev, next) {
715
+ if (FLOW_BLOCK_TYPES.has(prev.type) && FLOW_BLOCK_TYPES.has(next.type)) {
716
+ return true;
717
+ }
718
+ return CODE_BLOCK_TYPES.has(prev.type) && CODE_BLOCK_TYPES.has(next.type);
719
+ }
720
+ /**
721
+ * Emit a (possibly multi-line) code block the way ClickUp's Quill-based format
722
+ * expects it: block attributes apply per line, so EVERY line needs its own '\n'
723
+ * fragment carrying the code-block attribute. A single text fragment with embedded
724
+ * newlines renders only its last line as code - the rest degrades to plain text.
725
+ */
726
+ function pushCodeBlockLines(blocks, code, lang) {
727
+ for (const line of code.split('\n')) {
728
+ if (line) {
729
+ blocks.push({ text: line, attributes: {} });
730
+ }
731
+ blocks.push({
732
+ text: '\n',
733
+ attributes: { 'code-block': { 'code-block': lang } }
734
+ });
735
+ }
736
+ }
737
+ /**
738
+ * Serialize phrasing content back to compact markdown for use inside a code block.
739
+ * Inline formatting markers are kept so nothing is lost, even though a code block
740
+ * renders them literally.
741
+ */
742
+ function serializePhrasingToMarkdown(nodes) {
743
+ let out = '';
744
+ for (const node of nodes) {
745
+ switch (node.type) {
746
+ case 'text':
747
+ out += node.value;
748
+ break;
749
+ case 'strong':
750
+ out += `**${serializePhrasingToMarkdown(node.children)}**`;
751
+ break;
752
+ case 'emphasis':
753
+ out += `*${serializePhrasingToMarkdown(node.children)}*`;
754
+ break;
755
+ case 'delete':
756
+ out += `~~${serializePhrasingToMarkdown(node.children)}~~`;
757
+ break;
758
+ case 'inlineCode':
759
+ out += `\`${node.value}\``;
760
+ break;
761
+ case 'link':
762
+ out += `[${serializePhrasingToMarkdown(node.children)}](${node.url})`;
763
+ break;
764
+ case 'image':
765
+ out += node.alt || node.url;
766
+ break;
767
+ case 'break':
768
+ out += ' ';
769
+ break;
770
+ default:
771
+ if ('value' in node && typeof node.value === 'string') {
772
+ out += node.value;
773
+ }
774
+ else if ('children' in node && Array.isArray(node.children)) {
775
+ out += serializePhrasingToMarkdown(node.children);
776
+ }
777
+ break;
646
778
  }
647
779
  }
780
+ return out;
648
781
  }
649
782
  /**
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.
783
+ * Render an mdast table as a column-aligned pipe table string.
652
784
  */
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;
785
+ function serializeTableAsAlignedPipes(table) {
786
+ const rows = table.children.map((row) => row.children.map((cell) => serializePhrasingToMarkdown(cell.children).replace(/\|/g, '\\|').trim()));
787
+ if (rows.length === 0) {
788
+ return '';
789
+ }
790
+ const colCount = Math.max(...rows.map((r) => r.length));
791
+ const widths = [];
792
+ for (let col = 0; col < colCount; col++) {
793
+ widths[col] = Math.max(3, ...rows.map((r) => (r[col] ?? '').length));
658
794
  }
659
- return nextStart - previousEnd > 1;
795
+ const renderRow = (cells) => '| ' + widths.map((w, col) => (cells[col] ?? '').padEnd(w)).join(' | ') + ' |';
796
+ const lines = [renderRow(rows[0])];
797
+ lines.push('| ' + widths.map((w) => '-'.repeat(w)).join(' | ') + ' |');
798
+ for (const row of rows.slice(1)) {
799
+ lines.push(renderRow(row));
800
+ }
801
+ return lines.join('\n');
660
802
  }
661
803
  /**
662
804
  * Recursively walk phrasing content (inline nodes) and build ClickUp blocks
@@ -705,6 +847,11 @@ function walkPhrasingContent(nodes, inheritedAttrs, blocks, attachmentsBySrc) {
705
847
  currentAttrs.italic = true;
706
848
  walkPhrasingContent(node.children, currentAttrs, blocks, attachmentsBySrc);
707
849
  break;
850
+ case 'delete':
851
+ // GFM strikethrough (~~text~~) - ClickUp renders this via the strike attribute
852
+ currentAttrs.strike = true;
853
+ walkPhrasingContent(node.children, currentAttrs, blocks, attachmentsBySrc);
854
+ break;
708
855
  case 'inlineCode':
709
856
  // Inline code
710
857
  if (node.value) {
@@ -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,CAwC9E;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,qBAAqB,EAAE,GAChC,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAMxC"}
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
- const name = decodeURIComponent((0, path_1.basename)(new URL(resolved.url).pathname));
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,98 @@
1
+ /** A task comment as returned by GET /api/v2/task/{task_id}/comment or GET /comment/{id}/reply */
2
+ export interface ExistingComment {
3
+ id: string;
4
+ date: string;
5
+ comment?: any[];
6
+ comment_text?: string;
7
+ user?: {
8
+ id?: number | string;
9
+ username?: string;
10
+ };
11
+ reply_count?: number;
12
+ }
13
+ /** Cursor into the comment list: the date and id of the last comment of the previous page */
14
+ export interface CommentPageCursor {
15
+ start: string;
16
+ startId: string;
17
+ }
18
+ /**
19
+ * Never page further back than this. A busy ticket would otherwise walk its
20
+ * entire comment history and eat the 100 calls/minute budget.
21
+ */
22
+ export declare const MAX_COMMENT_PAGES = 10;
23
+ /** Page size ClickUp uses for the comment list - a shorter page means the last page. */
24
+ export declare const COMMENTS_PER_PAGE = 25;
25
+ /** One page of task comments, newest first, 25 per page */
26
+ export declare function fetchCommentPage(taskId: string, cursor?: CommentPageCursor): Promise<ExistingComment[]>;
27
+ /** The top-level comments of a task plus whether the history is complete. */
28
+ export interface CommentHistory {
29
+ /** Top-level comments, newest first */
30
+ comments: ExistingComment[];
31
+ /**
32
+ * Why older comments are missing, or null when the whole list was loaded.
33
+ * `capped`: the task has more comments than MAX_COMMENT_PAGES pages;
34
+ * `error`: a page request failed (rate limit, 5xx...) - `comments` holds what was
35
+ * loaded before the failure, possibly nothing.
36
+ */
37
+ incomplete: {
38
+ reason: "capped" | "error";
39
+ detail: string;
40
+ } | null;
41
+ }
42
+ /**
43
+ * All top-level comments of a task, newest first.
44
+ *
45
+ * The comment list endpoint returns 25 comments per page, so longer histories are
46
+ * paged with `start`/`start_id`. Paging is capped at MAX_COMMENT_PAGES (250 comments)
47
+ * to protect the API budget. Never throws: a failing page (typically a 429 close to
48
+ * the API limit on exactly the busy tasks that need paging) keeps the comments
49
+ * already loaded and is reported through `incomplete`, so the caller can show a
50
+ * partial history with a warning instead of silently showing none.
51
+ */
52
+ export declare function fetchAllTopLevelComments(taskId: string): Promise<CommentHistory>;
53
+ /** Result of looking a top-level comment up in a task's comment list. */
54
+ export interface TopLevelCommentLookup {
55
+ /** The comment, or undefined when it was not found in the searched range */
56
+ comment: ExistingComment | undefined;
57
+ /**
58
+ * true when the whole comment list was searched, so "not found" is definitive;
59
+ * false when the search stopped at MAX_COMMENT_PAGES with older comments left
60
+ * unread, so the id may still be a valid but old top-level comment.
61
+ */
62
+ exhaustive: boolean;
63
+ }
64
+ /**
65
+ * Find a top-level comment of a task by id, paging as far as MAX_COMMENT_PAGES.
66
+ *
67
+ * Unlike editComment's lookup this does not stop at the edit window, because a
68
+ * thread parent can be arbitrarily old. `comment` is undefined when the id is not
69
+ * a top-level comment of this task - which also catches reply ids, since replies
70
+ * never appear in the task's comment list. Callers must check `exhaustive` before
71
+ * telling the user the id is invalid: past the page cap the answer is only
72
+ * "not among the newest comments".
73
+ */
74
+ export declare function findTopLevelComment(taskId: string, commentId: string): Promise<TopLevelCommentLookup>;
75
+ /**
76
+ * The replies inside a comment thread, oldest first.
77
+ *
78
+ * `GET /task/{id}/comment` only returns top-level comments; the replies of a
79
+ * thread (Threaded Comments ClickApp) live behind `GET /comment/{id}/reply`.
80
+ * Any failure (non-ok response, network error, malformed body) is logged and
81
+ * returns an empty array so one broken thread does not take down the whole
82
+ * task view.
83
+ */
84
+ export declare function fetchCommentReplies(commentId: string): Promise<ExistingComment[]>;
85
+ /**
86
+ * Never fetch more threads than this per task read, and never more than a few
87
+ * at once - together with MAX_COMMENT_PAGES this keeps a single getTaskById
88
+ * within ClickUp's 100 calls/minute budget even on a heavily threaded task.
89
+ */
90
+ export declare const MAX_REPLY_FETCHES = 30;
91
+ /**
92
+ * Fetch the replies of every comment with reply_count > 0, bounded in count and
93
+ * concurrency. Returns a map of comment id -> replies; a thread that was skipped
94
+ * (over the cap) or failed to load is simply absent or empty, so callers can
95
+ * render a "replies not loaded" hint from reply_count.
96
+ */
97
+ export declare function fetchRepliesByComment(comments: ExistingComment[]): Promise<Map<string, ExistingComment[]>>;
98
+ //# sourceMappingURL=comments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../../src/shared/comments.ts"],"names":[],"mappings":"AAEA,kGAAkG;AAClG,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,wFAAwF;AACxF,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,2DAA2D;AAC3D,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,eAAe,EAAE,CAAC,CAqB5B;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,EAAE;QAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACnE;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAsCtF;AAED,yEAAyE;AACzE,MAAM,WAAW,qBAAqB;IACpC,4EAA4E;IAC5E,OAAO,EAAE,eAAe,GAAG,SAAS,CAAC;IACrC;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,qBAAqB,CAAC,CAiBhC;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAsBvF;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAGpC;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,eAAe,EAAE,GAC1B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC,CAsBzC"}