@taazkareem/clickup-mcp-server 0.4.67 → 0.4.69
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 +4 -4
- package/build/index.js +58 -9
- package/build/logger.js +166 -0
- package/build/server-state.js +93 -0
- package/build/server.js +77 -56
- package/build/server.log +154 -0
- package/build/services/clickup/base.js +158 -105
- package/build/services/clickup/index.js +27 -5
- package/build/services/clickup/task.js +46 -2
- package/build/services/clickup/workspace.js +22 -17
- package/build/services/shared.js +24 -4
- package/build/tools/cache.js +442 -30
- package/build/tools/debug.js +76 -0
- package/build/tools/folder.js +3 -9
- package/build/tools/list.js +3 -8
- package/build/tools/logs.js +55 -0
- package/build/tools/task.js +163 -89
- package/build/tools/workspace.js +11 -0
- package/package.json +1 -1
package/build/server.log
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
[2025-03-18T02:45:47.590Z] [PID:17869] INFO: [SharedServices] Creating shared ClickUp services singleton
|
|
2
|
+
[2025-03-18T02:45:47.592Z] [PID:17869] INFO: [ClickUpServices] Starting ClickUp services initialization
|
|
3
|
+
{
|
|
4
|
+
"teamId": "9014370478",
|
|
5
|
+
"baseUrl": "https://api.clickup.com/api/v2"
|
|
6
|
+
}
|
|
7
|
+
[2025-03-18T02:45:47.592Z] [PID:17869] INFO: [ClickUpServices] Initializing ClickUp Workspace service
|
|
8
|
+
[2025-03-18T02:45:47.593Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Initialized WorkspaceService
|
|
9
|
+
{
|
|
10
|
+
"teamId": "9014370478",
|
|
11
|
+
"baseUrl": "https://api.clickup.com/api/v2"
|
|
12
|
+
}
|
|
13
|
+
[2025-03-18T02:45:47.593Z] [PID:17869] INFO: [ClickUpServices] Initializing ClickUp Task service
|
|
14
|
+
[2025-03-18T02:45:47.594Z] [PID:17869] DEBUG: [ClickUp:TaskService] Initialized TaskService
|
|
15
|
+
{
|
|
16
|
+
"teamId": "9014370478",
|
|
17
|
+
"baseUrl": "https://api.clickup.com/api/v2"
|
|
18
|
+
}
|
|
19
|
+
[2025-03-18T02:45:47.594Z] [PID:17869] DEBUG: [ClickUp:ListService] Initialized ListService
|
|
20
|
+
{
|
|
21
|
+
"teamId": "9014370478",
|
|
22
|
+
"baseUrl": "https://api.clickup.com/api/v2"
|
|
23
|
+
}
|
|
24
|
+
[2025-03-18T02:45:47.594Z] [PID:17869] INFO: [ClickUpServices] Initializing ClickUp List service
|
|
25
|
+
[2025-03-18T02:45:47.595Z] [PID:17869] DEBUG: [ClickUp:ListService] Initialized ListService
|
|
26
|
+
{
|
|
27
|
+
"teamId": "9014370478",
|
|
28
|
+
"baseUrl": "https://api.clickup.com/api/v2"
|
|
29
|
+
}
|
|
30
|
+
[2025-03-18T02:45:47.595Z] [PID:17869] INFO: [ClickUpServices] Initializing ClickUp Folder service
|
|
31
|
+
[2025-03-18T02:45:47.595Z] [PID:17869] DEBUG: [ClickUp:FolderService] Initialized FolderService
|
|
32
|
+
{
|
|
33
|
+
"teamId": "9014370478",
|
|
34
|
+
"baseUrl": "https://api.clickup.com/api/v2"
|
|
35
|
+
}
|
|
36
|
+
[2025-03-18T02:45:47.595Z] [PID:17869] INFO: [ClickUpServices] All ClickUp services initialized successfully
|
|
37
|
+
{
|
|
38
|
+
"services": [
|
|
39
|
+
"workspace",
|
|
40
|
+
"task",
|
|
41
|
+
"list",
|
|
42
|
+
"folder"
|
|
43
|
+
],
|
|
44
|
+
"baseUrl": "https://api.clickup.com/api/v2"
|
|
45
|
+
}
|
|
46
|
+
[2025-03-18T02:45:47.595Z] [PID:17869] INFO: [SharedServices] Services initialization complete
|
|
47
|
+
{
|
|
48
|
+
"services": "workspace, task, list, folder",
|
|
49
|
+
"teamId": "9014370478"
|
|
50
|
+
}
|
|
51
|
+
[2025-03-18T02:45:47.596Z] [PID:17869] INFO: Starting ClickUp MCP Server...
|
|
52
|
+
[2025-03-18T02:45:48.175Z] [PID:17869] INFO: Server environment
|
|
53
|
+
{
|
|
54
|
+
"pid": 17869,
|
|
55
|
+
"node": "v23.5.0",
|
|
56
|
+
"os": "darwin",
|
|
57
|
+
"arch": "x64"
|
|
58
|
+
}
|
|
59
|
+
[2025-03-18T02:45:48.176Z] [PID:17869] INFO: Initializing workspace tools
|
|
60
|
+
[2025-03-18T02:45:48.176Z] [PID:17869] INFO: [WorkspaceTool] Initializing workspace tool
|
|
61
|
+
[2025-03-18T02:45:48.176Z] [PID:17869] INFO: [WorkspaceTool] Workspace tool initialized successfully
|
|
62
|
+
{
|
|
63
|
+
"serviceType": "WorkspaceService"
|
|
64
|
+
}
|
|
65
|
+
[2025-03-18T02:45:48.176Z] [PID:17869] INFO: Configuring server request handlers
|
|
66
|
+
[2025-03-18T02:45:48.176Z] [PID:17869] INFO: [Server] Registering server request handlers
|
|
67
|
+
[2025-03-18T02:45:48.176Z] [PID:17869] INFO: [Server] Registering tool handlers
|
|
68
|
+
{
|
|
69
|
+
"toolCount": 22,
|
|
70
|
+
"categories": [
|
|
71
|
+
"workspace",
|
|
72
|
+
"task",
|
|
73
|
+
"list",
|
|
74
|
+
"folder"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
[2025-03-18T02:45:48.176Z] [PID:17869] INFO: Connecting to MCP stdio transport
|
|
78
|
+
[2025-03-18T02:45:48.177Z] [PID:17869] INFO: Server startup complete - ready to handle requests
|
|
79
|
+
[2025-03-18T02:45:48.908Z] [PID:17869] DEBUG: [Server] Received ListTools request
|
|
80
|
+
[2025-03-18T02:45:55.731Z] [PID:17869] INFO: [Server] Received CallTool request for tool: update_task_batch
|
|
81
|
+
{
|
|
82
|
+
"params": {
|
|
83
|
+
"task1_name": "📌 Now With Added Emoji",
|
|
84
|
+
"task1_list": "clickup-mcp-server",
|
|
85
|
+
"task1_status": "in progress",
|
|
86
|
+
"task2_id": "86b4a5d5h",
|
|
87
|
+
"task2_status": "in progress"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
[2025-03-18T02:45:56.405Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 673ms
|
|
91
|
+
[2025-03-18T02:45:56.778Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 373ms
|
|
92
|
+
[2025-03-18T02:45:57.131Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 351ms
|
|
93
|
+
[2025-03-18T02:45:57.697Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 566ms
|
|
94
|
+
[2025-03-18T02:45:57.697Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Found 2 folderless lists in space 90141365861
|
|
95
|
+
[2025-03-18T02:45:57.697Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding 2 lists directly to space Talib's Space (90141365861)
|
|
96
|
+
[2025-03-18T02:45:57.697Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: Personal List (901403617613)
|
|
97
|
+
[2025-03-18T02:45:57.697Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: Work List (901403621899)
|
|
98
|
+
[2025-03-18T02:45:58.083Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 385ms
|
|
99
|
+
[2025-03-18T02:45:58.517Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 433ms
|
|
100
|
+
[2025-03-18T02:45:58.916Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 398ms
|
|
101
|
+
[2025-03-18T02:45:58.917Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Found 0 folderless lists in space 90141864154
|
|
102
|
+
[2025-03-18T02:45:58.918Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding 0 lists directly to space Education (90141864154)
|
|
103
|
+
[2025-03-18T02:45:59.264Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 345ms
|
|
104
|
+
[2025-03-18T02:45:59.672Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 408ms
|
|
105
|
+
[2025-03-18T02:45:59.672Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Found 1 folderless lists in space 90141369187
|
|
106
|
+
[2025-03-18T02:45:59.672Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding 1 lists directly to space Social Media Content (90141369187)
|
|
107
|
+
[2025-03-18T02:45:59.672Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: VibeCase (901403679582)
|
|
108
|
+
[2025-03-18T02:46:00.159Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 487ms
|
|
109
|
+
[2025-03-18T02:46:00.564Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 405ms
|
|
110
|
+
[2025-03-18T02:46:01.094Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 530ms
|
|
111
|
+
[2025-03-18T02:46:01.691Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 597ms
|
|
112
|
+
[2025-03-18T02:46:02.048Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 357ms
|
|
113
|
+
[2025-03-18T02:46:02.510Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 461ms
|
|
114
|
+
[2025-03-18T02:46:02.891Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 381ms
|
|
115
|
+
[2025-03-18T02:46:03.412Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 521ms
|
|
116
|
+
[2025-03-18T02:46:03.933Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 521ms
|
|
117
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Request completed successfully in 523ms
|
|
118
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Found 8 folderless lists in space 90141392755
|
|
119
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding 8 lists directly to space Custom Space (90141392755)
|
|
120
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: Job Applications (901404823810)
|
|
121
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: Prompts | Snippets | Commands (901407112060)
|
|
122
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: Style Scraper (901408105509)
|
|
123
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: goal-tracker (901408127809)
|
|
124
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: Cursor Rules MCP Server (901408134949)
|
|
125
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: cursor-rules-mcp-server (901408144363)
|
|
126
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: Items to Sell Online (901404691843)
|
|
127
|
+
[2025-03-18T02:46:04.456Z] [PID:17869] DEBUG: [ClickUp:WorkspaceService] Adding list directly to space: clickup-mcp-server (901408020907)
|
|
128
|
+
[2025-03-18T02:46:04.457Z] [PID:17869] INFO: [ClickUp:TaskService] Operation: findTaskByName
|
|
129
|
+
{
|
|
130
|
+
"listId": "901407953112",
|
|
131
|
+
"taskName": "📌 Now With Added Emoji"
|
|
132
|
+
}
|
|
133
|
+
[2025-03-18T02:46:04.457Z] [PID:17869] INFO: [ClickUp:TaskService] Operation: getTasks
|
|
134
|
+
{
|
|
135
|
+
"listId": "901407953112",
|
|
136
|
+
"filters": {}
|
|
137
|
+
}
|
|
138
|
+
[2025-03-18T02:46:05.091Z] [PID:17869] DEBUG: [ClickUp:TaskService] Request completed successfully in 634ms
|
|
139
|
+
[2025-03-18T02:46:05.091Z] [PID:17869] INFO: [ClickUp:TaskService] Operation: updateTask
|
|
140
|
+
{
|
|
141
|
+
"taskId": "86b48gafv",
|
|
142
|
+
"status": "in progress"
|
|
143
|
+
}
|
|
144
|
+
[2025-03-18T02:46:05.924Z] [PID:17869] DEBUG: [ClickUp:TaskService] Request completed successfully in 832ms
|
|
145
|
+
[2025-03-18T02:46:05.924Z] [PID:17869] INFO: [ClickUp:TaskService] Operation: updateTask
|
|
146
|
+
{
|
|
147
|
+
"taskId": "86b4a5d5h",
|
|
148
|
+
"status": "in progress"
|
|
149
|
+
}
|
|
150
|
+
[2025-03-18T02:46:06.505Z] [PID:17869] DEBUG: [ClickUp:TaskService] Request completed successfully in 581ms
|
|
151
|
+
[2025-03-18T02:48:53.078Z] [PID:17869] INFO: [Server] Received CallTool request for tool: get_workspace_hierarchy
|
|
152
|
+
{
|
|
153
|
+
"params": {}
|
|
154
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Common request methods
|
|
9
9
|
*/
|
|
10
10
|
import axios from 'axios';
|
|
11
|
+
import { Logger, LogLevel } from '../../logger.js';
|
|
11
12
|
/**
|
|
12
13
|
* Error types for better error handling
|
|
13
14
|
*/
|
|
@@ -56,6 +57,9 @@ export class BaseClickUpService {
|
|
|
56
57
|
this.apiKey = apiKey;
|
|
57
58
|
this.teamId = teamId;
|
|
58
59
|
this.requestSpacing = this.defaultRequestSpacing;
|
|
60
|
+
// Create a logger with the actual class name for better context
|
|
61
|
+
const className = this.constructor.name;
|
|
62
|
+
this.logger = new Logger(`ClickUp:${className}`);
|
|
59
63
|
// Configure the Axios client with default settings
|
|
60
64
|
this.client = axios.create({
|
|
61
65
|
baseURL: baseUrl,
|
|
@@ -65,6 +69,7 @@ export class BaseClickUpService {
|
|
|
65
69
|
},
|
|
66
70
|
timeout: this.timeout
|
|
67
71
|
});
|
|
72
|
+
this.logger.debug(`Initialized ${className}`, { teamId, baseUrl });
|
|
68
73
|
// Add response interceptor for error handling
|
|
69
74
|
this.client.interceptors.response.use(response => response, error => this.handleAxiosError(error));
|
|
70
75
|
}
|
|
@@ -75,88 +80,89 @@ export class BaseClickUpService {
|
|
|
75
80
|
* @returns Never - always throws an error
|
|
76
81
|
*/
|
|
77
82
|
handleAxiosError(error) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
message = 'Resource not found';
|
|
97
|
-
code = ErrorCode.NOT_FOUND;
|
|
98
|
-
break;
|
|
99
|
-
case 429:
|
|
100
|
-
message = 'Rate limit exceeded';
|
|
101
|
-
code = ErrorCode.RATE_LIMIT;
|
|
102
|
-
break;
|
|
103
|
-
case 400:
|
|
104
|
-
message = 'Invalid request: ' + (error.response.data?.err || 'Validation error');
|
|
105
|
-
code = ErrorCode.VALIDATION;
|
|
106
|
-
break;
|
|
107
|
-
case 500:
|
|
108
|
-
case 502:
|
|
109
|
-
case 503:
|
|
110
|
-
case 504:
|
|
111
|
-
message = 'ClickUp server error';
|
|
112
|
-
code = ErrorCode.SERVER_ERROR;
|
|
113
|
-
break;
|
|
114
|
-
default:
|
|
115
|
-
message = `ClickUp API error (${status}): ${error.response.data?.err || 'Unknown error'}`;
|
|
116
|
-
}
|
|
83
|
+
// Determine error details
|
|
84
|
+
const status = error.response?.status;
|
|
85
|
+
const responseData = error.response?.data;
|
|
86
|
+
const errorMsg = responseData?.err || responseData?.error || error.message || 'Unknown API error';
|
|
87
|
+
const path = error.config?.url || 'unknown path';
|
|
88
|
+
// Context object for providing more detailed log information
|
|
89
|
+
const errorContext = {
|
|
90
|
+
path,
|
|
91
|
+
status,
|
|
92
|
+
method: error.config?.method?.toUpperCase() || 'UNKNOWN',
|
|
93
|
+
requestData: error.config?.data ? JSON.parse(error.config.data) : undefined
|
|
94
|
+
};
|
|
95
|
+
// Pick the appropriate error code based on status
|
|
96
|
+
let code;
|
|
97
|
+
let logMessage;
|
|
98
|
+
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
|
|
99
|
+
code = ErrorCode.NETWORK_ERROR;
|
|
100
|
+
logMessage = `Request timeout for ${path}`;
|
|
117
101
|
}
|
|
118
|
-
else if (error.
|
|
119
|
-
// Request was made but no response received
|
|
120
|
-
message = 'Network error: No response received from ClickUp';
|
|
102
|
+
else if (!error.response) {
|
|
121
103
|
code = ErrorCode.NETWORK_ERROR;
|
|
122
|
-
|
|
104
|
+
logMessage = `Network error accessing ${path}: ${error.message}`;
|
|
105
|
+
}
|
|
106
|
+
else if (status === 429) {
|
|
107
|
+
code = ErrorCode.RATE_LIMIT;
|
|
108
|
+
this.handleRateLimitHeaders(error.response.headers);
|
|
109
|
+
logMessage = `Rate limit exceeded for ${path}`;
|
|
110
|
+
}
|
|
111
|
+
else if (status === 401 || status === 403) {
|
|
112
|
+
code = ErrorCode.UNAUTHORIZED;
|
|
113
|
+
logMessage = `Authorization failed for ${path}`;
|
|
114
|
+
}
|
|
115
|
+
else if (status === 404) {
|
|
116
|
+
code = ErrorCode.NOT_FOUND;
|
|
117
|
+
logMessage = `Resource not found: ${path}`;
|
|
118
|
+
}
|
|
119
|
+
else if (status >= 400 && status < 500) {
|
|
120
|
+
code = ErrorCode.VALIDATION;
|
|
121
|
+
logMessage = `Validation error for ${path}: ${errorMsg}`;
|
|
122
|
+
}
|
|
123
|
+
else if (status >= 500) {
|
|
124
|
+
code = ErrorCode.SERVER_ERROR;
|
|
125
|
+
logMessage = `ClickUp server error: ${errorMsg}`;
|
|
123
126
|
}
|
|
124
127
|
else {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
details = { message: error.message };
|
|
128
|
+
code = ErrorCode.UNKNOWN;
|
|
129
|
+
logMessage = `Unknown API error: ${errorMsg}`;
|
|
128
130
|
}
|
|
129
|
-
|
|
131
|
+
// Log the error with context
|
|
132
|
+
this.logger.error(logMessage, errorContext);
|
|
133
|
+
// Throw a well-structured error
|
|
134
|
+
throw new ClickUpServiceError(errorMsg, code, responseData, status, errorContext);
|
|
130
135
|
}
|
|
131
136
|
/**
|
|
132
137
|
* Process the request queue, respecting rate limits by spacing out requests
|
|
133
138
|
* @private
|
|
134
139
|
*/
|
|
135
140
|
async processQueue() {
|
|
136
|
-
if (this.
|
|
141
|
+
if (this.requestQueue.length === 0) {
|
|
142
|
+
this.logger.debug('Queue empty, exiting queue processing mode');
|
|
143
|
+
this.processingQueue = false;
|
|
137
144
|
return;
|
|
138
145
|
}
|
|
139
|
-
this.
|
|
146
|
+
this.logger.debug(`Processing request queue (${this.requestQueue.length} items)`);
|
|
147
|
+
const startTime = Date.now();
|
|
140
148
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.error('Request failed:', error);
|
|
149
|
-
// Continue processing queue even if one request fails
|
|
150
|
-
}
|
|
151
|
-
// Space out requests to stay within rate limit
|
|
152
|
-
if (this.requestQueue.length > 0) {
|
|
153
|
-
await new Promise(resolve => setTimeout(resolve, this.requestSpacing));
|
|
154
|
-
}
|
|
155
|
-
}
|
|
149
|
+
// Take the first request from the queue
|
|
150
|
+
const request = this.requestQueue.shift();
|
|
151
|
+
if (request) {
|
|
152
|
+
// Wait for the request spacing interval
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, this.requestSpacing));
|
|
154
|
+
// Run the request
|
|
155
|
+
await request();
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
this.logger.error('Error executing queued request', error);
|
|
160
|
+
}
|
|
158
161
|
finally {
|
|
159
|
-
|
|
162
|
+
const duration = Date.now() - startTime;
|
|
163
|
+
this.logger.trace(`Queue item processed in ${duration}ms, ${this.requestQueue.length} items remaining`);
|
|
164
|
+
// Continue processing the queue after a short delay
|
|
165
|
+
setTimeout(() => this.processQueue(), this.requestSpacing);
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
168
|
/**
|
|
@@ -165,21 +171,38 @@ export class BaseClickUpService {
|
|
|
165
171
|
* @param headers Response headers from ClickUp
|
|
166
172
|
*/
|
|
167
173
|
handleRateLimitHeaders(headers) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.
|
|
174
|
+
try {
|
|
175
|
+
// Parse the rate limit headers
|
|
176
|
+
const limit = headers['x-ratelimit-limit'];
|
|
177
|
+
const remaining = headers['x-ratelimit-remaining'];
|
|
178
|
+
const reset = headers['x-ratelimit-reset'];
|
|
179
|
+
// Only log if we're getting close to the limit
|
|
180
|
+
if (remaining < limit * 0.2) {
|
|
181
|
+
this.logger.warn('Approaching rate limit', { remaining, limit, reset });
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
this.logger.debug('Rate limit status', { remaining, limit, reset });
|
|
185
|
+
}
|
|
186
|
+
if (reset) {
|
|
187
|
+
this.lastRateLimitReset = reset;
|
|
188
|
+
// If reset is in the future, calculate a safe request spacing
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
const resetTime = reset * 1000; // convert to milliseconds
|
|
191
|
+
const timeToReset = Math.max(0, resetTime - now);
|
|
192
|
+
if (timeToReset > 0 && remaining > 0) {
|
|
193
|
+
// Calculate time between requests to stay under limit
|
|
194
|
+
// Add 10% buffer to be safe
|
|
195
|
+
const safeSpacing = Math.ceil((timeToReset / remaining) * 1.1);
|
|
196
|
+
// Only adjust if it's greater than our current spacing
|
|
197
|
+
if (safeSpacing > this.requestSpacing) {
|
|
198
|
+
this.logger.debug(`Adjusting request spacing: ${this.requestSpacing}ms → ${safeSpacing}ms`);
|
|
199
|
+
this.requestSpacing = safeSpacing;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
179
202
|
}
|
|
180
203
|
}
|
|
181
|
-
|
|
182
|
-
this.
|
|
204
|
+
catch (error) {
|
|
205
|
+
this.logger.warn('Failed to parse rate limit headers', error);
|
|
183
206
|
}
|
|
184
207
|
}
|
|
185
208
|
/**
|
|
@@ -189,41 +212,59 @@ export class BaseClickUpService {
|
|
|
189
212
|
* @returns Promise that resolves with the result of the API request
|
|
190
213
|
*/
|
|
191
214
|
async makeRequest(fn) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
215
|
+
// If we're being rate limited, queue the request rather than executing immediately
|
|
216
|
+
if (this.processingQueue) {
|
|
217
|
+
this.logger.debug('Queue active, adding request to queue');
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
this.requestQueue.push(async () => {
|
|
220
|
+
try {
|
|
221
|
+
const result = await fn();
|
|
222
|
+
resolve(result);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
reject(error);
|
|
199
226
|
}
|
|
200
|
-
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const startTime = Date.now();
|
|
231
|
+
try {
|
|
232
|
+
// Execute the request function
|
|
233
|
+
const result = await fn();
|
|
234
|
+
// Debug log for successful requests with timing information
|
|
235
|
+
const duration = Date.now() - startTime;
|
|
236
|
+
this.logger.debug(`Request completed successfully in ${duration}ms`);
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
// If we hit a rate limit, start processing the queue
|
|
241
|
+
if (error instanceof ClickUpServiceError && error.code === ErrorCode.RATE_LIMIT) {
|
|
242
|
+
this.logger.warn('Rate limit reached, switching to queue mode', {
|
|
243
|
+
reset: this.lastRateLimitReset,
|
|
244
|
+
queueLength: this.requestQueue.length
|
|
245
|
+
});
|
|
246
|
+
if (!this.processingQueue) {
|
|
247
|
+
this.processingQueue = true;
|
|
248
|
+
this.processQueue().catch(err => {
|
|
249
|
+
this.logger.error('Error processing request queue', err);
|
|
250
|
+
});
|
|
201
251
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const resetTime = parseInt(error.response.headers['x-ratelimit-reset'] || '0', 10);
|
|
206
|
-
// Use the more precise reset time if available
|
|
207
|
-
const waitTime = resetTime > 0 ?
|
|
208
|
-
(resetTime * 1000) - Date.now() :
|
|
209
|
-
retryAfter * 1000;
|
|
210
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
252
|
+
// Queue this failed request and return a promise that will resolve when it's retried
|
|
253
|
+
return new Promise((resolve, reject) => {
|
|
254
|
+
this.requestQueue.push(async () => {
|
|
211
255
|
try {
|
|
212
|
-
// Retry the request once after waiting
|
|
213
256
|
const result = await fn();
|
|
214
257
|
resolve(result);
|
|
215
258
|
}
|
|
216
259
|
catch (retryError) {
|
|
217
260
|
reject(retryError);
|
|
218
261
|
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.processQueue().catch(reject);
|
|
226
|
-
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
// For other errors, just throw
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
227
268
|
}
|
|
228
269
|
/**
|
|
229
270
|
* Gets the ClickUp team ID associated with this service instance
|
|
@@ -239,6 +280,18 @@ export class BaseClickUpService {
|
|
|
239
280
|
* @param details - Details about the operation
|
|
240
281
|
*/
|
|
241
282
|
logOperation(operation, details) {
|
|
242
|
-
|
|
283
|
+
this.logger.info(`Operation: ${operation}`, details);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Log detailed information about a request (path and payload)
|
|
287
|
+
* For trace level logging only
|
|
288
|
+
*/
|
|
289
|
+
traceRequest(method, url, data) {
|
|
290
|
+
if (this.logger.isLevelEnabled(LogLevel.TRACE)) {
|
|
291
|
+
this.logger.trace(`${method} ${url}`, {
|
|
292
|
+
payload: data,
|
|
293
|
+
teamId: this.teamId
|
|
294
|
+
});
|
|
295
|
+
}
|
|
243
296
|
}
|
|
244
297
|
}
|
|
@@ -18,6 +18,9 @@ import { WorkspaceService } from './workspace.js';
|
|
|
18
18
|
import { TaskService } from './task.js';
|
|
19
19
|
import { ListService } from './list.js';
|
|
20
20
|
import { FolderService } from './folder.js';
|
|
21
|
+
import { Logger } from '../../logger.js';
|
|
22
|
+
// Singleton logger for ClickUp services
|
|
23
|
+
const logger = new Logger('ClickUpServices');
|
|
21
24
|
/**
|
|
22
25
|
* Factory function to create instances of all ClickUp services
|
|
23
26
|
* @param config Configuration for the services
|
|
@@ -25,12 +28,31 @@ import { FolderService } from './folder.js';
|
|
|
25
28
|
*/
|
|
26
29
|
export function createClickUpServices(config) {
|
|
27
30
|
const { apiKey, teamId, baseUrl } = config;
|
|
28
|
-
//
|
|
31
|
+
// Log start of overall initialization
|
|
32
|
+
logger.info('Starting ClickUp services initialization', {
|
|
33
|
+
teamId,
|
|
34
|
+
baseUrl: baseUrl || 'https://api.clickup.com/api/v2'
|
|
35
|
+
});
|
|
36
|
+
// Create workspace service first since others depend on it
|
|
37
|
+
logger.info('Initializing ClickUp Workspace service');
|
|
29
38
|
const workspaceService = new WorkspaceService(apiKey, teamId, baseUrl);
|
|
30
|
-
|
|
39
|
+
// Initialize remaining services with workspace dependency
|
|
40
|
+
logger.info('Initializing ClickUp Task service');
|
|
41
|
+
const taskService = new TaskService(apiKey, teamId, baseUrl, workspaceService);
|
|
42
|
+
logger.info('Initializing ClickUp List service');
|
|
43
|
+
const listService = new ListService(apiKey, teamId, baseUrl, workspaceService);
|
|
44
|
+
logger.info('Initializing ClickUp Folder service');
|
|
45
|
+
const folderService = new FolderService(apiKey, teamId, baseUrl, workspaceService);
|
|
46
|
+
const services = {
|
|
31
47
|
workspace: workspaceService,
|
|
32
|
-
task:
|
|
33
|
-
list:
|
|
34
|
-
folder:
|
|
48
|
+
task: taskService,
|
|
49
|
+
list: listService,
|
|
50
|
+
folder: folderService
|
|
35
51
|
};
|
|
52
|
+
// Log successful completion
|
|
53
|
+
logger.info('All ClickUp services initialized successfully', {
|
|
54
|
+
services: Object.keys(services),
|
|
55
|
+
baseUrl: baseUrl || 'https://api.clickup.com/api/v2'
|
|
56
|
+
});
|
|
57
|
+
return services;
|
|
36
58
|
}
|
|
@@ -158,7 +158,7 @@ export class TaskService extends BaseClickUpService {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
/**
|
|
161
|
-
* Find a task by
|
|
161
|
+
* Find a task by name within a list
|
|
162
162
|
* @param listId The list ID to search within
|
|
163
163
|
* @param taskName The name of the task to find
|
|
164
164
|
* @returns The task if found, otherwise null
|
|
@@ -167,7 +167,26 @@ export class TaskService extends BaseClickUpService {
|
|
|
167
167
|
this.logOperation('findTaskByName', { listId, taskName });
|
|
168
168
|
try {
|
|
169
169
|
const tasks = await this.getTasks(listId);
|
|
170
|
-
|
|
170
|
+
// Normalize the search term
|
|
171
|
+
const normalizedSearchTerm = taskName.toLowerCase().trim();
|
|
172
|
+
// First try exact match
|
|
173
|
+
let matchingTask = tasks.find(task => task.name.toLowerCase().trim() === normalizedSearchTerm);
|
|
174
|
+
// If no exact match, try substring match
|
|
175
|
+
if (!matchingTask) {
|
|
176
|
+
matchingTask = tasks.find(task => task.name.toLowerCase().trim().includes(normalizedSearchTerm) ||
|
|
177
|
+
normalizedSearchTerm.includes(task.name.toLowerCase().trim()));
|
|
178
|
+
}
|
|
179
|
+
// If still no match and there are emoji characters, try matching without emoji
|
|
180
|
+
if (!matchingTask && /[\p{Emoji}]/u.test(normalizedSearchTerm)) {
|
|
181
|
+
// Remove emoji and try again (simple approximation)
|
|
182
|
+
const withoutEmoji = normalizedSearchTerm.replace(/[\p{Emoji}]/gu, '').trim();
|
|
183
|
+
matchingTask = tasks.find(task => {
|
|
184
|
+
const taskNameWithoutEmoji = task.name.toLowerCase().replace(/[\p{Emoji}]/gu, '').trim();
|
|
185
|
+
return taskNameWithoutEmoji === withoutEmoji ||
|
|
186
|
+
taskNameWithoutEmoji.includes(withoutEmoji) ||
|
|
187
|
+
withoutEmoji.includes(taskNameWithoutEmoji);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
171
190
|
return matchingTask || null;
|
|
172
191
|
}
|
|
173
192
|
catch (error) {
|
|
@@ -489,4 +508,29 @@ export class TaskService extends BaseClickUpService {
|
|
|
489
508
|
throw new ClickUpServiceError(`Failed to delete tasks in bulk: ${error.message}`, ErrorCode.UNKNOWN, error);
|
|
490
509
|
}
|
|
491
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Get comments for a specific task
|
|
513
|
+
* @param taskId The ID of the task to retrieve comments for
|
|
514
|
+
* @param start Optional parameter for pagination, timestamp from which to start fetching comments
|
|
515
|
+
* @param startId Optional parameter for pagination, comment ID from which to start fetching
|
|
516
|
+
* @returns Array of task comments
|
|
517
|
+
*/
|
|
518
|
+
async getTaskComments(taskId, start, startId) {
|
|
519
|
+
this.logOperation('getTaskComments', { taskId, start, startId });
|
|
520
|
+
try {
|
|
521
|
+
return await this.makeRequest(async () => {
|
|
522
|
+
const params = new URLSearchParams();
|
|
523
|
+
// Add pagination parameters if provided
|
|
524
|
+
if (start)
|
|
525
|
+
params.append('start', String(start));
|
|
526
|
+
if (startId)
|
|
527
|
+
params.append('start_id', startId);
|
|
528
|
+
const response = await this.client.get(`/task/${taskId}/comment`, { params });
|
|
529
|
+
return response.data.comments;
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
throw this.handleError(error, `Failed to retrieve comments for task ${taskId}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
492
536
|
}
|