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