@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.
@@ -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"}
@@ -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;AAKpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QA4OtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAuEvD"}
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"}
@@ -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", "Lists all lists and folders in a space. They might also be referred to as boards or tables. Shows both direct lists (folderless) and folders containing lists. If folder_id is provided, lists only the lists within that specific folder.", {
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: ${list.private || false}`,
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: ${folder.private || false}`,
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: ${list.private || false}`,
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,QAsMpD"}
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;AAKpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,QAiEnD"}
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", "Lists all spaces in the workspace. These might also be referred to as customers or projects.", {
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: ${space.private || false}`,
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,QAoDrE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAwQtE;AAoLD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,YAAY,CAAC,CA6G3E"}
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"}
@@ -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", "Get a Clickup task with images and comments by ID", {
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
- "Adds a comment to a specific task.",
150
- "BEST PRACTICES: Include progress information and current status updates.",
151
- "If external links are provided, verify they are publicly accessible and incorporate relevant information from those links into the comment.",
152
- "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."
153
- ].join("\n"), {
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
- "Updates various aspects of an existing task.",
205
- "Use getListInfo first to see valid status options.",
206
- "SAFETY FEATURE: Description updates are APPEND-ONLY to prevent data loss - existing content is preserved.",
207
- "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.",
208
- "Suggest appropriate status transitions."
209
- ].join("\n"), {
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
- "Creates a new task in a specific list and assigns it to specified users (defaults to current user).",
306
- "IMPORTANT: Before creating, always search for similar existing tasks first using searchTasks to avoid duplicates - ask the user if they want to use an existing task instead.",
307
- "If related tasks exist, reference them with links in the description (format: https://app.clickup.com/t/TASK_ID).",
308
- "Use getListInfo first to understand the list context and available statuses.",
309
- "Task descriptions support full markdown formatting including **bold**, *italic*, lists, links, and code blocks."
310
- ].join("\n"), {
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;AAmCpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAmLtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAmFvD"}
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"}
@@ -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
- }, async ({ task_id, start_date, end_date }) => {
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
- const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries?${params}`, {
51
- headers: { Authorization: config_1.CONFIG.apiKey },
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
- const data = await response.json();
57
- if (!data.data || !Array.isArray(data.data)) {
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
- let summaryLines = [];
66
- let totalTimeMs = 0;
67
- if (task_id) {
68
- // Task-specific view: Group by user
69
- const timeByUser = new Map();
70
- const entriesByUser = new Map();
71
- data.data.forEach((entry) => {
72
- const username = entry.user?.username || 'Unknown User';
73
- const currentTime = timeByUser.get(username) || 0;
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
- else {
94
- // All user entries: Group by task
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 totalHours = totalTimeMs / (1000 * 60 * 60);
122
- const totalDisplayHours = Math.floor(totalHours);
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
- return {
155
- content: [
156
- {
157
- type: "text",
158
- text: summaryLines.join('\n')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hauptsache.net/clickup-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "A minimal implementation of a Model Context Protocol (MCP) server for ClickUp integration",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",