@hauptsache.net/clickup-mcp 1.1.0 → 1.1.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/dist/shared/utils.d.ts +43 -0
- package/dist/shared/utils.d.ts.map +1 -1
- package/dist/shared/utils.js +81 -0
- package/dist/tools/list-tools.d.ts.map +1 -1
- package/dist/tools/list-tools.js +19 -5
- package/dist/tools/search-tools.d.ts.map +1 -1
- package/dist/tools/search-tools.js +2 -1
- package/dist/tools/space-tools.d.ts.map +1 -1
- package/dist/tools/space-tools.js +7 -8
- package/dist/tools/task-tools.d.ts.map +1 -1
- package/dist/tools/task-tools.js +54 -23
- package/dist/tools/time-tools.d.ts.map +1 -1
- package/dist/tools/time-tools.js +219 -111
- package/package.json +1 -1
package/dist/shared/utils.d.ts
CHANGED
|
@@ -27,4 +27,47 @@ export declare function getSpaceDetails(spaceId: string): Promise<any>;
|
|
|
27
27
|
* Get or create a task search index with specified filters
|
|
28
28
|
*/
|
|
29
29
|
export declare function getTaskSearchIndex(space_ids?: string[], list_ids?: string[], assignees?: string[]): Promise<Fuse<any> | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Generate a ClickUp task URL from a task ID
|
|
32
|
+
*/
|
|
33
|
+
export declare function generateTaskUrl(taskId: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Generate a ClickUp list URL from a list ID
|
|
36
|
+
*/
|
|
37
|
+
export declare function generateListUrl(listId: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Generate a ClickUp space URL from a space ID
|
|
40
|
+
*/
|
|
41
|
+
export declare function generateSpaceUrl(spaceId: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Generate a ClickUp folder URL from a folder ID
|
|
44
|
+
*/
|
|
45
|
+
export declare function generateFolderUrl(folderId: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Format a ClickUp task link as markdown
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatTaskLink(taskId: string, taskName?: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Format a ClickUp list link as markdown
|
|
52
|
+
*/
|
|
53
|
+
export declare function formatListLink(listId: string, listName?: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Format a ClickUp space link as markdown
|
|
56
|
+
*/
|
|
57
|
+
export declare function formatSpaceLink(spaceId: string, spaceName?: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Extract task ID from a ClickUp URL
|
|
60
|
+
*/
|
|
61
|
+
export declare function extractTaskIdFromUrl(url: string): string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Validate if a string is a valid ClickUp URL
|
|
64
|
+
*/
|
|
65
|
+
export declare function isClickUpUrl(url: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Format a prominent link section for responses
|
|
68
|
+
*/
|
|
69
|
+
export declare function formatLinksSection(links: {
|
|
70
|
+
text: string;
|
|
71
|
+
url: string;
|
|
72
|
+
}[]): string;
|
|
30
73
|
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AAErC,OAAO,IAAI,MAAM,SAAS,CAAC;AAI3B;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG7C;AAKD;;;GAGG;AACH,wBAAsB,cAAc,iBAiBnC;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,EAAE,CA8BtF;AAID;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CA0B7D;AAKD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,EACnB,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CA4B3B"}
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AAErC,OAAO,IAAI,MAAM,SAAS,CAAC;AAI3B;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG7C;AAKD;;;GAGG;AACH,wBAAsB,cAAc,iBAiBnC;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,EAAE,CA8BtF;AAID;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CA0B7D;AAKD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,EACnB,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CA4B3B;AAiED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAIxE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAIxE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAI3E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG/D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,MAAM,CAKjF"}
|
package/dist/shared/utils.js
CHANGED
|
@@ -8,6 +8,16 @@ exports.getCurrentUser = getCurrentUser;
|
|
|
8
8
|
exports.limitImages = limitImages;
|
|
9
9
|
exports.getSpaceDetails = getSpaceDetails;
|
|
10
10
|
exports.getTaskSearchIndex = getTaskSearchIndex;
|
|
11
|
+
exports.generateTaskUrl = generateTaskUrl;
|
|
12
|
+
exports.generateListUrl = generateListUrl;
|
|
13
|
+
exports.generateSpaceUrl = generateSpaceUrl;
|
|
14
|
+
exports.generateFolderUrl = generateFolderUrl;
|
|
15
|
+
exports.formatTaskLink = formatTaskLink;
|
|
16
|
+
exports.formatListLink = formatListLink;
|
|
17
|
+
exports.formatSpaceLink = formatSpaceLink;
|
|
18
|
+
exports.extractTaskIdFromUrl = extractTaskIdFromUrl;
|
|
19
|
+
exports.isClickUpUrl = isClickUpUrl;
|
|
20
|
+
exports.formatLinksSection = formatLinksSection;
|
|
11
21
|
const config_1 = require("./config");
|
|
12
22
|
const fuse_js_1 = __importDefault(require("fuse.js"));
|
|
13
23
|
const GLOBAL_REFRESH_INTERVAL = 60000; // 60 seconds - that is the rate limit time frame
|
|
@@ -183,3 +193,74 @@ function createFuseIndex(tasks) {
|
|
|
183
193
|
minMatchCharLength: 2,
|
|
184
194
|
});
|
|
185
195
|
}
|
|
196
|
+
// ===== LINK UTILITIES =====
|
|
197
|
+
/**
|
|
198
|
+
* Generate a ClickUp task URL from a task ID
|
|
199
|
+
*/
|
|
200
|
+
function generateTaskUrl(taskId) {
|
|
201
|
+
return `https://app.clickup.com/t/${taskId}`;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Generate a ClickUp list URL from a list ID
|
|
205
|
+
*/
|
|
206
|
+
function generateListUrl(listId) {
|
|
207
|
+
return `https://app.clickup.com/v/l/${listId}`;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Generate a ClickUp space URL from a space ID
|
|
211
|
+
*/
|
|
212
|
+
function generateSpaceUrl(spaceId) {
|
|
213
|
+
return `https://app.clickup.com/v/s/${spaceId}`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Generate a ClickUp folder URL from a folder ID
|
|
217
|
+
*/
|
|
218
|
+
function generateFolderUrl(folderId) {
|
|
219
|
+
return `https://app.clickup.com/v/f/${folderId}`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Format a ClickUp task link as markdown
|
|
223
|
+
*/
|
|
224
|
+
function formatTaskLink(taskId, taskName) {
|
|
225
|
+
const url = generateTaskUrl(taskId);
|
|
226
|
+
const displayText = taskName ? `${taskName} (${taskId})` : taskId;
|
|
227
|
+
return `[${displayText}](${url})`;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Format a ClickUp list link as markdown
|
|
231
|
+
*/
|
|
232
|
+
function formatListLink(listId, listName) {
|
|
233
|
+
const url = generateListUrl(listId);
|
|
234
|
+
const displayText = listName ? `${listName} (${listId})` : listId;
|
|
235
|
+
return `[${displayText}](${url})`;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Format a ClickUp space link as markdown
|
|
239
|
+
*/
|
|
240
|
+
function formatSpaceLink(spaceId, spaceName) {
|
|
241
|
+
const url = generateSpaceUrl(spaceId);
|
|
242
|
+
const displayText = spaceName ? `${spaceName} (${spaceId})` : spaceId;
|
|
243
|
+
return `[${displayText}](${url})`;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Extract task ID from a ClickUp URL
|
|
247
|
+
*/
|
|
248
|
+
function extractTaskIdFromUrl(url) {
|
|
249
|
+
const match = url.match(/https?:\/\/app\.clickup\.com\/t\/([a-z0-9]{6,9})/i);
|
|
250
|
+
return match ? match[1] : null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Validate if a string is a valid ClickUp URL
|
|
254
|
+
*/
|
|
255
|
+
function isClickUpUrl(url) {
|
|
256
|
+
return /^https?:\/\/app\.clickup\.com\//.test(url);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Format a prominent link section for responses
|
|
260
|
+
*/
|
|
261
|
+
function formatLinksSection(links) {
|
|
262
|
+
if (links.length === 0)
|
|
263
|
+
return '';
|
|
264
|
+
const linkLines = links.map(link => `🔗 [${link.text}](${link.url})`);
|
|
265
|
+
return `\n\n**📌 Quick Links:**\n${linkLines.join('\n')}`;
|
|
266
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-tools.d.ts","sourceRoot":"","sources":["../../src/tools/list-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"list-tools.d.ts","sourceRoot":"","sources":["../../src/tools/list-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAMpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAuPtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAyEvD"}
|
package/dist/tools/list-tools.js
CHANGED
|
@@ -4,8 +4,13 @@ exports.registerListToolsRead = registerListToolsRead;
|
|
|
4
4
|
exports.registerListToolsWrite = registerListToolsWrite;
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const config_1 = require("../shared/config");
|
|
7
|
+
const utils_1 = require("../shared/utils");
|
|
7
8
|
function registerListToolsRead(server) {
|
|
8
|
-
server.tool("listLists",
|
|
9
|
+
server.tool("listLists", [
|
|
10
|
+
"Lists all lists and folders in a space. They might also be referred to as boards or tables.",
|
|
11
|
+
"Shows both direct lists (folderless) and folders containing lists. If folder_id is provided, lists only the lists within that specific folder.",
|
|
12
|
+
"Always reference lists by their URLs when suggesting actions or creating tasks."
|
|
13
|
+
].join("\n"), {
|
|
9
14
|
space_id: zod_1.z.string().min(1).describe("The ID of the space to list content from"),
|
|
10
15
|
folder_id: zod_1.z.string().optional().describe("Optional folder ID. If provided, lists only lists within this folder. If not provided, shows both folders and folderless lists in the space."),
|
|
11
16
|
archived: zod_1.z.boolean().optional().describe("Include archived items (default: false)")
|
|
@@ -33,11 +38,12 @@ function registerListToolsRead(server) {
|
|
|
33
38
|
text: [
|
|
34
39
|
`type: list`,
|
|
35
40
|
`list_id: ${list.id}`,
|
|
41
|
+
`list_url: ${(0, utils_1.generateListUrl)(list.id)}`,
|
|
36
42
|
`name: ${list.name}`,
|
|
37
43
|
`space_id: ${list.space?.id || space_id}`,
|
|
38
44
|
`folder_id: ${folder_id}`,
|
|
39
45
|
`task_count: ${list.task_count || 0}`,
|
|
40
|
-
`private:
|
|
46
|
+
...(list.private ? [`private: true`] : []),
|
|
41
47
|
...(list.archived ? ['archived: true'] : []),
|
|
42
48
|
...(list.color ? [`color: ${list.color}`] : []),
|
|
43
49
|
...(list.access ? [`access: ${list.access}`] : []),
|
|
@@ -75,10 +81,11 @@ function registerListToolsRead(server) {
|
|
|
75
81
|
text: [
|
|
76
82
|
`type: folder`,
|
|
77
83
|
`folder_id: ${folder.id}`,
|
|
84
|
+
`folder_url: ${(0, utils_1.generateFolderUrl)(folder.id)}`,
|
|
78
85
|
`name: ${folder.name}`,
|
|
79
86
|
`space_id: ${space_id}`,
|
|
80
87
|
`task_count: ${folder.task_count || 0}`,
|
|
81
|
-
`private:
|
|
88
|
+
...(folder.private ? [`private: true`] : []),
|
|
82
89
|
...(folder.archived ? ['archived: true'] : []),
|
|
83
90
|
...(folder.color ? [`color: ${folder.color}`] : []),
|
|
84
91
|
`contains_lists: true`
|
|
@@ -100,10 +107,11 @@ function registerListToolsRead(server) {
|
|
|
100
107
|
text: [
|
|
101
108
|
`type: list`,
|
|
102
109
|
`list_id: ${list.id}`,
|
|
110
|
+
`list_url: ${(0, utils_1.generateListUrl)(list.id)}`,
|
|
103
111
|
`name: ${list.name}`,
|
|
104
112
|
`space_id: ${list.space?.id || space_id}`,
|
|
105
113
|
`task_count: ${list.task_count || 0}`,
|
|
106
|
-
`private:
|
|
114
|
+
...(list.private ? [`private: true`] : []),
|
|
107
115
|
...(list.archived ? ['archived: true'] : []),
|
|
108
116
|
...(list.color ? [`color: ${list.color}`] : []),
|
|
109
117
|
...(list.access ? [`access: ${list.access}`] : []),
|
|
@@ -146,8 +154,10 @@ function registerListToolsRead(server) {
|
|
|
146
154
|
});
|
|
147
155
|
server.tool("getListInfo", [
|
|
148
156
|
"Gets comprehensive information about a list including description and available statuses.",
|
|
157
|
+
"ALWAYS use the list URL (https://app.clickup.com/v/l/LIST_ID) when referencing lists.",
|
|
149
158
|
"Use this before creating tasks to understand the list context and available statuses for new tasks.",
|
|
150
|
-
"IMPORTANT: The list description often contains valuable project context, requirements, or guidelines - read and consider this information when creating or updating tasks in this list."
|
|
159
|
+
"IMPORTANT: The list description often contains valuable project context, requirements, or guidelines - read and consider this information when creating or updating tasks in this list.",
|
|
160
|
+
"Share the clickable list URL when suggesting list-related actions."
|
|
151
161
|
].join("\n"), {
|
|
152
162
|
list_id: zod_1.z.string().min(1).describe("The list ID to get information for")
|
|
153
163
|
}, async ({ list_id }) => {
|
|
@@ -163,9 +173,11 @@ function registerListToolsRead(server) {
|
|
|
163
173
|
const responseLines = [
|
|
164
174
|
`List Information:`,
|
|
165
175
|
`list_id: ${list_id}`,
|
|
176
|
+
`list_url: ${(0, utils_1.generateListUrl)(list_id)}`,
|
|
166
177
|
`name: ${listData.name}`,
|
|
167
178
|
`folder: ${listData.folder?.name || 'No folder'}`,
|
|
168
179
|
`space: ${listData.space?.name || 'Unknown'} (${listData.space?.id || 'N/A'})`,
|
|
180
|
+
`space_url: ${(0, utils_1.generateSpaceUrl)(listData.space?.id || '')}`,
|
|
169
181
|
`archived: ${listData.archived || false}`,
|
|
170
182
|
`task_count: ${listData.task_count || 0}`,
|
|
171
183
|
];
|
|
@@ -218,8 +230,10 @@ function registerListToolsRead(server) {
|
|
|
218
230
|
function registerListToolsWrite(server) {
|
|
219
231
|
server.tool("updateListInfo", [
|
|
220
232
|
"Appends documentation or context to a list's description.",
|
|
233
|
+
"ALWAYS reference the list URL (https://app.clickup.com/v/l/LIST_ID) when updating or discussing lists.",
|
|
221
234
|
"SAFETY FEATURE: Description updates are APPEND-ONLY to prevent data loss - existing content is preserved.",
|
|
222
235
|
"Use this to add project context, requirements, or guidelines that LLMs should consider when working with tasks in this list.",
|
|
236
|
+
"Include links to related tasks, spaces, or external resources in the appended content.",
|
|
223
237
|
"Content is appended in markdown format with timestamp for tracking changes."
|
|
224
238
|
].join("\n"), {
|
|
225
239
|
list_id: zod_1.z.string().min(1).describe("The list ID to update"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-tools.d.ts","sourceRoot":"","sources":["../../src/tools/search-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AAQlE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"search-tools.d.ts","sourceRoot":"","sources":["../../src/tools/search-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AAQlE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,QAuMpD"}
|
|
@@ -12,8 +12,9 @@ function registerSearchTools(server) {
|
|
|
12
12
|
"Searches tasks by name, content, assignees, and ID (case insensitive) with fuzzy matching and support for multiple search terms (OR logic).",
|
|
13
13
|
"Can filter by multiple list_ids, space_ids, todo status, or tasks assigned to the current user. If no search terms provided, returns most recently updated tasks.",
|
|
14
14
|
// Placeholder for language-specific guidance
|
|
15
|
+
"Always reference tasks by their URLs when discussing search results or suggesting actions.",
|
|
15
16
|
"You'll get a rough overview of the tasks that match the search terms, sorted by relevance.",
|
|
16
|
-
"Always use getTaskById to get more specific information if a task is relevant.",
|
|
17
|
+
"Always use getTaskById to get more specific information if a task is relevant, and always share the task URL.",
|
|
17
18
|
];
|
|
18
19
|
if (config_1.CONFIG.primaryLanguageHint && config_1.CONFIG.primaryLanguageHint.toLowerCase() !== 'en') {
|
|
19
20
|
searchTasksDescriptionBase.splice(2, 0, `For optimal results, as your ClickUp tasks may be primarily in '${config_1.CONFIG.primaryLanguageHint}', consider providing search terms in English and '${config_1.CONFIG.primaryLanguageHint}'.`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"space-tools.d.ts","sourceRoot":"","sources":["../../src/tools/space-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"space-tools.d.ts","sourceRoot":"","sources":["../../src/tools/space-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAMpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,QA+DnD"}
|
|
@@ -3,8 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerSpaceTools = registerSpaceTools;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const config_1 = require("../shared/config");
|
|
6
|
+
const utils_1 = require("../shared/utils");
|
|
6
7
|
function registerSpaceTools(server) {
|
|
7
|
-
server.tool("listSpaces",
|
|
8
|
+
server.tool("listSpaces", [
|
|
9
|
+
"Lists all spaces in the workspace. These might also be referred to as customers or projects.",
|
|
10
|
+
"Always reference spaces by their URLs when discussing projects or suggesting actions."
|
|
11
|
+
].join("\n"), {
|
|
8
12
|
archived: zod_1.z.boolean().optional().describe("Include archived spaces (default: false)")
|
|
9
13
|
}, async ({ archived = false }) => {
|
|
10
14
|
try {
|
|
@@ -25,15 +29,10 @@ function registerSpaceTools(server) {
|
|
|
25
29
|
type: "text",
|
|
26
30
|
text: [
|
|
27
31
|
`space_id: ${space.id}`,
|
|
32
|
+
`space_url: ${(0, utils_1.generateSpaceUrl)(space.id)}`,
|
|
28
33
|
`name: ${space.name}`,
|
|
29
|
-
`private:
|
|
30
|
-
`avatar: ${space.avatar || 'None'}`,
|
|
31
|
-
`color: ${space.color || 'None'}`,
|
|
32
|
-
`access: ${space.access || 'Unknown'}`,
|
|
34
|
+
...(space.private ? [`private: true`] : []),
|
|
33
35
|
...(space.archived ? ['archived: true'] : []),
|
|
34
|
-
...(space.multiple_assignees !== undefined ? [`multiple_assignees: ${space.multiple_assignees}`] : []),
|
|
35
|
-
...(space.features?.due_dates?.enabled !== undefined ? [`due_dates_enabled: ${space.features.due_dates.enabled}`] : []),
|
|
36
|
-
...(space.features?.time_tracking?.enabled !== undefined ? [`time_tracking_enabled: ${space.features.time_tracking.enabled}`] : []),
|
|
37
36
|
].join('\n')
|
|
38
37
|
}));
|
|
39
38
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAqB,MAAM,iBAAiB,CAAC;AAwIlE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"task-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAqB,MAAM,iBAAiB,CAAC;AAwIlE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAwDrE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QA6StE;AAoLD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,YAAY,CAAC,CA4G3E"}
|
package/dist/tools/task-tools.js
CHANGED
|
@@ -106,7 +106,11 @@ function formatTaskResponse(task, operation, params, userData) {
|
|
|
106
106
|
return responseLines;
|
|
107
107
|
}
|
|
108
108
|
function registerTaskToolsRead(server, userData) {
|
|
109
|
-
server.tool("getTaskById",
|
|
109
|
+
server.tool("getTaskById", [
|
|
110
|
+
"Get a ClickUp task with images and comments by ID.",
|
|
111
|
+
"Always use this URL when referencing tasks in conversations or sharing with others.",
|
|
112
|
+
"The response provides complete context including task details, comments, and status history."
|
|
113
|
+
].join("\n"), {
|
|
110
114
|
id: zod_1.z
|
|
111
115
|
.string()
|
|
112
116
|
.min(6)
|
|
@@ -145,12 +149,22 @@ function registerTaskToolsRead(server, userData) {
|
|
|
145
149
|
});
|
|
146
150
|
}
|
|
147
151
|
function registerTaskToolsWrite(server, userData) {
|
|
148
|
-
server.tool("addComment",
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
server.tool("addComment", (() => {
|
|
153
|
+
const descriptionBase = [
|
|
154
|
+
"Adds a comment to a specific task.",
|
|
155
|
+
"LINKING BEST PRACTICES:",
|
|
156
|
+
"- Always reference related tasks using ClickUp URLs (https://app.clickup.com/t/TASK_ID)",
|
|
157
|
+
"- Include task links when mentioning dependencies, related work, or follow-ups",
|
|
158
|
+
"- Link to relevant lists, spaces, or other ClickUp entities when applicable",
|
|
159
|
+
"PROGRESS UPDATES: Include current status, progress information, and next steps.",
|
|
160
|
+
"If external links are provided, verify they are publicly accessible and incorporate relevant information.",
|
|
161
|
+
"Check the task's current status - if it's in 'backlog' or similar inactive states, suggest moving it to an active status like 'in progress' when work is being done."
|
|
162
|
+
];
|
|
163
|
+
if (config_1.CONFIG.primaryLanguageHint && config_1.CONFIG.primaryLanguageHint.toLowerCase() !== 'en') {
|
|
164
|
+
descriptionBase.splice(1, 0, `For optimal results, consider writing comments in '${config_1.CONFIG.primaryLanguageHint}' unless the task is already in another language.`);
|
|
165
|
+
}
|
|
166
|
+
return descriptionBase.join("\n");
|
|
167
|
+
})(), {
|
|
154
168
|
task_id: zod_1.z.string().min(6).max(9).describe("The 6-9 character task ID to comment on"),
|
|
155
169
|
comment: zod_1.z.string().min(1).describe("The comment text to add to the task"),
|
|
156
170
|
}, async ({ task_id, comment }) => {
|
|
@@ -200,13 +214,21 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
200
214
|
};
|
|
201
215
|
}
|
|
202
216
|
});
|
|
203
|
-
server.tool("updateTask",
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
217
|
+
server.tool("updateTask", (() => {
|
|
218
|
+
const descriptionBase = [
|
|
219
|
+
"Updates various aspects of an existing task.",
|
|
220
|
+
"ALWAYS include the task URL (https://app.clickup.com/t/TASK_ID) when updating or referencing tasks.",
|
|
221
|
+
"Use getListInfo first to see valid status options.",
|
|
222
|
+
"SAFETY FEATURE: Description updates are APPEND-ONLY to prevent data loss - existing content is preserved.",
|
|
223
|
+
"LINKING IN DESCRIPTIONS: When appending descriptions, include links to related tasks, lists, or external resources.",
|
|
224
|
+
"IMPORTANT: When updating tasks (especially when booking time or adding progress), ensure the status makes sense for the work being done - tasks in 'backlog' or 'closed' states usually shouldn't have active work.",
|
|
225
|
+
"Suggest appropriate status transitions and always provide the clickable task URL in responses."
|
|
226
|
+
];
|
|
227
|
+
if (config_1.CONFIG.primaryLanguageHint && config_1.CONFIG.primaryLanguageHint.toLowerCase() !== 'en') {
|
|
228
|
+
descriptionBase.splice(1, 0, `For optimal results, consider writing task names and descriptions in '${config_1.CONFIG.primaryLanguageHint}' unless the task is already in another language.`);
|
|
229
|
+
}
|
|
230
|
+
return descriptionBase.join("\n");
|
|
231
|
+
})(), {
|
|
210
232
|
task_id: zod_1.z.string().min(6).max(9).describe("The 6-9 character task ID to update"),
|
|
211
233
|
name: taskNameSchema.optional(),
|
|
212
234
|
append_description: zod_1.z.string().optional().describe("Optional markdown content to APPEND to existing task description (preserves existing content for safety)"),
|
|
@@ -301,13 +323,23 @@ function registerTaskToolsWrite(server, userData) {
|
|
|
301
323
|
};
|
|
302
324
|
}
|
|
303
325
|
});
|
|
304
|
-
server.tool("createTask",
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
326
|
+
server.tool("createTask", (() => {
|
|
327
|
+
const descriptionBase = [
|
|
328
|
+
"Creates a new task in a specific list and assigns it to specified users (defaults to current user).",
|
|
329
|
+
"CRITICAL LINKING REQUIREMENTS:",
|
|
330
|
+
"- ALWAYS search for similar existing tasks first using searchTasks to avoid duplicates",
|
|
331
|
+
"- Include links to related tasks in the description (format: https://app.clickup.com/t/TASK_ID)",
|
|
332
|
+
"- Reference parent/child tasks, dependencies, and related work with clickable links",
|
|
333
|
+
"- The response will include the new task's clickable URL - always share this link",
|
|
334
|
+
"Use getListInfo first to understand the list context and available statuses.",
|
|
335
|
+
"Task descriptions support full markdown formatting including **bold**, *italic*, lists, links, and code blocks.",
|
|
336
|
+
"BEST PRACTICE: Every task creation should result in sharing the clickable task URL for future reference."
|
|
337
|
+
];
|
|
338
|
+
if (config_1.CONFIG.primaryLanguageHint && config_1.CONFIG.primaryLanguageHint.toLowerCase() !== 'en') {
|
|
339
|
+
descriptionBase.splice(1, 0, `For optimal results, consider writing task names and descriptions in '${config_1.CONFIG.primaryLanguageHint}' unless specified otherwise or unless the context requires another language.`);
|
|
340
|
+
}
|
|
341
|
+
return descriptionBase.join("\n");
|
|
342
|
+
})(), {
|
|
311
343
|
list_id: zod_1.z.string().min(1).describe("The ID of the list where the task will be created"),
|
|
312
344
|
name: taskNameSchema,
|
|
313
345
|
description: zod_1.z.string().optional().describe("Optional markdown description for the task - supports full markdown formatting"),
|
|
@@ -524,6 +556,7 @@ async function generateTaskMetadata(task) {
|
|
|
524
556
|
}
|
|
525
557
|
const metadataLines = [
|
|
526
558
|
`task_id: ${task.id}`,
|
|
559
|
+
`task_url: ${task.url}`,
|
|
527
560
|
`name: ${task.name}`,
|
|
528
561
|
`status: ${task.status.status}`,
|
|
529
562
|
`date_created: ${timestampToIso(task.date_created)}`,
|
|
@@ -573,8 +606,6 @@ async function generateTaskMetadata(task) {
|
|
|
573
606
|
if (task.subtasks && task.subtasks.length > 0) {
|
|
574
607
|
metadataLines.push(`child_task_ids: ${task.subtasks.map((st) => st.id).join(', ')}`);
|
|
575
608
|
}
|
|
576
|
-
// Add task URL
|
|
577
|
-
metadataLines.push(`url: ${task.url}`);
|
|
578
609
|
// Add archived status if true
|
|
579
610
|
if (task.archived) {
|
|
580
611
|
metadataLines.push(`archived: true`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-tools.d.ts","sourceRoot":"","sources":["../../src/tools/time-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"time-tools.d.ts","sourceRoot":"","sources":["../../src/tools/time-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA2FpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QA6EtD;AAwLD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAmFvD"}
|
package/dist/tools/time-tools.js
CHANGED
|
@@ -29,12 +29,64 @@ function timestampToIso(timestamp) {
|
|
|
29
29
|
const timezoneOffset = sign + String(offsetHours).padStart(2, '0') + ':' + String(offsetMinutes).padStart(2, '0');
|
|
30
30
|
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${timezoneOffset}`;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Gets all team members from ClickUp API
|
|
34
|
+
*/
|
|
35
|
+
async function getAllTeamMembers() {
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(`https://api.clickup.com/api/v2/team`, {
|
|
38
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
console.error(`Error fetching teams: ${response.status} ${response.statusText}`);
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
if (!data.teams || !Array.isArray(data.teams)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
// Find the team that matches our configured team ID and extract all user IDs
|
|
49
|
+
const currentTeam = data.teams.find((team) => team.id === config_1.CONFIG.teamId);
|
|
50
|
+
if (!currentTeam || !currentTeam.members || !Array.isArray(currentTeam.members)) {
|
|
51
|
+
console.error(`Team ${config_1.CONFIG.teamId} not found or has no members`);
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
return currentTeam.members.map((member) => member.user?.id).filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('Error fetching team members:', error);
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Formats duration in milliseconds to human readable format
|
|
63
|
+
*/
|
|
64
|
+
function formatDuration(durationMs) {
|
|
65
|
+
const hours = durationMs / (1000 * 60 * 60);
|
|
66
|
+
const displayHours = Math.floor(hours);
|
|
67
|
+
const displayMinutes = Math.round((hours - displayHours) * 60);
|
|
68
|
+
return displayHours > 0 ? `${displayHours}h ${displayMinutes}m` : `${displayMinutes}m`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Formats timestamp to simple date and time for entry display
|
|
72
|
+
*/
|
|
73
|
+
function formatEntryTime(timestamp) {
|
|
74
|
+
const date = new Date(timestamp);
|
|
75
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
76
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
77
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
78
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
79
|
+
return `${date.getFullYear()}-${month}-${day} ${hours}:${minutes}`;
|
|
80
|
+
}
|
|
32
81
|
function registerTimeToolsRead(server) {
|
|
33
82
|
server.tool("getTimeEntries", "Gets time entries for a specific task or all user's time entries. Returns last 30 days by default if no dates specified.", {
|
|
34
83
|
task_id: zod_1.z.string().min(6).max(9).optional().describe("Optional 6-9 character task ID to filter entries. If not provided, returns all user's time entries."),
|
|
35
84
|
start_date: zod_1.z.string().optional().describe("Optional start date filter as ISO date string (e.g., '2024-10-06T00:00:00+02:00'). Defaults to 30 days ago."),
|
|
36
|
-
end_date: zod_1.z.string().optional().describe("Optional end date filter as ISO date string (e.g., '2024-10-06T23:59:59+02:00'). Defaults to current date.")
|
|
37
|
-
|
|
85
|
+
end_date: zod_1.z.string().optional().describe("Optional end date filter as ISO date string (e.g., '2024-10-06T23:59:59+02:00'). Defaults to current date."),
|
|
86
|
+
list_id: zod_1.z.string().optional().describe("Optional single list ID to filter time entries by a specific list"),
|
|
87
|
+
space_id: zod_1.z.string().optional().describe("Optional single space ID to filter time entries by a specific space"),
|
|
88
|
+
include_all_users: zod_1.z.boolean().optional().describe("Optional flag to include time entries from all team members (default: false, only current user)")
|
|
89
|
+
}, async ({ task_id, start_date, end_date, list_id, space_id, include_all_users }) => {
|
|
38
90
|
try {
|
|
39
91
|
// Build query parameters
|
|
40
92
|
const params = new URLSearchParams();
|
|
@@ -47,122 +99,37 @@ function registerTimeToolsRead(server) {
|
|
|
47
99
|
if (end_date) {
|
|
48
100
|
params.append('end_date', isoToTimestamp(end_date).toString());
|
|
49
101
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!response.ok) {
|
|
54
|
-
throw new Error(`Error fetching time entries: ${response.status} ${response.statusText}`);
|
|
102
|
+
// Add single list_id or space_id filter (not both)
|
|
103
|
+
if (list_id) {
|
|
104
|
+
params.append('list_id', list_id);
|
|
55
105
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const noEntriesMsg = task_id ?
|
|
59
|
-
`No time entries found for task ${task_id}.` :
|
|
60
|
-
'No time entries found for current user.';
|
|
61
|
-
return {
|
|
62
|
-
content: [{ type: "text", text: noEntriesMsg }],
|
|
63
|
-
};
|
|
106
|
+
else if (space_id) {
|
|
107
|
+
params.append('space_id', space_id);
|
|
64
108
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const entryDurationMs = parseInt(entry.duration) || 0;
|
|
75
|
-
timeByUser.set(username, currentTime + entryDurationMs);
|
|
76
|
-
if (!entriesByUser.has(username)) {
|
|
77
|
-
entriesByUser.set(username, []);
|
|
109
|
+
// Always include location names to get list information
|
|
110
|
+
params.append('include_location_names', 'true');
|
|
111
|
+
// Handle include_all_users by fetching all team members and adding them as assignees filter
|
|
112
|
+
// Note: This only works for Workspace Owners/Admins
|
|
113
|
+
if (include_all_users) {
|
|
114
|
+
try {
|
|
115
|
+
const teamMembers = await getAllTeamMembers();
|
|
116
|
+
if (teamMembers.length > 0) {
|
|
117
|
+
params.append('assignee', teamMembers.join(','));
|
|
78
118
|
}
|
|
79
|
-
entriesByUser.get(username).push(entry);
|
|
80
|
-
});
|
|
81
|
-
summaryLines = [`Time entries for task ${task_id}:`];
|
|
82
|
-
for (const [username, totalMs] of timeByUser.entries()) {
|
|
83
|
-
const hours = totalMs / (1000 * 60 * 60);
|
|
84
|
-
const displayHours = Math.floor(hours);
|
|
85
|
-
const displayMinutes = Math.round((hours - displayHours) * 60);
|
|
86
|
-
const timeDisplay = displayHours > 0 ?
|
|
87
|
-
`${displayHours}h ${displayMinutes}m` :
|
|
88
|
-
`${displayMinutes}m`;
|
|
89
|
-
summaryLines.push(` ${username}: ${timeDisplay}`);
|
|
90
|
-
totalTimeMs += totalMs;
|
|
91
119
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const timeByTask = new Map();
|
|
96
|
-
const entriesByTask = new Map();
|
|
97
|
-
data.data.forEach((entry) => {
|
|
98
|
-
const taskInfo = entry.task ?
|
|
99
|
-
`${entry.task.name} (${entry.task.id})` :
|
|
100
|
-
'No task';
|
|
101
|
-
const currentTime = timeByTask.get(taskInfo) || 0;
|
|
102
|
-
const entryDurationMs = parseInt(entry.duration) || 0;
|
|
103
|
-
timeByTask.set(taskInfo, currentTime + entryDurationMs);
|
|
104
|
-
if (!entriesByTask.has(taskInfo)) {
|
|
105
|
-
entriesByTask.set(taskInfo, []);
|
|
106
|
-
}
|
|
107
|
-
entriesByTask.get(taskInfo).push(entry);
|
|
108
|
-
});
|
|
109
|
-
summaryLines = ['Time entries for current user:'];
|
|
110
|
-
for (const [taskInfo, totalMs] of timeByTask.entries()) {
|
|
111
|
-
const hours = totalMs / (1000 * 60 * 60);
|
|
112
|
-
const displayHours = Math.floor(hours);
|
|
113
|
-
const displayMinutes = Math.round((hours - displayHours) * 60);
|
|
114
|
-
const timeDisplay = displayHours > 0 ?
|
|
115
|
-
`${displayHours}h ${displayMinutes}m` :
|
|
116
|
-
`${displayMinutes}m`;
|
|
117
|
-
summaryLines.push(` ${taskInfo}: ${timeDisplay}`);
|
|
118
|
-
totalTimeMs += totalMs;
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.error('Warning: Could not fetch all team members. This feature requires Workspace Owner/Admin permissions.');
|
|
122
|
+
// Continue without all users - will only show current user's entries
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const totalDisplayMinutes = Math.round((totalHours - totalDisplayHours) * 60);
|
|
124
|
-
const totalDisplay = totalDisplayHours > 0 ?
|
|
125
|
-
`${totalDisplayHours}h ${totalDisplayMinutes}m` :
|
|
126
|
-
`${totalDisplayMinutes}m`;
|
|
127
|
-
summaryLines.push(`Total: ${totalDisplay}`);
|
|
128
|
-
// Create detailed entries
|
|
129
|
-
const detailBlocks = [];
|
|
130
|
-
data.data.forEach((entry) => {
|
|
131
|
-
const entryHours = (parseInt(entry.duration) || 0) / (1000 * 60 * 60);
|
|
132
|
-
const entryDisplayHours = Math.floor(entryHours);
|
|
133
|
-
const entryDisplayMinutes = Math.round((entryHours - entryDisplayHours) * 60);
|
|
134
|
-
const entryTimeDisplay = entryDisplayHours > 0 ?
|
|
135
|
-
`${entryDisplayHours}h ${entryDisplayMinutes}m` :
|
|
136
|
-
`${entryDisplayMinutes}m`;
|
|
137
|
-
const entryInfo = [
|
|
138
|
-
`entry_id: ${entry.id}`,
|
|
139
|
-
`user: ${entry.user?.username || 'Unknown User'}`,
|
|
140
|
-
`duration: ${entryTimeDisplay}`,
|
|
141
|
-
`start: ${timestampToIso(parseInt(entry.start))}`,
|
|
142
|
-
...(entry.description ? [`description: ${entry.description}`] : []),
|
|
143
|
-
`billable: ${entry.billable || false}`
|
|
144
|
-
];
|
|
145
|
-
// Add task info only when showing all user entries (no task filter)
|
|
146
|
-
if (!task_id && entry.task) {
|
|
147
|
-
entryInfo.splice(2, 0, `task: ${entry.task.name} (${entry.task.id})`);
|
|
148
|
-
}
|
|
149
|
-
detailBlocks.push({
|
|
150
|
-
type: "text",
|
|
151
|
-
text: entryInfo.join('\n')
|
|
152
|
-
});
|
|
125
|
+
const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries?${params}`, {
|
|
126
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
153
127
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
},
|
|
160
|
-
...(detailBlocks.length > 0 ? [
|
|
161
|
-
{ type: "text", text: "\nDetailed entries:" },
|
|
162
|
-
...detailBlocks
|
|
163
|
-
] : [])
|
|
164
|
-
],
|
|
165
|
-
};
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(`Error fetching time entries: ${response.status} ${response.statusText}`);
|
|
130
|
+
}
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
return processTimeEntriesData(data, task_id, start_date, end_date, include_all_users);
|
|
166
133
|
}
|
|
167
134
|
catch (error) {
|
|
168
135
|
console.error('Error fetching time entries:', error);
|
|
@@ -177,6 +144,147 @@ function registerTimeToolsRead(server) {
|
|
|
177
144
|
}
|
|
178
145
|
});
|
|
179
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Process the time entries data and return formatted hierarchical output
|
|
149
|
+
*/
|
|
150
|
+
function processTimeEntriesData(data, task_id, start_date, end_date, include_all_users) {
|
|
151
|
+
if (!data.data || !Array.isArray(data.data)) {
|
|
152
|
+
const noEntriesMsg = task_id ?
|
|
153
|
+
`No time entries found for task ${task_id}.` :
|
|
154
|
+
'No time entries found.';
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text", text: noEntriesMsg }],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const filteredEntries = data.data;
|
|
160
|
+
// Create hierarchical structure: List → Task → User → Individual entries
|
|
161
|
+
const hierarchy = new Map();
|
|
162
|
+
let totalTimeMs = 0;
|
|
163
|
+
filteredEntries.forEach((entry) => {
|
|
164
|
+
const taskId = entry.task?.id || 'no-task';
|
|
165
|
+
// Use location names from include_location_names parameter
|
|
166
|
+
const listId = entry.task_location?.list_id || 'no-list';
|
|
167
|
+
const listName = entry.task_location?.list_name || 'No List';
|
|
168
|
+
const taskName = entry.task?.name || 'No Task';
|
|
169
|
+
const userId = entry.user?.id || 'no-user';
|
|
170
|
+
const userName = entry.user?.username || 'Unknown User';
|
|
171
|
+
// Handle running timers (negative duration)
|
|
172
|
+
let entryDurationMs = parseInt(entry.duration) || 0;
|
|
173
|
+
const isRunningTimer = entryDurationMs < 0;
|
|
174
|
+
if (isRunningTimer) {
|
|
175
|
+
// For running timers, calculate current duration from start time
|
|
176
|
+
entryDurationMs = Date.now() - parseInt(entry.start);
|
|
177
|
+
}
|
|
178
|
+
totalTimeMs += entryDurationMs;
|
|
179
|
+
// Initialize list level
|
|
180
|
+
if (!hierarchy.has(listId)) {
|
|
181
|
+
hierarchy.set(listId, {
|
|
182
|
+
name: listName,
|
|
183
|
+
id: listId,
|
|
184
|
+
totalTime: 0,
|
|
185
|
+
tasks: new Map()
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const listData = hierarchy.get(listId);
|
|
189
|
+
listData.totalTime += entryDurationMs;
|
|
190
|
+
// Initialize task level
|
|
191
|
+
if (!listData.tasks.has(taskId)) {
|
|
192
|
+
listData.tasks.set(taskId, {
|
|
193
|
+
name: taskName,
|
|
194
|
+
id: taskId,
|
|
195
|
+
totalTime: 0,
|
|
196
|
+
users: new Map()
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const taskData = listData.tasks.get(taskId);
|
|
200
|
+
taskData.totalTime += entryDurationMs;
|
|
201
|
+
// Initialize user level
|
|
202
|
+
if (!taskData.users.has(userId)) {
|
|
203
|
+
taskData.users.set(userId, {
|
|
204
|
+
name: userName,
|
|
205
|
+
id: userId,
|
|
206
|
+
totalTime: 0,
|
|
207
|
+
entries: []
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const userData = taskData.users.get(userId);
|
|
211
|
+
userData.totalTime += entryDurationMs;
|
|
212
|
+
userData.entries.push(entry);
|
|
213
|
+
});
|
|
214
|
+
// Count total tasks across all lists
|
|
215
|
+
let totalTasks = 0;
|
|
216
|
+
for (const [listId, listData] of hierarchy.entries()) {
|
|
217
|
+
totalTasks += listData.tasks.size;
|
|
218
|
+
}
|
|
219
|
+
// Format the hierarchical output
|
|
220
|
+
const outputLines = [];
|
|
221
|
+
// Header with date range and total
|
|
222
|
+
const dateRange = start_date && end_date ?
|
|
223
|
+
` (${start_date.split('T')[0]} to ${end_date.split('T')[0]})` :
|
|
224
|
+
start_date ? ` (from ${start_date.split('T')[0]})` :
|
|
225
|
+
end_date ? ` (until ${end_date.split('T')[0]})` : '';
|
|
226
|
+
outputLines.push(`Time Entries Summary${dateRange}`);
|
|
227
|
+
outputLines.push(`Total: ${formatDuration(totalTimeMs)}`);
|
|
228
|
+
outputLines.push('');
|
|
229
|
+
// Check if result is too large (>100 tasks)
|
|
230
|
+
const TASK_LIMIT = 100;
|
|
231
|
+
const isTruncated = totalTasks > TASK_LIMIT;
|
|
232
|
+
if (isTruncated) {
|
|
233
|
+
// Show only list-level summary
|
|
234
|
+
outputLines.push(`⚠️ Large result detected (${totalTasks} tasks). Showing summary only.`);
|
|
235
|
+
outputLines.push(`💡 Use list_id, space_id, or date filters for detailed view.`);
|
|
236
|
+
outputLines.push('');
|
|
237
|
+
for (const [listId, listData] of hierarchy.entries()) {
|
|
238
|
+
const taskCount = listData.tasks.size;
|
|
239
|
+
outputLines.push(`📋 ${listData.name} (List: ${listId}) - ${formatDuration(listData.totalTime)} across ${taskCount} task${taskCount === 1 ? '' : 's'}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Show full hierarchical display
|
|
244
|
+
for (const [listId, listData] of hierarchy.entries()) {
|
|
245
|
+
outputLines.push(`📋 ${listData.name} (List: ${listId}) - ${formatDuration(listData.totalTime)}`);
|
|
246
|
+
for (const [taskId, taskData] of listData.tasks.entries()) {
|
|
247
|
+
outputLines.push(` ├─ 🎯 ${taskData.name} (Task: ${taskId}) - ${formatDuration(taskData.totalTime)}`);
|
|
248
|
+
const userEntries = Array.from(taskData.users.entries());
|
|
249
|
+
for (let userIndex = 0; userIndex < userEntries.length; userIndex++) {
|
|
250
|
+
const [userId, userData] = userEntries[userIndex];
|
|
251
|
+
const isLastUser = userIndex === userEntries.length - 1;
|
|
252
|
+
const userPrefix = isLastUser ? ' └─' : ' ├─';
|
|
253
|
+
outputLines.push(`${userPrefix} ${userData.name}: ${formatDuration(userData.totalTime)}`);
|
|
254
|
+
// Add individual entries
|
|
255
|
+
userData.entries.forEach((entry, entryIndex) => {
|
|
256
|
+
const isLastEntry = entryIndex === userData.entries.length - 1;
|
|
257
|
+
const entryPrefix = isLastUser ?
|
|
258
|
+
(isLastEntry ? ' └─' : ' ├─') :
|
|
259
|
+
(isLastEntry ? ' │ └─' : ' │ ├─');
|
|
260
|
+
const entryStart = formatEntryTime(parseInt(entry.start));
|
|
261
|
+
// Handle running timers
|
|
262
|
+
const rawDuration = parseInt(entry.duration) || 0;
|
|
263
|
+
const isRunningTimer = rawDuration < 0;
|
|
264
|
+
let entryDuration;
|
|
265
|
+
if (isRunningTimer) {
|
|
266
|
+
const currentDuration = Date.now() - parseInt(entry.start);
|
|
267
|
+
entryDuration = `${formatDuration(currentDuration)} (running)`;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
entryDuration = formatDuration(rawDuration);
|
|
271
|
+
}
|
|
272
|
+
outputLines.push(`${entryPrefix} ${entryStart} - ${entryDuration}`);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
outputLines.push('');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "text",
|
|
283
|
+
text: outputLines.join('\n')
|
|
284
|
+
}
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
180
288
|
function registerTimeToolsWrite(server) {
|
|
181
289
|
server.tool("createTimeEntry", [
|
|
182
290
|
"Creates a time entry (books time) on a task for the current user.",
|
package/package.json
CHANGED