@taazkareem/clickup-mcp-server 0.6.9 → 0.7.0
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 +73 -51
- package/build/config.js +17 -1
- package/build/server.js +65 -6
- package/build/services/clickup/document.js +159 -0
- package/build/services/clickup/index.js +11 -1
- package/build/services/clickup/task/task-core.js +14 -2
- package/build/services/clickup/time.js +244 -0
- package/build/services/clickup/types.js +11 -0
- package/build/services/shared.js +1 -1
- package/build/tools/documents.js +501 -0
- package/build/tools/task/bulk-operations.js +2 -1
- package/build/tools/task/index.js +2 -0
- package/build/tools/task/single-operations.js +2 -1
- package/build/tools/task/time-tracking.js +684 -0
- package/build/utils/sponsor-service.js +1 -1
- package/package.json +1 -1
- package/build/mcp-tools.js +0 -64
- package/build/server-state.js +0 -93
- package/build/server.log +0 -76
- package/build/services/clickup/task/handlers.js +0 -1
- package/build/services/clickup/task.js +0 -976
- package/build/services/clickup/tools/tag.js +0 -149
- package/build/tools/bulk-tasks.js +0 -36
- package/build/tools/debug.js +0 -76
- package/build/tools/logs.js +0 -55
- package/build/tools/task.js +0 -1554
- package/build/utils/params-utils.js +0 -39
- package/build/utils/sponsor-analytics.js +0 -100
- package/build/utils/sponsor-utils.js +0 -57
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Task time tracking tools
|
|
6
|
+
*
|
|
7
|
+
* This module provides tools for time tracking operations on ClickUp tasks:
|
|
8
|
+
* - Get time entries for a task
|
|
9
|
+
* - Start time tracking on a task
|
|
10
|
+
* - Stop time tracking
|
|
11
|
+
* - Add a manual time entry
|
|
12
|
+
* - Delete a time entry
|
|
13
|
+
*/
|
|
14
|
+
import { timeTrackingService } from "../../services/shared.js";
|
|
15
|
+
import { getTaskId } from "./utilities.js";
|
|
16
|
+
import { Logger } from "../../logger.js";
|
|
17
|
+
import { parseDueDate } from "../../utils/date-utils.js";
|
|
18
|
+
// Logger instance
|
|
19
|
+
const logger = new Logger('TimeTrackingTools');
|
|
20
|
+
/**
|
|
21
|
+
* Tool definition for getting time entries
|
|
22
|
+
*/
|
|
23
|
+
export const getTaskTimeEntriesTool = {
|
|
24
|
+
name: "get_task_time_entries",
|
|
25
|
+
description: "Gets all time entries for a task with filtering options. Use taskId (preferred) or taskName + optional listName. Returns all tracked time with user info, descriptions, tags, start/end times, and durations.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
taskId: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "ID of the task to get time entries for. Works with both regular task IDs and custom IDs."
|
|
32
|
+
},
|
|
33
|
+
taskName: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Name of the task to get time entries for. When using this parameter, it's recommended to also provide listName."
|
|
36
|
+
},
|
|
37
|
+
listName: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "Name of the list containing the task. Helps find the right task when using taskName."
|
|
40
|
+
},
|
|
41
|
+
startDate: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Optional start date filter. Supports Unix timestamps (in milliseconds) and natural language expressions like 'yesterday', 'last week', etc."
|
|
44
|
+
},
|
|
45
|
+
endDate: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Optional end date filter. Supports Unix timestamps (in milliseconds) and natural language expressions."
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Tool definition for starting time tracking
|
|
54
|
+
*/
|
|
55
|
+
export const startTimeTrackingTool = {
|
|
56
|
+
name: "start_time_tracking",
|
|
57
|
+
description: "Starts time tracking on a task. Use taskId (preferred) or taskName + optional listName. Optional fields: description, billable status, and tags. Only one timer can be running at a time.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
taskId: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "ID of the task to start tracking time on. Works with both regular task IDs and custom IDs."
|
|
64
|
+
},
|
|
65
|
+
taskName: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "Name of the task to start tracking time on. When using this parameter, it's recommended to also provide listName."
|
|
68
|
+
},
|
|
69
|
+
listName: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Name of the list containing the task. Helps find the right task when using taskName."
|
|
72
|
+
},
|
|
73
|
+
description: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Optional description for the time entry."
|
|
76
|
+
},
|
|
77
|
+
billable: {
|
|
78
|
+
type: "boolean",
|
|
79
|
+
description: "Whether this time is billable. Default is workspace setting."
|
|
80
|
+
},
|
|
81
|
+
tags: {
|
|
82
|
+
type: "array",
|
|
83
|
+
items: {
|
|
84
|
+
type: "string"
|
|
85
|
+
},
|
|
86
|
+
description: "Optional array of tag names to assign to the time entry."
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Tool definition for stopping time tracking
|
|
93
|
+
*/
|
|
94
|
+
export const stopTimeTrackingTool = {
|
|
95
|
+
name: "stop_time_tracking",
|
|
96
|
+
description: "Stops the currently running time tracker. Optional fields: description and tags. Returns the completed time entry details.",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
description: {
|
|
101
|
+
type: "string",
|
|
102
|
+
description: "Optional description to update or add to the time entry."
|
|
103
|
+
},
|
|
104
|
+
tags: {
|
|
105
|
+
type: "array",
|
|
106
|
+
items: {
|
|
107
|
+
type: "string"
|
|
108
|
+
},
|
|
109
|
+
description: "Optional array of tag names to assign to the time entry."
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Tool definition for adding a manual time entry
|
|
116
|
+
*/
|
|
117
|
+
export const addTimeEntryTool = {
|
|
118
|
+
name: "add_time_entry",
|
|
119
|
+
description: "Adds a manual time entry to a task. Use taskId (preferred) or taskName + optional listName. Required: start time, duration. Optional: description, billable, tags.",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
taskId: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "ID of the task to add time entry to. Works with both regular task IDs and custom IDs."
|
|
126
|
+
},
|
|
127
|
+
taskName: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "Name of the task to add time entry to. When using this parameter, it's recommended to also provide listName."
|
|
130
|
+
},
|
|
131
|
+
listName: {
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "Name of the list containing the task. Helps find the right task when using taskName."
|
|
134
|
+
},
|
|
135
|
+
start: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "Start time for the entry. Supports Unix timestamps (in milliseconds) and natural language expressions like '2 hours ago', 'yesterday 9am', etc."
|
|
138
|
+
},
|
|
139
|
+
duration: {
|
|
140
|
+
type: "string",
|
|
141
|
+
description: "Duration of the time entry. Format as 'Xh Ym' (e.g., '1h 30m') or just minutes (e.g., '90m')."
|
|
142
|
+
},
|
|
143
|
+
description: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "Optional description for the time entry."
|
|
146
|
+
},
|
|
147
|
+
billable: {
|
|
148
|
+
type: "boolean",
|
|
149
|
+
description: "Whether this time is billable. Default is workspace setting."
|
|
150
|
+
},
|
|
151
|
+
tags: {
|
|
152
|
+
type: "array",
|
|
153
|
+
items: {
|
|
154
|
+
type: "string"
|
|
155
|
+
},
|
|
156
|
+
description: "Optional array of tag names to assign to the time entry."
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
required: ["start", "duration"]
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Tool definition for deleting a time entry
|
|
164
|
+
*/
|
|
165
|
+
export const deleteTimeEntryTool = {
|
|
166
|
+
name: "delete_time_entry",
|
|
167
|
+
description: "Deletes a time entry. Required: time entry ID.",
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
timeEntryId: {
|
|
172
|
+
type: "string",
|
|
173
|
+
description: "ID of the time entry to delete."
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
required: ["timeEntryId"]
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Tool definition for getting current time entry
|
|
181
|
+
*/
|
|
182
|
+
export const getCurrentTimeEntryTool = {
|
|
183
|
+
name: "get_current_time_entry",
|
|
184
|
+
description: "Gets the currently running time entry, if any. No parameters needed.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Handle get task time entries tool
|
|
192
|
+
*/
|
|
193
|
+
export async function handleGetTaskTimeEntries(params) {
|
|
194
|
+
logger.info("Handling request to get task time entries", params);
|
|
195
|
+
try {
|
|
196
|
+
// Resolve task ID
|
|
197
|
+
const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
|
|
198
|
+
if (!taskId) {
|
|
199
|
+
return {
|
|
200
|
+
success: false,
|
|
201
|
+
error: {
|
|
202
|
+
message: "Task not found. Please provide a valid taskId or taskName + listName combination."
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Parse date filters
|
|
207
|
+
let startDate;
|
|
208
|
+
let endDate;
|
|
209
|
+
if (params.startDate) {
|
|
210
|
+
startDate = parseDueDate(params.startDate);
|
|
211
|
+
}
|
|
212
|
+
if (params.endDate) {
|
|
213
|
+
endDate = parseDueDate(params.endDate);
|
|
214
|
+
}
|
|
215
|
+
// Get time entries
|
|
216
|
+
const result = await timeTrackingService.getTimeEntries(taskId, startDate, endDate);
|
|
217
|
+
if (!result.success) {
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
error: {
|
|
221
|
+
message: result.error?.message || "Failed to get time entries"
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const timeEntries = result.data || [];
|
|
226
|
+
// Format the response
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
time_entries: timeEntries.map(entry => ({
|
|
230
|
+
id: entry.id,
|
|
231
|
+
description: entry.description,
|
|
232
|
+
start: entry.start,
|
|
233
|
+
end: entry.end,
|
|
234
|
+
duration: formatDuration(entry.duration),
|
|
235
|
+
duration_ms: entry.duration,
|
|
236
|
+
billable: entry.billable,
|
|
237
|
+
tags: entry.tags,
|
|
238
|
+
user: {
|
|
239
|
+
id: entry.user.id,
|
|
240
|
+
username: entry.user.username
|
|
241
|
+
},
|
|
242
|
+
task: {
|
|
243
|
+
id: entry.task.id,
|
|
244
|
+
name: entry.task.name,
|
|
245
|
+
status: entry.task.status.status
|
|
246
|
+
}
|
|
247
|
+
}))
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
logger.error("Error getting task time entries", error);
|
|
252
|
+
return {
|
|
253
|
+
success: false,
|
|
254
|
+
error: {
|
|
255
|
+
message: error.message || "An unknown error occurred"
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Handle start time tracking tool
|
|
262
|
+
*/
|
|
263
|
+
export async function handleStartTimeTracking(params) {
|
|
264
|
+
logger.info("Handling request to start time tracking", params);
|
|
265
|
+
try {
|
|
266
|
+
// Resolve task ID
|
|
267
|
+
const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
|
|
268
|
+
if (!taskId) {
|
|
269
|
+
return {
|
|
270
|
+
success: false,
|
|
271
|
+
error: {
|
|
272
|
+
message: "Task not found. Please provide a valid taskId or taskName + listName combination."
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// Check for currently running timer
|
|
277
|
+
const currentTimerResult = await timeTrackingService.getCurrentTimeEntry();
|
|
278
|
+
if (currentTimerResult.success && currentTimerResult.data) {
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
error: {
|
|
282
|
+
message: "A timer is already running. Please stop the current timer before starting a new one.",
|
|
283
|
+
timer: {
|
|
284
|
+
id: currentTimerResult.data.id,
|
|
285
|
+
task: {
|
|
286
|
+
id: currentTimerResult.data.task.id,
|
|
287
|
+
name: currentTimerResult.data.task.name
|
|
288
|
+
},
|
|
289
|
+
start: currentTimerResult.data.start,
|
|
290
|
+
description: currentTimerResult.data.description
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
// Prepare request data
|
|
296
|
+
const requestData = {
|
|
297
|
+
tid: taskId,
|
|
298
|
+
description: params.description,
|
|
299
|
+
billable: params.billable,
|
|
300
|
+
tags: params.tags
|
|
301
|
+
};
|
|
302
|
+
// Start time tracking
|
|
303
|
+
const result = await timeTrackingService.startTimeTracking(requestData);
|
|
304
|
+
if (!result.success) {
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
error: {
|
|
308
|
+
message: result.error?.message || "Failed to start time tracking"
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const timeEntry = result.data;
|
|
313
|
+
if (!timeEntry) {
|
|
314
|
+
return {
|
|
315
|
+
success: false,
|
|
316
|
+
error: {
|
|
317
|
+
message: "No time entry data returned from API"
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
// Format the response
|
|
322
|
+
return {
|
|
323
|
+
success: true,
|
|
324
|
+
time_entry: {
|
|
325
|
+
id: timeEntry.id,
|
|
326
|
+
description: timeEntry.description,
|
|
327
|
+
start: timeEntry.start,
|
|
328
|
+
end: timeEntry.end,
|
|
329
|
+
task: {
|
|
330
|
+
id: timeEntry.task.id,
|
|
331
|
+
name: timeEntry.task.name
|
|
332
|
+
},
|
|
333
|
+
billable: timeEntry.billable,
|
|
334
|
+
tags: timeEntry.tags
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
logger.error("Error starting time tracking", error);
|
|
340
|
+
return {
|
|
341
|
+
success: false,
|
|
342
|
+
error: {
|
|
343
|
+
message: error.message || "An unknown error occurred"
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Handle stop time tracking tool
|
|
350
|
+
*/
|
|
351
|
+
export async function handleStopTimeTracking(params) {
|
|
352
|
+
logger.info("Handling request to stop time tracking", params);
|
|
353
|
+
try {
|
|
354
|
+
// Check for currently running timer
|
|
355
|
+
const currentTimerResult = await timeTrackingService.getCurrentTimeEntry();
|
|
356
|
+
if (currentTimerResult.success && !currentTimerResult.data) {
|
|
357
|
+
return {
|
|
358
|
+
success: false,
|
|
359
|
+
error: {
|
|
360
|
+
message: "No timer is currently running. Start a timer before trying to stop it."
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
// Prepare request data
|
|
365
|
+
const requestData = {
|
|
366
|
+
description: params.description,
|
|
367
|
+
tags: params.tags
|
|
368
|
+
};
|
|
369
|
+
// Stop time tracking
|
|
370
|
+
const result = await timeTrackingService.stopTimeTracking(requestData);
|
|
371
|
+
if (!result.success) {
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
error: {
|
|
375
|
+
message: result.error?.message || "Failed to stop time tracking"
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const timeEntry = result.data;
|
|
380
|
+
if (!timeEntry) {
|
|
381
|
+
return {
|
|
382
|
+
success: false,
|
|
383
|
+
error: {
|
|
384
|
+
message: "No time entry data returned from API"
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
// Format the response
|
|
389
|
+
return {
|
|
390
|
+
success: true,
|
|
391
|
+
time_entry: {
|
|
392
|
+
id: timeEntry.id,
|
|
393
|
+
description: timeEntry.description,
|
|
394
|
+
start: timeEntry.start,
|
|
395
|
+
end: timeEntry.end,
|
|
396
|
+
duration: formatDuration(timeEntry.duration),
|
|
397
|
+
duration_ms: timeEntry.duration,
|
|
398
|
+
task: {
|
|
399
|
+
id: timeEntry.task.id,
|
|
400
|
+
name: timeEntry.task.name
|
|
401
|
+
},
|
|
402
|
+
billable: timeEntry.billable,
|
|
403
|
+
tags: timeEntry.tags
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
logger.error("Error stopping time tracking", error);
|
|
409
|
+
return {
|
|
410
|
+
success: false,
|
|
411
|
+
error: {
|
|
412
|
+
message: error.message || "An unknown error occurred"
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Handle add time entry tool
|
|
419
|
+
*/
|
|
420
|
+
export async function handleAddTimeEntry(params) {
|
|
421
|
+
logger.info("Handling request to add time entry", params);
|
|
422
|
+
try {
|
|
423
|
+
// Resolve task ID
|
|
424
|
+
const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
|
|
425
|
+
if (!taskId) {
|
|
426
|
+
return {
|
|
427
|
+
success: false,
|
|
428
|
+
error: {
|
|
429
|
+
message: "Task not found. Please provide a valid taskId or taskName + listName combination."
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// Parse start time
|
|
434
|
+
const startTime = parseDueDate(params.start);
|
|
435
|
+
if (!startTime) {
|
|
436
|
+
return {
|
|
437
|
+
success: false,
|
|
438
|
+
error: {
|
|
439
|
+
message: "Invalid start time format. Use a Unix timestamp (in milliseconds) or a natural language date string."
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
// Parse duration
|
|
444
|
+
const durationMs = parseDuration(params.duration);
|
|
445
|
+
if (durationMs === 0) {
|
|
446
|
+
return {
|
|
447
|
+
success: false,
|
|
448
|
+
error: {
|
|
449
|
+
message: "Invalid duration format. Use 'Xh Ym' format (e.g., '1h 30m') or just minutes (e.g., '90m')."
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
// Prepare request data
|
|
454
|
+
const requestData = {
|
|
455
|
+
tid: taskId,
|
|
456
|
+
start: startTime,
|
|
457
|
+
duration: durationMs,
|
|
458
|
+
description: params.description,
|
|
459
|
+
billable: params.billable,
|
|
460
|
+
tags: params.tags
|
|
461
|
+
};
|
|
462
|
+
// Add time entry
|
|
463
|
+
const result = await timeTrackingService.addTimeEntry(requestData);
|
|
464
|
+
if (!result.success) {
|
|
465
|
+
return {
|
|
466
|
+
success: false,
|
|
467
|
+
error: {
|
|
468
|
+
message: result.error?.message || "Failed to add time entry"
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const timeEntry = result.data;
|
|
473
|
+
if (!timeEntry) {
|
|
474
|
+
return {
|
|
475
|
+
success: false,
|
|
476
|
+
error: {
|
|
477
|
+
message: "No time entry data returned from API"
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
// Format the response
|
|
482
|
+
return {
|
|
483
|
+
success: true,
|
|
484
|
+
time_entry: {
|
|
485
|
+
id: timeEntry.id,
|
|
486
|
+
description: timeEntry.description,
|
|
487
|
+
start: timeEntry.start,
|
|
488
|
+
end: timeEntry.end,
|
|
489
|
+
duration: formatDuration(timeEntry.duration),
|
|
490
|
+
duration_ms: timeEntry.duration,
|
|
491
|
+
task: {
|
|
492
|
+
id: timeEntry.task.id,
|
|
493
|
+
name: timeEntry.task.name
|
|
494
|
+
},
|
|
495
|
+
billable: timeEntry.billable,
|
|
496
|
+
tags: timeEntry.tags
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
logger.error("Error adding time entry", error);
|
|
502
|
+
return {
|
|
503
|
+
success: false,
|
|
504
|
+
error: {
|
|
505
|
+
message: error.message || "An unknown error occurred"
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Handle delete time entry tool
|
|
512
|
+
*/
|
|
513
|
+
export async function handleDeleteTimeEntry(params) {
|
|
514
|
+
logger.info("Handling request to delete time entry", params);
|
|
515
|
+
try {
|
|
516
|
+
const { timeEntryId } = params;
|
|
517
|
+
if (!timeEntryId) {
|
|
518
|
+
return {
|
|
519
|
+
success: false,
|
|
520
|
+
error: {
|
|
521
|
+
message: "Time entry ID is required."
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
// Delete time entry
|
|
526
|
+
const result = await timeTrackingService.deleteTimeEntry(timeEntryId);
|
|
527
|
+
if (!result.success) {
|
|
528
|
+
return {
|
|
529
|
+
success: false,
|
|
530
|
+
error: {
|
|
531
|
+
message: result.error?.message || "Failed to delete time entry"
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
// Format the response
|
|
536
|
+
return {
|
|
537
|
+
success: true,
|
|
538
|
+
message: "Time entry deleted successfully."
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
logger.error("Error deleting time entry", error);
|
|
543
|
+
return {
|
|
544
|
+
success: false,
|
|
545
|
+
error: {
|
|
546
|
+
message: error.message || "An unknown error occurred"
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Handle get current time entry tool
|
|
553
|
+
*/
|
|
554
|
+
export async function handleGetCurrentTimeEntry(params) {
|
|
555
|
+
logger.info("Handling request to get current time entry");
|
|
556
|
+
try {
|
|
557
|
+
// Get current time entry
|
|
558
|
+
const result = await timeTrackingService.getCurrentTimeEntry();
|
|
559
|
+
if (!result.success) {
|
|
560
|
+
return {
|
|
561
|
+
success: false,
|
|
562
|
+
error: {
|
|
563
|
+
message: result.error?.message || "Failed to get current time entry"
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const timeEntry = result.data;
|
|
568
|
+
// If no timer is running
|
|
569
|
+
if (!timeEntry) {
|
|
570
|
+
return {
|
|
571
|
+
success: true,
|
|
572
|
+
timer_running: false,
|
|
573
|
+
message: "No timer is currently running."
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
// Format the response
|
|
577
|
+
const elapsedTime = calculateElapsedTime(timeEntry.start);
|
|
578
|
+
return {
|
|
579
|
+
success: true,
|
|
580
|
+
timer_running: true,
|
|
581
|
+
time_entry: {
|
|
582
|
+
id: timeEntry.id,
|
|
583
|
+
description: timeEntry.description,
|
|
584
|
+
start: timeEntry.start,
|
|
585
|
+
elapsed: formatDuration(elapsedTime),
|
|
586
|
+
elapsed_ms: elapsedTime,
|
|
587
|
+
task: {
|
|
588
|
+
id: timeEntry.task.id,
|
|
589
|
+
name: timeEntry.task.name
|
|
590
|
+
},
|
|
591
|
+
billable: timeEntry.billable,
|
|
592
|
+
tags: timeEntry.tags
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
logger.error("Error getting current time entry", error);
|
|
598
|
+
return {
|
|
599
|
+
success: false,
|
|
600
|
+
error: {
|
|
601
|
+
message: error.message || "An unknown error occurred"
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Calculate elapsed time in milliseconds from a start time string to now
|
|
608
|
+
*/
|
|
609
|
+
function calculateElapsedTime(startTimeString) {
|
|
610
|
+
const startTime = new Date(startTimeString).getTime();
|
|
611
|
+
const now = Date.now();
|
|
612
|
+
return Math.max(0, now - startTime);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Format duration in milliseconds to a human-readable string
|
|
616
|
+
*/
|
|
617
|
+
function formatDuration(durationMs) {
|
|
618
|
+
if (!durationMs)
|
|
619
|
+
return "0m";
|
|
620
|
+
const seconds = Math.floor(durationMs / 1000);
|
|
621
|
+
const minutes = Math.floor(seconds / 60);
|
|
622
|
+
const hours = Math.floor(minutes / 60);
|
|
623
|
+
const remainingMinutes = minutes % 60;
|
|
624
|
+
if (hours === 0) {
|
|
625
|
+
return `${remainingMinutes}m`;
|
|
626
|
+
}
|
|
627
|
+
else if (remainingMinutes === 0) {
|
|
628
|
+
return `${hours}h`;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Parse duration string to milliseconds
|
|
636
|
+
*/
|
|
637
|
+
function parseDuration(durationString) {
|
|
638
|
+
if (!durationString)
|
|
639
|
+
return 0;
|
|
640
|
+
// Clean the input and handle potential space issues
|
|
641
|
+
const cleanDuration = durationString.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
642
|
+
// Handle simple minute format like "90m"
|
|
643
|
+
if (/^\d+m$/.test(cleanDuration)) {
|
|
644
|
+
const minutes = parseInt(cleanDuration.replace('m', ''), 10);
|
|
645
|
+
return minutes * 60 * 1000;
|
|
646
|
+
}
|
|
647
|
+
// Handle simple hour format like "2h"
|
|
648
|
+
if (/^\d+h$/.test(cleanDuration)) {
|
|
649
|
+
const hours = parseInt(cleanDuration.replace('h', ''), 10);
|
|
650
|
+
return hours * 60 * 60 * 1000;
|
|
651
|
+
}
|
|
652
|
+
// Handle combined format like "1h 30m"
|
|
653
|
+
const combinedPattern = /^(\d+)h\s*(?:(\d+)m)?$|^(?:(\d+)h\s*)?(\d+)m$/;
|
|
654
|
+
const match = cleanDuration.match(combinedPattern);
|
|
655
|
+
if (match) {
|
|
656
|
+
const hours = parseInt(match[1] || match[3] || '0', 10);
|
|
657
|
+
const minutes = parseInt(match[2] || match[4] || '0', 10);
|
|
658
|
+
return (hours * 60 * 60 + minutes * 60) * 1000;
|
|
659
|
+
}
|
|
660
|
+
// Try to parse as just a number of minutes
|
|
661
|
+
const justMinutes = parseInt(cleanDuration, 10);
|
|
662
|
+
if (!isNaN(justMinutes)) {
|
|
663
|
+
return justMinutes * 60 * 1000;
|
|
664
|
+
}
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
// Export all time tracking tools
|
|
668
|
+
export const timeTrackingTools = [
|
|
669
|
+
getTaskTimeEntriesTool,
|
|
670
|
+
startTimeTrackingTool,
|
|
671
|
+
stopTimeTrackingTool,
|
|
672
|
+
addTimeEntryTool,
|
|
673
|
+
deleteTimeEntryTool,
|
|
674
|
+
getCurrentTimeEntryTool
|
|
675
|
+
];
|
|
676
|
+
// Export all time tracking handlers
|
|
677
|
+
export const timeTrackingHandlers = {
|
|
678
|
+
get_task_time_entries: handleGetTaskTimeEntries,
|
|
679
|
+
start_time_tracking: handleStartTimeTracking,
|
|
680
|
+
stop_time_tracking: handleStopTimeTracking,
|
|
681
|
+
add_time_entry: handleAddTimeEntry,
|
|
682
|
+
delete_time_entry: handleDeleteTimeEntry,
|
|
683
|
+
get_current_time_entry: handleGetCurrentTimeEntry
|
|
684
|
+
};
|
|
@@ -59,7 +59,7 @@ export class SponsorService {
|
|
|
59
59
|
if (this.isEnabled && includeSponsorMessage) {
|
|
60
60
|
content.push({
|
|
61
61
|
type: "text",
|
|
62
|
-
text:
|
|
62
|
+
text: `\n♥ Support this project by sponsoring the developer at ${this.sponsorUrl}`
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
return { content };
|
package/package.json
CHANGED