Skip to main content
Glama
comments.ts16.1 kB
import { ClickUpClient } from './index.js'; import { processClickUpResponse } from '../utils/markdown.js'; import { prepareCommentForClickUp, clickUpCommentToMarkdown, ClickUpCommentBlock } from '../utils/clickup-comment-formatter.js'; export interface Comment { id: string; comment: ClickUpCommentBlock[]; // ClickUp's structured comment format comment_text: string; // Plain text representation comment_markdown?: string; // Markdown version for display user: { id: number; username: string; email: string; color: string; profilePicture?: string; }; resolved: boolean; assignee?: { id: number; username: string; email: string; color: string; profilePicture?: string; }; assigned_by?: { id: number; username: string; email: string; color: string; profilePicture?: string; }; reactions?: { [key: string]: { count: number; users: Array<{ id: number; username: string; email: string; }>; }; }; date: string; start_date?: string; due_date?: string; parent?: string; replies_count?: number; } export interface GetTaskCommentsParams { start?: number; start_id?: string; } export interface CreateTaskCommentParams { comment_text?: string; // Make optional since we'll be using comment array assignee?: number; notify_all?: boolean; } export interface GetChatViewCommentsParams { start?: number; start_id?: string; } export interface CreateChatViewCommentParams { comment_text?: string; // Make optional since we'll be using comment array notify_all?: boolean; } export interface GetListCommentsParams { start?: number; start_id?: string; } export interface CreateListCommentParams { comment_text?: string; // Make optional since we'll be using comment array assignee?: number; notify_all?: boolean; } export interface UpdateCommentParams { comment_text?: string; // Make optional since we'll be using comment array assignee?: number; resolved?: boolean; } export interface GetThreadedCommentsParams { start?: number; start_id?: string; } export interface CreateThreadedCommentParams { comment_text?: string; // Make optional since we'll be using comment array notify_all?: boolean; } export class CommentsClient { private client: ClickUpClient; constructor(client: ClickUpClient) { this.client = client; } /** * Get comments for a specific task * @param taskId The ID of the task to get comments for * @param params Optional parameters for pagination * @returns A list of comments with processed content */ async getTaskComments(taskId: string, params?: GetTaskCommentsParams): Promise<{ comments: Comment[] }> { const result = await this.client.get(`/task/${taskId}/comment`, params); // Process each comment's content if (result.comments && Array.isArray(result.comments)) { result.comments = result.comments.map((comment: any) => { const processed = processClickUpResponse(comment); // Convert ClickUp comment format to markdown for display if (processed.comment && Array.isArray(processed.comment)) { try { processed.comment_markdown = clickUpCommentToMarkdown({ comment: processed.comment }); } catch (error) { console.warn('Failed to convert ClickUp comment to markdown:', error); } } return processed; }); } return result; } /** * Clean up ClickUp comment response by removing duplicate text blocks * ClickUp automatically appends original text as final block - we remove it */ private cleanupCommentResponse(processed: any): any { if (processed.comment && Array.isArray(processed.comment) && processed.comment.length > 1) { const lastBlock = processed.comment[processed.comment.length - 1]; // Check if the last block is a duplicate of the original markdown if (lastBlock && typeof lastBlock.text === 'string' && (!lastBlock.attributes || Object.keys(lastBlock.attributes).length === 0)) { // If the last block contains markdown syntax, it's likely the duplicate const hasMarkdownSyntax = /[*_`#[\]()>-]/.test(lastBlock.text) || lastBlock.text.includes('```') || lastBlock.text.includes('**') || lastBlock.text.includes('##'); if (hasMarkdownSyntax && lastBlock.text.length > 50) { // Remove the duplicate block processed.comment = processed.comment.slice(0, -1); // Also clean up the comment_text field if (processed.comment_text && processed.comment_text.includes(lastBlock.text)) { processed.comment_text = processed.comment_text.replace(lastBlock.text, '').trim(); } // Regenerate clean comment_markdown if (processed.comment && Array.isArray(processed.comment)) { try { processed.comment_markdown = clickUpCommentToMarkdown({ comment: processed.comment }); } catch (error) { console.warn('Failed to regenerate clean comment markdown:', error); } } } } } return processed; } /** * Create a new comment on a task * @param taskId The ID of the task to comment on * @param params The comment parameters (supports markdown in comment_text) * @returns The created comment with processed content */ async createTaskComment(taskId: string, params: CreateTaskCommentParams): Promise<Comment> { // Use ONLY structured format - no comment_text to avoid duplication const processedParams: any = { ...params }; if (params.comment_text) { const commentData = prepareCommentForClickUp(params.comment_text); // Send ONLY structured format - remove comment_text completely delete processedParams.comment_text; processedParams.comment = commentData.comment; // Remove any other text fields to prevent conflicts delete processedParams.text; delete processedParams.description; } const result = await this.client.post(`/task/${taskId}/comment`, processedParams); // Process the response let processed = processClickUpResponse(result); // Clean up ClickUp's duplicate text blocks processed = this.cleanupCommentResponse(processed); return processed; } /** * Get comments for a chat view * @param viewId The ID of the chat view to get comments for * @param params Optional parameters for pagination * @returns A list of comments with processed content */ async getChatViewComments(viewId: string, params?: GetChatViewCommentsParams): Promise<{ comments: Comment[] }> { const result = await this.client.get(`/view/${viewId}/comment`, params); // Process each comment's content if (result.comments && Array.isArray(result.comments)) { result.comments = result.comments.map((comment: any) => { const processed = processClickUpResponse(comment); // Convert ClickUp comment format to markdown for display if (processed.comment && Array.isArray(processed.comment)) { try { processed.comment_markdown = clickUpCommentToMarkdown({ comment: processed.comment }); } catch (error) { console.warn('Failed to convert ClickUp comment to markdown:', error); } } return processed; }); } return result; } /** * Create a new comment on a chat view * @param viewId The ID of the chat view to comment on * @param params The comment parameters (supports markdown in comment_text) * @returns The created comment with processed content */ async createChatViewComment(viewId: string, params: CreateChatViewCommentParams): Promise<Comment> { // Use ONLY structured format - no comment_text to avoid duplication const processedParams: any = { ...params }; if (params.comment_text) { const commentData = prepareCommentForClickUp(params.comment_text); // Send ONLY structured format - remove comment_text completely delete processedParams.comment_text; processedParams.comment = commentData.comment; // Remove any other text fields to prevent conflicts delete processedParams.text; delete processedParams.description; } const result = await this.client.post(`/view/${viewId}/comment`, processedParams); // Process the response const processed = processClickUpResponse(result); // Convert ClickUp comment format to markdown for display if (processed.comment && Array.isArray(processed.comment)) { try { processed.comment_markdown = clickUpCommentToMarkdown({ comment: processed.comment }); } catch (error) { console.warn('Failed to convert ClickUp comment to markdown:', error); } } return processed; } /** * Get comments for a list * @param listId The ID of the list to get comments for * @param params Optional parameters for pagination * @returns A list of comments with processed content */ async getListComments(listId: string, params?: GetListCommentsParams): Promise<{ comments: Comment[] }> { const result = await this.client.get(`/list/${listId}/comment`, params); // Process each comment's content if (result.comments && Array.isArray(result.comments)) { result.comments = result.comments.map((comment: any) => { const processed = processClickUpResponse(comment); // Convert ClickUp comment format to markdown for display if (processed.comment && Array.isArray(processed.comment)) { try { processed.comment_markdown = clickUpCommentToMarkdown({ comment: processed.comment }); } catch (error) { console.warn('Failed to convert ClickUp comment to markdown:', error); } } return processed; }); } return result; } /** * Create a new comment on a list * @param listId The ID of the list to comment on * @param params The comment parameters (supports markdown in comment_text) * @returns The created comment with processed content */ async createListComment(listId: string, params: CreateListCommentParams): Promise<Comment> { // Use ONLY structured format - no comment_text to avoid duplication const processedParams: any = { ...params }; if (params.comment_text) { const commentData = prepareCommentForClickUp(params.comment_text); // Send ONLY structured format - remove comment_text completely delete processedParams.comment_text; processedParams.comment = commentData.comment; // Remove any other text fields to prevent conflicts delete processedParams.text; delete processedParams.description; } const result = await this.client.post(`/list/${listId}/comment`, processedParams); // Process the response const processed = processClickUpResponse(result); // Convert ClickUp comment format to markdown for display if (processed.comment && Array.isArray(processed.comment)) { try { processed.comment_markdown = clickUpCommentToMarkdown({ comment: processed.comment }); } catch (error) { console.warn('Failed to convert ClickUp comment to markdown:', error); } } return processed; } /** * Update an existing comment * @param commentId The ID of the comment to update * @param params The comment parameters to update (supports markdown in comment_text) * @returns The updated comment with processed content */ async updateComment(commentId: string, params: UpdateCommentParams): Promise<Comment> { // Use ONLY structured format - no comment_text to avoid duplication const processedParams: any = { ...params }; if (params.comment_text) { const commentData = prepareCommentForClickUp(params.comment_text); // Send ONLY structured format - remove comment_text completely delete processedParams.comment_text; processedParams.comment = commentData.comment; // Remove any other text fields to prevent conflicts delete processedParams.text; delete processedParams.description; } const result = await this.client.put(`/comment/${commentId}`, processedParams); // Process the response const processed = processClickUpResponse(result); // Convert ClickUp comment format to markdown for display if (processed.comment && Array.isArray(processed.comment)) { try { processed.comment_markdown = clickUpCommentToMarkdown({ comment: processed.comment }); } catch (error) { console.warn('Failed to convert ClickUp comment to markdown:', error); } } return processed; } /** * Delete a comment * @param commentId The ID of the comment to delete * @returns Success message */ async deleteComment(commentId: string): Promise<{ success: boolean }> { return this.client.delete(`/comment/${commentId}`); } /** * Get threaded comments for a parent comment * @param commentId The ID of the parent comment * @param params Optional parameters for pagination * @returns A list of threaded comments with processed content */ async getThreadedComments(commentId: string, params?: GetThreadedCommentsParams): Promise<{ comments: Comment[] }> { const result = await this.client.get(`/comment/${commentId}/reply`, params); // Process each comment's content if (result.comments && Array.isArray(result.comments)) { result.comments = result.comments.map((comment: any) => { const processed = processClickUpResponse(comment); // Convert ClickUp comment format to markdown for display if (processed.comment && Array.isArray(processed.comment)) { try { processed.comment_markdown = clickUpCommentToMarkdown({ comment: processed.comment }); } catch (error) { console.warn('Failed to convert ClickUp comment to markdown:', error); } } return processed; }); } return result; } /** * Create a new threaded comment on a parent comment * @param commentId The ID of the parent comment * @param params The comment parameters (supports markdown in comment_text) * @returns The created threaded comment with processed content */ async createThreadedComment(commentId: string, params: CreateThreadedCommentParams): Promise<Comment> { // Process comment text for ClickUp's structured format ONLY const processedParams: any = { ...params }; if (params.comment_text) { const commentData = prepareCommentForClickUp(params.comment_text); // Use ONLY ClickUp's structured comment format - no comment_text at all delete processedParams.comment_text; processedParams.comment = commentData.comment; // Remove any other text fields to prevent ClickUp auto-detection delete processedParams.text; delete processedParams.description; } const result = await this.client.post(`/comment/${commentId}/reply`, processedParams); // Process the response const processed = processClickUpResponse(result); // Convert ClickUp comment format to markdown for display if (processed.comment && Array.isArray(processed.comment)) { try { processed.comment_markdown = clickUpCommentToMarkdown({ comment: processed.comment }); } catch (error) { console.warn('Failed to convert ClickUp comment to markdown:', error); } } return processed; } } export const createCommentsClient = (client: ClickUpClient): CommentsClient => { return new CommentsClient(client); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Chykalophia/ClickUp-MCP-Server---Enhanced'

If you have feedback or need assistance with the MCP directory API, please join our Discord server