@pripanggalih/clickup-mcp 1.6.1
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/LICENSE +22 -0
- package/README.md +295 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +184 -0
- package/dist/clickup-text.d.ts +83 -0
- package/dist/clickup-text.d.ts.map +1 -0
- package/dist/clickup-text.js +563 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/resources/space-resources.d.ts +6 -0
- package/dist/resources/space-resources.d.ts.map +1 -0
- package/dist/resources/space-resources.js +95 -0
- package/dist/shared/config.d.ts +11 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +61 -0
- package/dist/shared/data-uri.d.ts +14 -0
- package/dist/shared/data-uri.d.ts.map +1 -0
- package/dist/shared/data-uri.js +34 -0
- package/dist/shared/image-processing.d.ts +13 -0
- package/dist/shared/image-processing.d.ts.map +1 -0
- package/dist/shared/image-processing.js +199 -0
- package/dist/shared/types.d.ts +21 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/utils.d.ts +71 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +508 -0
- package/dist/test-utils.d.ts +23 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +44 -0
- package/dist/tools/admin-tools.d.ts +3 -0
- package/dist/tools/admin-tools.d.ts.map +1 -0
- package/dist/tools/admin-tools.js +288 -0
- package/dist/tools/doc-tools.d.ts +4 -0
- package/dist/tools/doc-tools.d.ts.map +1 -0
- package/dist/tools/doc-tools.js +436 -0
- package/dist/tools/list-tools.d.ts +4 -0
- package/dist/tools/list-tools.d.ts.map +1 -0
- package/dist/tools/list-tools.js +175 -0
- package/dist/tools/search-tools.d.ts +3 -0
- package/dist/tools/search-tools.d.ts.map +1 -0
- package/dist/tools/search-tools.js +161 -0
- package/dist/tools/space-tools.d.ts +3 -0
- package/dist/tools/space-tools.d.ts.map +1 -0
- package/dist/tools/space-tools.js +128 -0
- package/dist/tools/task-tools.d.ts +8 -0
- package/dist/tools/task-tools.d.ts.map +1 -0
- package/dist/tools/task-tools.js +329 -0
- package/dist/tools/task-write-tools.d.ts +3 -0
- package/dist/tools/task-write-tools.d.ts.map +1 -0
- package/dist/tools/task-write-tools.js +567 -0
- package/dist/tools/time-tools.d.ts +4 -0
- package/dist/tools/time-tools.d.ts.map +1 -0
- package/dist/tools/time-tools.js +338 -0
- package/package.json +74 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.convertClickUpTextItemsToToolCallResult = convertClickUpTextItemsToToolCallResult;
|
|
7
|
+
exports.convertMarkdownToToolCallResult = convertMarkdownToToolCallResult;
|
|
8
|
+
exports.convertMarkdownToClickUpBlocks = convertMarkdownToClickUpBlocks;
|
|
9
|
+
const data_uri_1 = require("./shared/data-uri");
|
|
10
|
+
const unified_1 = require("unified");
|
|
11
|
+
const remark_parse_1 = __importDefault(require("remark-parse"));
|
|
12
|
+
const remark_gfm_1 = __importDefault(require("remark-gfm"));
|
|
13
|
+
/**
|
|
14
|
+
* Extract thumbnail URLs from data-attachment attribute JSON
|
|
15
|
+
* ClickUp API sometimes has broken thumbnail URLs, but data-attachment contains working ones
|
|
16
|
+
*/
|
|
17
|
+
function extractThumbnailsFromDataAttachment(attributes) {
|
|
18
|
+
if (!attributes || !attributes['data-attachment']) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const attachmentData = JSON.parse(attributes['data-attachment']);
|
|
23
|
+
return {
|
|
24
|
+
thumbnail_large: attachmentData.thumbnail_large,
|
|
25
|
+
thumbnail_medium: attachmentData.thumbnail_medium,
|
|
26
|
+
thumbnail_small: attachmentData.thumbnail_small,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error('Error parsing data-attachment:', error);
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Process an array of ClickUp text items into a structured content format
|
|
36
|
+
* that includes both text and images in their original sequence
|
|
37
|
+
*
|
|
38
|
+
* @param textItems Array of text items from ClickUp API
|
|
39
|
+
* @returns Promise resolving to an array of content blocks (text and images)
|
|
40
|
+
*/
|
|
41
|
+
async function convertClickUpTextItemsToToolCallResult(textItems) {
|
|
42
|
+
const contentBlocks = [];
|
|
43
|
+
let currentTextBlock = "";
|
|
44
|
+
let currentLine = ""; // Track current line separately for block formatting
|
|
45
|
+
// Track current formatting state to avoid unnecessary close/reopen
|
|
46
|
+
let activeBold = false;
|
|
47
|
+
let activeItalic = false;
|
|
48
|
+
let activeCode = false;
|
|
49
|
+
for (let i = 0; i < textItems.length; i++) {
|
|
50
|
+
const item = textItems[i];
|
|
51
|
+
// Handle image items
|
|
52
|
+
if (item.type === "image" && item.image && item.image.url) {
|
|
53
|
+
const imageFileName = item.image.name || item.image.title || "image";
|
|
54
|
+
const imageUrl = item.image.url;
|
|
55
|
+
const altText = item.text || imageFileName;
|
|
56
|
+
if (imageUrl.startsWith("data:")) {
|
|
57
|
+
const parsedData = (0, data_uri_1.parseDataUri)(imageUrl);
|
|
58
|
+
currentTextBlock += `\nImage: ${imageFileName} - [inline image data]`;
|
|
59
|
+
if (currentTextBlock.trim()) {
|
|
60
|
+
contentBlocks.push({
|
|
61
|
+
type: "text",
|
|
62
|
+
text: currentTextBlock.trim(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
currentTextBlock = "";
|
|
66
|
+
if (parsedData) {
|
|
67
|
+
contentBlocks.push({
|
|
68
|
+
type: "image_metadata",
|
|
69
|
+
urls: [],
|
|
70
|
+
alt: altText,
|
|
71
|
+
inlineData: parsedData,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.error(`Unable to parse inline image data for ${imageFileName}`);
|
|
76
|
+
contentBlocks.push({
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `[Image "${altText}" omitted: unsupported inline data URI]`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Add image URL reference inline to current text block
|
|
84
|
+
currentTextBlock += `\nImage: ${imageFileName} - ${imageUrl}`;
|
|
85
|
+
// Get working thumbnail URLs from data-attachment if available
|
|
86
|
+
const extractedThumbnails = extractThumbnailsFromDataAttachment(item.attributes);
|
|
87
|
+
// Determine best thumbnail URLs (prefer extracted over API thumbnails)
|
|
88
|
+
const thumbnail_large = extractedThumbnails.thumbnail_large || item.image.thumbnail_large;
|
|
89
|
+
const thumbnail_medium = extractedThumbnails.thumbnail_medium || item.image.thumbnail_medium;
|
|
90
|
+
const thumbnail_small = extractedThumbnails.thumbnail_small || item.image.thumbnail_small;
|
|
91
|
+
// Only create image_metadata if we have at least one thumbnail (never use original image)
|
|
92
|
+
if (thumbnail_large || thumbnail_medium || thumbnail_small) {
|
|
93
|
+
// Push accumulated text (including image URL) as a text block
|
|
94
|
+
if (currentTextBlock.trim()) {
|
|
95
|
+
contentBlocks.push({
|
|
96
|
+
type: "text",
|
|
97
|
+
text: currentTextBlock.trim(),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Reset current text block after pushing it
|
|
101
|
+
currentTextBlock = "";
|
|
102
|
+
// Create URLs array with largest to smallest preference, filter out undefined
|
|
103
|
+
const urls = [thumbnail_large, thumbnail_medium, thumbnail_small].filter(Boolean);
|
|
104
|
+
// Add image_metadata block for lazy loading
|
|
105
|
+
contentBlocks.push({
|
|
106
|
+
type: "image_metadata",
|
|
107
|
+
urls: urls,
|
|
108
|
+
alt: altText,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// If no thumbnails, just treat as a file reference (already added to currentTextBlock)
|
|
112
|
+
}
|
|
113
|
+
// Handle text items
|
|
114
|
+
else if (typeof item.text === "string") {
|
|
115
|
+
// Check if this is a newline with block formatting (header, blockquote, list)
|
|
116
|
+
if (item.text === '\n' && item.attributes) {
|
|
117
|
+
// Header formatting
|
|
118
|
+
if (item.attributes.header) {
|
|
119
|
+
const level = item.attributes.header;
|
|
120
|
+
currentLine = '#'.repeat(level) + ' ' + currentLine;
|
|
121
|
+
}
|
|
122
|
+
// Blockquote formatting
|
|
123
|
+
else if (item.attributes.blockquote) {
|
|
124
|
+
currentLine = '> ' + currentLine;
|
|
125
|
+
}
|
|
126
|
+
// List formatting
|
|
127
|
+
else if (item.attributes.list) {
|
|
128
|
+
const listType = item.attributes.list.list;
|
|
129
|
+
const indent = item.attributes.indent || 0;
|
|
130
|
+
// Add indentation (2 spaces per level) for nested lists
|
|
131
|
+
const indentStr = ' '.repeat(indent);
|
|
132
|
+
switch (listType) {
|
|
133
|
+
case 'bullet':
|
|
134
|
+
currentLine = indentStr + '- ' + currentLine;
|
|
135
|
+
break;
|
|
136
|
+
case 'ordered':
|
|
137
|
+
currentLine = indentStr + '1. ' + currentLine;
|
|
138
|
+
break;
|
|
139
|
+
case 'checked':
|
|
140
|
+
currentLine = indentStr + '- [x] ' + currentLine;
|
|
141
|
+
break;
|
|
142
|
+
case 'unchecked':
|
|
143
|
+
currentLine = indentStr + '- [ ] ' + currentLine;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Code block formatting
|
|
148
|
+
else if (item.attributes['code-block']) {
|
|
149
|
+
// Wrap the current line in code block markers
|
|
150
|
+
currentLine = '```\n' + currentLine + '\n```';
|
|
151
|
+
}
|
|
152
|
+
// Add formatted line to text block
|
|
153
|
+
currentTextBlock += currentLine;
|
|
154
|
+
// Add newline unless it's code block (already has newlines)
|
|
155
|
+
if (!item.attributes['code-block']) {
|
|
156
|
+
currentTextBlock += '\n';
|
|
157
|
+
}
|
|
158
|
+
currentLine = ""; // Reset for next line
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// Regular text with inline formatting
|
|
162
|
+
let formattedText = item.text;
|
|
163
|
+
// Determine current and next formatting state
|
|
164
|
+
const hasBold = item.attributes?.bold === true;
|
|
165
|
+
const hasItalic = item.attributes?.italic === true;
|
|
166
|
+
const hasLink = item.attributes?.link;
|
|
167
|
+
// Look ahead to next non-newline block
|
|
168
|
+
let nextHasBold = false;
|
|
169
|
+
let nextHasItalic = false;
|
|
170
|
+
for (let j = i + 1; j < textItems.length; j++) {
|
|
171
|
+
const nextItem = textItems[j];
|
|
172
|
+
if (nextItem.text !== '\n' || !nextItem.attributes) {
|
|
173
|
+
nextHasBold = nextItem.attributes?.bold === true;
|
|
174
|
+
nextHasItalic = nextItem.attributes?.italic === true;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Build prefix (open new formatting)
|
|
179
|
+
let prefix = "";
|
|
180
|
+
if (hasBold && !activeBold)
|
|
181
|
+
prefix += "**";
|
|
182
|
+
if (hasItalic && !activeItalic)
|
|
183
|
+
prefix += "*";
|
|
184
|
+
// Build suffix (close formatting that won't continue)
|
|
185
|
+
let suffix = "";
|
|
186
|
+
if (hasItalic && !nextHasItalic)
|
|
187
|
+
suffix += "*";
|
|
188
|
+
if (hasBold && !nextHasBold)
|
|
189
|
+
suffix += "**";
|
|
190
|
+
// Close formatting that's active but not in this block
|
|
191
|
+
let closingPrefix = "";
|
|
192
|
+
if (activeBold && !hasBold)
|
|
193
|
+
closingPrefix += "**";
|
|
194
|
+
if (activeItalic && !hasItalic)
|
|
195
|
+
closingPrefix += "*";
|
|
196
|
+
formattedText = closingPrefix + prefix + formattedText + suffix;
|
|
197
|
+
// Update state
|
|
198
|
+
activeBold = hasBold && nextHasBold;
|
|
199
|
+
activeItalic = hasItalic && nextHasItalic;
|
|
200
|
+
// Link formatting (wraps everything)
|
|
201
|
+
if (hasLink) {
|
|
202
|
+
formattedText = `[${formattedText}](${hasLink})`;
|
|
203
|
+
}
|
|
204
|
+
// Code formatting
|
|
205
|
+
if (item.attributes?.code) {
|
|
206
|
+
formattedText = `\`${formattedText}\``;
|
|
207
|
+
}
|
|
208
|
+
// Add to current line (not text block yet)
|
|
209
|
+
if (item.text === '\n') {
|
|
210
|
+
// Plain newline without formatting
|
|
211
|
+
currentTextBlock += currentLine + '\n';
|
|
212
|
+
currentLine = "";
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
currentLine += formattedText;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Handle other types of items like bookmarks or whatever clickup can think of
|
|
219
|
+
else {
|
|
220
|
+
currentTextBlock += JSON.stringify(item);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Add any remaining text
|
|
224
|
+
if (currentLine) {
|
|
225
|
+
currentTextBlock += currentLine;
|
|
226
|
+
}
|
|
227
|
+
if (currentTextBlock.trim()) {
|
|
228
|
+
contentBlocks.push({
|
|
229
|
+
type: "text",
|
|
230
|
+
text: currentTextBlock.trim(),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return contentBlocks;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Splits markdown text at image references and converts them to image blocks
|
|
237
|
+
* @param markdownText The markdown text to process
|
|
238
|
+
* @param attachments Array of attachments from the Clickup API
|
|
239
|
+
* @returns Array of content blocks (text and images)
|
|
240
|
+
*/
|
|
241
|
+
function convertMarkdownToToolCallResult(markdownText, attachments) {
|
|
242
|
+
const contentBlocks = [];
|
|
243
|
+
let currentTextBlock = "";
|
|
244
|
+
// Create a map of attachment URLs to their full info for easy lookup
|
|
245
|
+
const attachmentMap = new Map();
|
|
246
|
+
if (attachments && Array.isArray(attachments)) {
|
|
247
|
+
for (const attachment of attachments) {
|
|
248
|
+
attachmentMap.set(attachment.url, attachment);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Regular expression to match markdown image syntax: 
|
|
252
|
+
const imageRegex = /!\[([^\]]*)\]\(([^\)]+)\)/g;
|
|
253
|
+
let lastIndex = 0;
|
|
254
|
+
let match;
|
|
255
|
+
while ((match = imageRegex.exec(markdownText)) !== null) {
|
|
256
|
+
const [fullMatch, altText, imageUrl] = match;
|
|
257
|
+
// Add text before the image reference to the current text block
|
|
258
|
+
currentTextBlock += markdownText.substring(lastIndex, match.index);
|
|
259
|
+
if (imageUrl.startsWith("data:")) {
|
|
260
|
+
const imageFileName = altText || "image";
|
|
261
|
+
const parsedData = (0, data_uri_1.parseDataUri)(imageUrl);
|
|
262
|
+
currentTextBlock += `\nImage: ${imageFileName} - [inline image data]`;
|
|
263
|
+
if (currentTextBlock.trim()) {
|
|
264
|
+
contentBlocks.push({
|
|
265
|
+
type: "text",
|
|
266
|
+
text: currentTextBlock.trim(),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
currentTextBlock = "";
|
|
270
|
+
if (parsedData) {
|
|
271
|
+
contentBlocks.push({
|
|
272
|
+
type: "image_metadata",
|
|
273
|
+
urls: [],
|
|
274
|
+
alt: altText || imageFileName,
|
|
275
|
+
inlineData: parsedData,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
console.error(`Unable to parse inline image data for ${imageFileName}`);
|
|
280
|
+
contentBlocks.push({
|
|
281
|
+
type: "text",
|
|
282
|
+
text: `[Image "${altText || imageFileName}" omitted: unsupported inline data URI]`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
lastIndex = match.index + fullMatch.length;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
// Check if this image URL exists in our attachments
|
|
289
|
+
const attachment = attachmentMap.get(imageUrl);
|
|
290
|
+
if (attachment) {
|
|
291
|
+
// Add image URL reference inline to current text block
|
|
292
|
+
const imageFileName = altText || "image";
|
|
293
|
+
currentTextBlock += `\nImage: ${imageFileName} - ${imageUrl}`;
|
|
294
|
+
// Only create image_metadata if we have at least one thumbnail (never use original image)
|
|
295
|
+
if (attachment.thumbnail_large || attachment.thumbnail_medium || attachment.thumbnail_small) {
|
|
296
|
+
// Push accumulated text (including image URL) as a text block
|
|
297
|
+
if (currentTextBlock.trim()) {
|
|
298
|
+
contentBlocks.push({
|
|
299
|
+
type: "text",
|
|
300
|
+
text: currentTextBlock.trim(),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// Reset current text block after pushing it
|
|
304
|
+
currentTextBlock = "";
|
|
305
|
+
// Create URLs array with largest to smallest preference, filter out undefined
|
|
306
|
+
const urls = [attachment.thumbnail_large, attachment.thumbnail_medium, attachment.thumbnail_small].filter(Boolean);
|
|
307
|
+
// Add image_metadata block for lazy loading
|
|
308
|
+
contentBlocks.push({
|
|
309
|
+
type: "image_metadata",
|
|
310
|
+
urls: urls,
|
|
311
|
+
alt: altText || imageFileName,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// If no thumbnails, just treat as a file reference (already added to currentTextBlock)
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// If the image URL doesn't match any attachment, keep the original markdown in the current text block
|
|
318
|
+
currentTextBlock += fullMatch;
|
|
319
|
+
console.error(`Image URL ${imageUrl} not found in attachments`, attachmentMap);
|
|
320
|
+
}
|
|
321
|
+
lastIndex = match.index + fullMatch.length;
|
|
322
|
+
}
|
|
323
|
+
// Add any remaining text after the last image
|
|
324
|
+
currentTextBlock += markdownText.substring(lastIndex);
|
|
325
|
+
// Process non-image attachments that weren't referenced in markdown
|
|
326
|
+
const referencedUrls = new Set();
|
|
327
|
+
const imageMatches = markdownText.matchAll(/!\[([^\]]*)\]\(([^\)]+)\)/g);
|
|
328
|
+
for (const match of imageMatches) {
|
|
329
|
+
referencedUrls.add(match[2]);
|
|
330
|
+
}
|
|
331
|
+
// Add non-image files inline to the current text block
|
|
332
|
+
if (attachments && Array.isArray(attachments)) {
|
|
333
|
+
for (const attachment of attachments) {
|
|
334
|
+
if (!referencedUrls.has(attachment.url)) {
|
|
335
|
+
// Determine if this is an image based on URL or type
|
|
336
|
+
const isImage = attachment.thumbnail_large ||
|
|
337
|
+
/\.(jpg|jpeg|png|gif|webp|svg)$/i.test(attachment.url);
|
|
338
|
+
if (!isImage) {
|
|
339
|
+
// This is a non-image file - add inline to current text block
|
|
340
|
+
const fileName = extractFileNameFromUrl(attachment.url) || "file";
|
|
341
|
+
const fileType = extractFileTypeFromUrl(attachment.url);
|
|
342
|
+
const fileTypeText = fileType ? ` (${fileType.toUpperCase()})` : "";
|
|
343
|
+
currentTextBlock += `\nFile: ${fileName}${fileTypeText} - ${attachment.url}`;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Add any remaining text (including file references) as final text block
|
|
349
|
+
if (currentTextBlock.trim()) {
|
|
350
|
+
contentBlocks.push({
|
|
351
|
+
type: "text",
|
|
352
|
+
text: currentTextBlock.trim(),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return contentBlocks;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Extract filename from URL
|
|
359
|
+
*/
|
|
360
|
+
function extractFileNameFromUrl(url) {
|
|
361
|
+
try {
|
|
362
|
+
const urlObj = new URL(url);
|
|
363
|
+
const pathname = urlObj.pathname;
|
|
364
|
+
const filename = pathname.split('/').pop();
|
|
365
|
+
return filename && filename !== '' ? filename : null;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Extract file extension from URL
|
|
373
|
+
*/
|
|
374
|
+
function extractFileTypeFromUrl(url) {
|
|
375
|
+
const filename = extractFileNameFromUrl(url);
|
|
376
|
+
if (!filename)
|
|
377
|
+
return null;
|
|
378
|
+
const lastDot = filename.lastIndexOf('.');
|
|
379
|
+
if (lastDot === -1)
|
|
380
|
+
return null;
|
|
381
|
+
return filename.substring(lastDot + 1);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Convert markdown text to ClickUp comment blocks format using remark
|
|
385
|
+
* Supports: headers, bold, italic, code, links, lists, blockquotes, code blocks
|
|
386
|
+
*
|
|
387
|
+
* @param markdown The markdown text to convert
|
|
388
|
+
* @returns Array of ClickUp comment blocks
|
|
389
|
+
*/
|
|
390
|
+
function convertMarkdownToClickUpBlocks(markdown) {
|
|
391
|
+
const blocks = [];
|
|
392
|
+
try {
|
|
393
|
+
// Parse the entire markdown document using remark with GFM support (for task lists)
|
|
394
|
+
const tree = (0, unified_1.unified)()
|
|
395
|
+
.use(remark_parse_1.default)
|
|
396
|
+
.use(remark_gfm_1.default)
|
|
397
|
+
.parse(markdown);
|
|
398
|
+
// Walk the tree recursively
|
|
399
|
+
walkMdastNodes(tree.children, {}, blocks);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
console.error('Failed to parse markdown:', error);
|
|
403
|
+
// Fallback to plain text
|
|
404
|
+
return [{ text: markdown, attributes: {} }];
|
|
405
|
+
}
|
|
406
|
+
return blocks;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Recursively walk mdast nodes and convert to ClickUp blocks
|
|
410
|
+
* @param nodes Array of mdast nodes to process
|
|
411
|
+
* @param inheritedAttrs Formatting attributes inherited from parent nodes
|
|
412
|
+
* @param blocks Output array to append ClickUp blocks to
|
|
413
|
+
* @param depth Nesting depth for lists (0 = top level, 1 = first nest, etc.)
|
|
414
|
+
*/
|
|
415
|
+
function walkMdastNodes(nodes, inheritedAttrs, blocks, depth = 0) {
|
|
416
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
417
|
+
const node = nodes[i];
|
|
418
|
+
const currentAttrs = { ...inheritedAttrs };
|
|
419
|
+
switch (node.type) {
|
|
420
|
+
case 'heading':
|
|
421
|
+
// Process heading content with inline formatting
|
|
422
|
+
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
423
|
+
// Add newline with header attribute
|
|
424
|
+
blocks.push({ text: '\n', attributes: { header: node.depth } });
|
|
425
|
+
break;
|
|
426
|
+
case 'paragraph':
|
|
427
|
+
// Process paragraph content with inline formatting
|
|
428
|
+
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
429
|
+
// Add newline unless it's the last node
|
|
430
|
+
if (i < nodes.length - 1) {
|
|
431
|
+
blocks.push({ text: '\n', attributes: {} });
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
case 'blockquote':
|
|
435
|
+
// ClickUp limitation: Blockquotes only support paragraph content, not headers or lists
|
|
436
|
+
// For complex blockquote content (headers, lists), only paragraph text is preserved
|
|
437
|
+
const blockquoteChildren = node.children;
|
|
438
|
+
for (const child of blockquoteChildren) {
|
|
439
|
+
if (child.type === 'paragraph') {
|
|
440
|
+
walkPhrasingContent(child.children, currentAttrs, blocks);
|
|
441
|
+
blocks.push({ text: '\n', attributes: { blockquote: {} } });
|
|
442
|
+
}
|
|
443
|
+
// Note: Other child types (heading, list) are not supported by ClickUp blockquotes
|
|
444
|
+
// and will be silently skipped, preserving only inline paragraph content
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
case 'list':
|
|
448
|
+
const listNode = node;
|
|
449
|
+
const listType = listNode.ordered ? 'ordered' : 'bullet';
|
|
450
|
+
for (const item of listNode.children) {
|
|
451
|
+
const listItem = item;
|
|
452
|
+
// Check if it's a checkbox item
|
|
453
|
+
const isChecked = listItem.checked === true;
|
|
454
|
+
const isUnchecked = listItem.checked === false;
|
|
455
|
+
const finalListType = isChecked ? 'checked' : isUnchecked ? 'unchecked' : listType;
|
|
456
|
+
// Process list item content
|
|
457
|
+
for (const itemChild of listItem.children) {
|
|
458
|
+
if (itemChild.type === 'paragraph') {
|
|
459
|
+
// Process paragraph content with inline formatting
|
|
460
|
+
walkPhrasingContent(itemChild.children, currentAttrs, blocks);
|
|
461
|
+
// Add newline with list formatting and optional indent
|
|
462
|
+
const listAttrs = {
|
|
463
|
+
list: { list: finalListType }
|
|
464
|
+
};
|
|
465
|
+
// Add indent for nested lists (depth 0 = no indent, depth 1+ = indented)
|
|
466
|
+
if (depth > 0) {
|
|
467
|
+
listAttrs.indent = depth;
|
|
468
|
+
}
|
|
469
|
+
blocks.push({ text: '\n', attributes: listAttrs });
|
|
470
|
+
}
|
|
471
|
+
else if (itemChild.type === 'list') {
|
|
472
|
+
// Nested list - recursively process with increased depth
|
|
473
|
+
walkMdastNodes([itemChild], currentAttrs, blocks, depth + 1);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
case 'code':
|
|
479
|
+
// Code block
|
|
480
|
+
const codeNode = node;
|
|
481
|
+
if (codeNode.value) {
|
|
482
|
+
blocks.push({ text: codeNode.value, attributes: {} });
|
|
483
|
+
blocks.push({
|
|
484
|
+
text: '\n',
|
|
485
|
+
attributes: { 'code-block': { 'code-block': codeNode.lang || 'plain' } }
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
case 'thematicBreak':
|
|
490
|
+
// Horizontal rule - just add a line break
|
|
491
|
+
blocks.push({ text: '\n', attributes: {} });
|
|
492
|
+
break;
|
|
493
|
+
default:
|
|
494
|
+
// For any other block-level nodes, try to process children
|
|
495
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
496
|
+
walkMdastNodes(node.children, currentAttrs, blocks, depth);
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Recursively walk phrasing content (inline nodes) and build ClickUp blocks
|
|
504
|
+
* Accumulates formatting attributes from parent nodes
|
|
505
|
+
*/
|
|
506
|
+
function walkPhrasingContent(nodes, inheritedAttrs, blocks) {
|
|
507
|
+
for (const node of nodes) {
|
|
508
|
+
const currentAttrs = { ...inheritedAttrs };
|
|
509
|
+
switch (node.type) {
|
|
510
|
+
case 'text':
|
|
511
|
+
// Plain text node
|
|
512
|
+
if (node.value) {
|
|
513
|
+
blocks.push({
|
|
514
|
+
text: node.value,
|
|
515
|
+
attributes: Object.keys(currentAttrs).length > 0 ? currentAttrs : {}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
break;
|
|
519
|
+
case 'strong':
|
|
520
|
+
// Bold text - recurse with bold attribute
|
|
521
|
+
currentAttrs.bold = true;
|
|
522
|
+
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
523
|
+
break;
|
|
524
|
+
case 'emphasis':
|
|
525
|
+
// Italic text - recurse with italic attribute
|
|
526
|
+
currentAttrs.italic = true;
|
|
527
|
+
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
528
|
+
break;
|
|
529
|
+
case 'inlineCode':
|
|
530
|
+
// Inline code
|
|
531
|
+
if (node.value) {
|
|
532
|
+
currentAttrs.code = true;
|
|
533
|
+
blocks.push({
|
|
534
|
+
text: node.value,
|
|
535
|
+
attributes: currentAttrs
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
case 'link':
|
|
540
|
+
// Link - recurse with link attribute
|
|
541
|
+
currentAttrs.link = node.url;
|
|
542
|
+
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
543
|
+
break;
|
|
544
|
+
case 'break':
|
|
545
|
+
// Line break - add as plain text
|
|
546
|
+
blocks.push({ text: '\n', attributes: {} });
|
|
547
|
+
break;
|
|
548
|
+
default:
|
|
549
|
+
// For any other node types, try to extract text if available
|
|
550
|
+
if ('value' in node && typeof node.value === 'string') {
|
|
551
|
+
blocks.push({
|
|
552
|
+
text: node.value,
|
|
553
|
+
attributes: Object.keys(currentAttrs).length > 0 ? currentAttrs : {}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
else if ('children' in node && Array.isArray(node.children)) {
|
|
557
|
+
// Recurse into children for other container nodes
|
|
558
|
+
walkPhrasingContent(node.children, currentAttrs, blocks);
|
|
559
|
+
}
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsIpE,QAAA,MAAM,aAAa,oBAAqB,CAAC;AAIzC,OAAO,EAAE,aAAa,EAAE,CAAC"}
|