@taazkareem/clickup-mcp-server 0.6.2 → 0.6.3
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 +6 -6
- package/build/logger.js +26 -1
- package/build/server.js +1 -1
- package/build/services/clickup/base.js +22 -1
- package/build/services/clickup/bulk.js +76 -45
- package/build/services/clickup/index.js +2 -2
- package/build/services/clickup/task/index.js +32 -0
- package/build/services/clickup/task/task-attachments.js +97 -0
- package/build/services/clickup/task/task-comments.js +104 -0
- package/build/services/clickup/task/task-core.js +477 -0
- package/build/services/clickup/task/task-custom-fields.js +97 -0
- package/build/services/clickup/task/task-search.js +462 -0
- package/build/services/clickup/task/task-service.js +25 -0
- package/build/services/clickup/task/task-tags.js +101 -0
- package/build/services/clickup/workspace.js +81 -36
- package/build/tools/folder.js +1 -1
- package/build/tools/list.js +2 -4
- package/build/tools/task/attachments.js +18 -5
- package/build/tools/task/attachments.types.js +9 -0
- package/build/tools/task/bulk-operations.js +111 -15
- package/build/tools/task/handlers.js +169 -24
- package/build/tools/task/index.js +1 -1
- package/build/tools/task/main.js +36 -1
- package/build/tools/task/single-operations.js +51 -4
- package/build/tools/task/utilities.js +24 -71
- package/build/tools/utils.js +2 -2
- package/build/utils/date-utils.js +149 -30
- package/build/utils/resolver-utils.js +33 -40
- 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 -0
- package/build/services/clickup/task.js +0 -701
- 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
|
@@ -30,6 +30,31 @@ export function getRelativeTimestamp(minutes = 0, hours = 0, days = 0, weeks = 0
|
|
|
30
30
|
now.setMonth(now.getMonth() + months);
|
|
31
31
|
return now.getTime();
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the start of today (midnight) in Unix milliseconds
|
|
35
|
+
* @returns Timestamp in milliseconds for start of current day
|
|
36
|
+
*/
|
|
37
|
+
export function getStartOfDay() {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
now.setHours(0, 0, 0, 0);
|
|
40
|
+
return now.getTime();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the end of today (23:59:59.999) in Unix milliseconds
|
|
44
|
+
* @returns Timestamp in milliseconds for end of current day
|
|
45
|
+
*/
|
|
46
|
+
export function getEndOfDay() {
|
|
47
|
+
const now = new Date();
|
|
48
|
+
now.setHours(23, 59, 59, 999);
|
|
49
|
+
return now.getTime();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get the current time in Unix milliseconds
|
|
53
|
+
* @returns Current timestamp in milliseconds
|
|
54
|
+
*/
|
|
55
|
+
export function getCurrentTimestamp() {
|
|
56
|
+
return new Date().getTime();
|
|
57
|
+
}
|
|
33
58
|
/**
|
|
34
59
|
* Parse a due date string into a timestamp
|
|
35
60
|
* Supports ISO 8601 format or natural language like "tomorrow"
|
|
@@ -42,15 +67,36 @@ export function parseDueDate(dateString) {
|
|
|
42
67
|
return undefined;
|
|
43
68
|
try {
|
|
44
69
|
// Handle natural language dates
|
|
45
|
-
const lowerDate = dateString.toLowerCase();
|
|
46
|
-
|
|
70
|
+
const lowerDate = dateString.toLowerCase().trim();
|
|
71
|
+
// Handle "now" specifically
|
|
72
|
+
if (lowerDate === 'now') {
|
|
73
|
+
return getCurrentTimestamp();
|
|
74
|
+
}
|
|
75
|
+
// Handle "today" with different options
|
|
47
76
|
if (lowerDate === 'today') {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
77
|
+
return getEndOfDay();
|
|
78
|
+
}
|
|
79
|
+
if (lowerDate === 'today start' || lowerDate === 'start of today') {
|
|
80
|
+
return getStartOfDay();
|
|
81
|
+
}
|
|
82
|
+
if (lowerDate === 'today end' || lowerDate === 'end of today') {
|
|
83
|
+
return getEndOfDay();
|
|
84
|
+
}
|
|
85
|
+
// Handle "yesterday" and "tomorrow"
|
|
86
|
+
if (lowerDate === 'yesterday') {
|
|
87
|
+
const yesterday = new Date();
|
|
88
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
89
|
+
yesterday.setHours(23, 59, 59, 999);
|
|
90
|
+
return yesterday.getTime();
|
|
91
|
+
}
|
|
92
|
+
if (lowerDate === 'tomorrow') {
|
|
93
|
+
const tomorrow = new Date();
|
|
94
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
95
|
+
tomorrow.setHours(23, 59, 59, 999);
|
|
96
|
+
return tomorrow.getTime();
|
|
51
97
|
}
|
|
52
98
|
// Handle relative dates with specific times
|
|
53
|
-
const relativeTimeRegex = /(?:(\d+)\s*(minutes?|days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
|
|
99
|
+
const relativeTimeRegex = /(?:(\d+)\s*(minutes?|hours?|days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month|year))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
|
|
54
100
|
const match = lowerDate.match(relativeTimeRegex);
|
|
55
101
|
if (match) {
|
|
56
102
|
const date = new Date();
|
|
@@ -61,6 +107,9 @@ export function parseDueDate(dateString) {
|
|
|
61
107
|
if (unit.startsWith('minute')) {
|
|
62
108
|
date.setMinutes(date.getMinutes() + value);
|
|
63
109
|
}
|
|
110
|
+
else if (unit.startsWith('hour')) {
|
|
111
|
+
date.setHours(date.getHours() + value);
|
|
112
|
+
}
|
|
64
113
|
else if (unit.startsWith('day')) {
|
|
65
114
|
date.setDate(date.getDate() + value);
|
|
66
115
|
}
|
|
@@ -80,6 +129,9 @@ export function parseDueDate(dateString) {
|
|
|
80
129
|
else if (lowerDate.includes('next month')) {
|
|
81
130
|
date.setMonth(date.getMonth() + 1);
|
|
82
131
|
}
|
|
132
|
+
else if (lowerDate.includes('next year')) {
|
|
133
|
+
date.setFullYear(date.getFullYear() + 1);
|
|
134
|
+
}
|
|
83
135
|
// Set the time if specified
|
|
84
136
|
if (hours) {
|
|
85
137
|
let parsedHours = parseInt(hours);
|
|
@@ -97,31 +149,44 @@ export function parseDueDate(dateString) {
|
|
|
97
149
|
}
|
|
98
150
|
return date.getTime();
|
|
99
151
|
}
|
|
100
|
-
// Handle
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
if (daysRegex.test(lowerDate)) {
|
|
115
|
-
const days = parseInt(lowerDate.match(daysRegex)[1]);
|
|
116
|
-
return getRelativeTimestamp(0, 0, days);
|
|
117
|
-
}
|
|
118
|
-
if (weeksRegex.test(lowerDate)) {
|
|
119
|
-
const weeks = parseInt(lowerDate.match(weeksRegex)[1]);
|
|
120
|
-
return getRelativeTimestamp(0, 0, 0, weeks);
|
|
152
|
+
// Handle various relative formats
|
|
153
|
+
const relativeFormats = [
|
|
154
|
+
{ regex: /(\d+)\s*minutes?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(m) },
|
|
155
|
+
{ regex: /(\d+)\s*hours?\s*from\s*now/i, handler: (h) => getRelativeTimestamp(0, h) },
|
|
156
|
+
{ regex: /(\d+)\s*days?\s*from\s*now/i, handler: (d) => getRelativeTimestamp(0, 0, d) },
|
|
157
|
+
{ regex: /(\d+)\s*weeks?\s*from\s*now/i, handler: (w) => getRelativeTimestamp(0, 0, 0, w) },
|
|
158
|
+
{ regex: /(\d+)\s*months?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(0, 0, 0, 0, m) }
|
|
159
|
+
];
|
|
160
|
+
for (const format of relativeFormats) {
|
|
161
|
+
if (format.regex.test(lowerDate)) {
|
|
162
|
+
const value = parseInt(lowerDate.match(format.regex)[1]);
|
|
163
|
+
return format.handler(value);
|
|
164
|
+
}
|
|
121
165
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
166
|
+
// Handle specific date formats
|
|
167
|
+
// Format: MM/DD/YYYY
|
|
168
|
+
const usDateRegex = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2})(?::(\d{1,2}))?(?:\s+(am|pm))?)?$/i;
|
|
169
|
+
const usDateMatch = lowerDate.match(usDateRegex);
|
|
170
|
+
if (usDateMatch) {
|
|
171
|
+
const [_, month, day, year, hours, minutes, meridian] = usDateMatch;
|
|
172
|
+
const date = new Date(parseInt(year), parseInt(month) - 1, // JS months are 0-indexed
|
|
173
|
+
parseInt(day));
|
|
174
|
+
// Add time if specified
|
|
175
|
+
if (hours) {
|
|
176
|
+
let parsedHours = parseInt(hours);
|
|
177
|
+
const parsedMinutes = minutes ? parseInt(minutes) : 0;
|
|
178
|
+
// Convert to 24-hour format if meridian is specified
|
|
179
|
+
if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
|
|
180
|
+
parsedHours += 12;
|
|
181
|
+
if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
|
|
182
|
+
parsedHours = 0;
|
|
183
|
+
date.setHours(parsedHours, parsedMinutes, 0, 0);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Default to end of day if no time specified
|
|
187
|
+
date.setHours(23, 59, 59, 999);
|
|
188
|
+
}
|
|
189
|
+
return date.getTime();
|
|
125
190
|
}
|
|
126
191
|
// Try to parse as a date string
|
|
127
192
|
const date = new Date(dateString);
|
|
@@ -164,3 +229,57 @@ export function formatDueDate(timestamp) {
|
|
|
164
229
|
return undefined;
|
|
165
230
|
}
|
|
166
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Checks if a timestamp is for today
|
|
234
|
+
*
|
|
235
|
+
* @param timestamp Unix timestamp in milliseconds
|
|
236
|
+
* @returns Boolean indicating if the timestamp is for today
|
|
237
|
+
*/
|
|
238
|
+
export function isToday(timestamp) {
|
|
239
|
+
const date = new Date(timestamp);
|
|
240
|
+
const today = new Date();
|
|
241
|
+
return date.getDate() === today.getDate() &&
|
|
242
|
+
date.getMonth() === today.getMonth() &&
|
|
243
|
+
date.getFullYear() === today.getFullYear();
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get timestamp range for today (start to end)
|
|
247
|
+
*
|
|
248
|
+
* @returns Object with start and end timestamps for today
|
|
249
|
+
*/
|
|
250
|
+
export function getTodayRange() {
|
|
251
|
+
return {
|
|
252
|
+
start: getStartOfDay(),
|
|
253
|
+
end: getEndOfDay()
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Format a date for display in errors and messages
|
|
258
|
+
* @param timestamp The timestamp to format
|
|
259
|
+
* @returns A human-readable relative time (e.g., "2 hours ago")
|
|
260
|
+
*/
|
|
261
|
+
export function formatRelativeTime(timestamp) {
|
|
262
|
+
if (!timestamp)
|
|
263
|
+
return 'Unknown';
|
|
264
|
+
const timestampNum = typeof timestamp === 'string' ? parseInt(timestamp, 10) : timestamp;
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
const diffMs = now - timestampNum;
|
|
267
|
+
// Convert to appropriate time unit
|
|
268
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
269
|
+
if (diffSec < 60)
|
|
270
|
+
return `${diffSec} seconds ago`;
|
|
271
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
272
|
+
if (diffMin < 60)
|
|
273
|
+
return `${diffMin} minutes ago`;
|
|
274
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
275
|
+
if (diffHour < 24)
|
|
276
|
+
return `${diffHour} hours ago`;
|
|
277
|
+
const diffDays = Math.floor(diffHour / 24);
|
|
278
|
+
if (diffDays < 30)
|
|
279
|
+
return `${diffDays} days ago`;
|
|
280
|
+
const diffMonths = Math.floor(diffDays / 30);
|
|
281
|
+
if (diffMonths < 12)
|
|
282
|
+
return `${diffMonths} months ago`;
|
|
283
|
+
const diffYears = Math.floor(diffMonths / 12);
|
|
284
|
+
return `${diffYears} years ago`;
|
|
285
|
+
}
|
|
@@ -9,13 +9,40 @@
|
|
|
9
9
|
import { clickUpServices } from '../services/shared.js';
|
|
10
10
|
import { findListIDByName } from '../tools/list.js';
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Check if a task name matches search criteria
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
* Performs flexible case-insensitive and emoji-aware text matching
|
|
15
|
+
* Used by multiple components for consistent name matching behavior
|
|
16
|
+
*/
|
|
17
|
+
export function isNameMatch(taskName, searchTerm) {
|
|
18
|
+
const normalizedTask = taskName.toLowerCase().trim();
|
|
19
|
+
const normalizedSearch = searchTerm.toLowerCase().trim();
|
|
20
|
+
// Handle empty strings - don't match empty task names
|
|
21
|
+
if (normalizedTask === '')
|
|
22
|
+
return false;
|
|
23
|
+
if (normalizedSearch === '')
|
|
24
|
+
return false;
|
|
25
|
+
// Exact match check
|
|
26
|
+
if (normalizedTask === normalizedSearch)
|
|
27
|
+
return true;
|
|
28
|
+
// Substring match check
|
|
29
|
+
if (normalizedTask.includes(normalizedSearch) || normalizedSearch.includes(normalizedTask))
|
|
30
|
+
return true;
|
|
31
|
+
// Handle emoji characters in names
|
|
32
|
+
if (/[\p{Emoji}]/u.test(normalizedSearch) || /[\p{Emoji}]/u.test(normalizedTask)) {
|
|
33
|
+
const taskWithoutEmoji = normalizedTask.replace(/[\p{Emoji}]/gu, '').trim();
|
|
34
|
+
const searchWithoutEmoji = normalizedSearch.replace(/[\p{Emoji}]/gu, '').trim();
|
|
35
|
+
// Don't match if either becomes empty after emoji removal
|
|
36
|
+
if (taskWithoutEmoji === '' || searchWithoutEmoji === '')
|
|
37
|
+
return false;
|
|
38
|
+
return taskWithoutEmoji === searchWithoutEmoji ||
|
|
39
|
+
taskWithoutEmoji.includes(searchWithoutEmoji) ||
|
|
40
|
+
searchWithoutEmoji.includes(taskWithoutEmoji);
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a list ID from either a direct ID or list name
|
|
19
46
|
*/
|
|
20
47
|
export async function resolveListId(listId, listName, workspaceService = clickUpServices.workspace) {
|
|
21
48
|
// If list ID is directly provided, use it
|
|
@@ -33,37 +60,3 @@ export async function resolveListId(listId, listName, workspaceService = clickUp
|
|
|
33
60
|
// If neither is provided, throw an error
|
|
34
61
|
throw new Error("Either listId or listName must be provided");
|
|
35
62
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Resolve a task ID from either a direct ID or task name + list info
|
|
38
|
-
*
|
|
39
|
-
* @param taskId Optional direct task ID
|
|
40
|
-
* @param taskName Optional task name to resolve
|
|
41
|
-
* @param listId Optional list ID for task lookup
|
|
42
|
-
* @param listName Optional list name for task lookup
|
|
43
|
-
* @param taskService Task service to use for lookup
|
|
44
|
-
* @returns Resolved task ID
|
|
45
|
-
* @throws Error if parameters are insufficient or task can't be found
|
|
46
|
-
*/
|
|
47
|
-
export async function resolveTaskId(taskId, taskName, listId, listName, taskService = clickUpServices.task) {
|
|
48
|
-
// If task ID is directly provided, use it
|
|
49
|
-
if (taskId) {
|
|
50
|
-
return taskId;
|
|
51
|
-
}
|
|
52
|
-
// If task name is provided, we need list info to find it
|
|
53
|
-
if (taskName) {
|
|
54
|
-
// We need either listId or listName to find a task by name
|
|
55
|
-
if (!listId && !listName) {
|
|
56
|
-
throw new Error(`List name or ID is required when using task name for task "${taskName}"`);
|
|
57
|
-
}
|
|
58
|
-
// Get list ID
|
|
59
|
-
const targetListId = await resolveListId(listId, listName);
|
|
60
|
-
// Find the task in the list
|
|
61
|
-
const foundTask = await taskService.findTaskByName(targetListId, taskName);
|
|
62
|
-
if (!foundTask) {
|
|
63
|
-
throw new Error(`Task "${taskName}" not found in list`);
|
|
64
|
-
}
|
|
65
|
-
return foundTask.id;
|
|
66
|
-
}
|
|
67
|
-
// If neither is provided, throw an error
|
|
68
|
-
throw new Error("Either taskId or taskName must be provided");
|
|
69
|
-
}
|
|
@@ -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: `♥ Support this project by sponsoring the developer at ${this.sponsorUrl}`
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
return { content };
|
package/package.json
CHANGED
package/build/mcp-tools.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Logger } from "./logger.js";
|
|
2
|
-
// Create a logger instance
|
|
3
|
-
const logger = new Logger('MCPTools');
|
|
4
|
-
/**
|
|
5
|
-
* Register a handler for a tool that may receive JSON string parameters
|
|
6
|
-
* This wrapper ensures that array and object parameters are properly parsed
|
|
7
|
-
*
|
|
8
|
-
* @param server MCP Server instance
|
|
9
|
-
* @param name Tool name
|
|
10
|
-
* @param handler Handler function
|
|
11
|
-
*/
|
|
12
|
-
export function registerToolHandler(server, name, handler) {
|
|
13
|
-
// Create a wrapper handler that pre-processes parameters
|
|
14
|
-
const wrappedHandler = async (params) => {
|
|
15
|
-
logger.debug(`Processing parameters for tool ${name}`, { params });
|
|
16
|
-
try {
|
|
17
|
-
// Process the parameters before passing them to the actual handler
|
|
18
|
-
const processedParams = {};
|
|
19
|
-
// Process each parameter - try to parse strings that might be JSON
|
|
20
|
-
for (const [key, value] of Object.entries(params)) {
|
|
21
|
-
if (typeof value === 'string') {
|
|
22
|
-
try {
|
|
23
|
-
// Check if this might be a JSON array or object
|
|
24
|
-
if ((value.startsWith('[') && value.endsWith(']')) ||
|
|
25
|
-
(value.startsWith('{') && value.endsWith('}'))) {
|
|
26
|
-
try {
|
|
27
|
-
processedParams[key] = JSON.parse(value);
|
|
28
|
-
logger.debug(`Parsed JSON parameter: ${key}`, { original: value, parsed: processedParams[key] });
|
|
29
|
-
}
|
|
30
|
-
catch (parseError) {
|
|
31
|
-
// If parsing fails, use the original string
|
|
32
|
-
processedParams[key] = value;
|
|
33
|
-
logger.debug(`Failed to parse JSON for parameter: ${key}, using original`, { error: parseError.message });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
processedParams[key] = value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
// If there's any error, use the original value
|
|
42
|
-
processedParams[key] = value;
|
|
43
|
-
logger.debug(`Error processing parameter: ${key}`, { error: error.message });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
// Non-string values are used as-is
|
|
48
|
-
processedParams[key] = value;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
logger.debug(`Processed parameters for tool ${name}`, { processedParams });
|
|
52
|
-
// Call the original handler with processed parameters
|
|
53
|
-
return handler(processedParams);
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
logger.error(`Error in wrapped handler for tool ${name}:`, { error: error.stack || error.message });
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
// Use setRequestHandler to register the wrapped handler
|
|
61
|
-
logger.info(`Registering wrapped handler for tool: ${name}`);
|
|
62
|
-
// Override the tool's handler in the CallTool switch statement
|
|
63
|
-
// The server.ts file will use the switch case to call this handler
|
|
64
|
-
}
|
package/build/server-state.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server State Management Module
|
|
3
|
-
*
|
|
4
|
-
* This module provides shared state management for the MCP server,
|
|
5
|
-
* particularly for controlling shutdown behavior and tracking busy states.
|
|
6
|
-
*/
|
|
7
|
-
// State variables
|
|
8
|
-
let serverBusyState = false; // Tracks if server is doing critical work
|
|
9
|
-
let gracePeriodActive = false; // Tracks if we're in post-initialization grace period
|
|
10
|
-
let gracePeriodTimer = null;
|
|
11
|
-
export const GRACE_PERIOD_MS = 10000; // 10 second grace period after startup
|
|
12
|
-
/**
|
|
13
|
-
* Logging helper that avoids circular dependency
|
|
14
|
-
*/
|
|
15
|
-
function safeLog(level, message, data) {
|
|
16
|
-
// Use console as a fallback to avoid circular dependency with logger
|
|
17
|
-
const timestamp = new Date().toISOString();
|
|
18
|
-
if (level === 'error') {
|
|
19
|
-
console.error(`[${timestamp}] ${level.toUpperCase()}: ${message}`, data || '');
|
|
20
|
-
}
|
|
21
|
-
else if (level === 'debug' && process.env.DEBUG) {
|
|
22
|
-
console.debug(`[${timestamp}] ${level.toUpperCase()}: ${message}`, data || '');
|
|
23
|
-
}
|
|
24
|
-
else if (level !== 'debug') {
|
|
25
|
-
console.log(`[${timestamp}] ${level.toUpperCase()}: ${message}`, data || '');
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Set the server as busy doing critical work that shouldn't be interrupted
|
|
30
|
-
* @param busy Whether the server is currently busy with critical operations
|
|
31
|
-
*/
|
|
32
|
-
export function setServerBusy(busy) {
|
|
33
|
-
serverBusyState = busy;
|
|
34
|
-
safeLog('debug', `Server busy state set to: ${busy}`);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Start grace period after initialization to prevent immediate shutdown
|
|
38
|
-
*/
|
|
39
|
-
export function startGracePeriod() {
|
|
40
|
-
gracePeriodActive = true;
|
|
41
|
-
safeLog('debug', `Starting ${GRACE_PERIOD_MS}ms grace period to prevent premature shutdown`);
|
|
42
|
-
if (gracePeriodTimer) {
|
|
43
|
-
clearTimeout(gracePeriodTimer);
|
|
44
|
-
}
|
|
45
|
-
gracePeriodTimer = setTimeout(() => {
|
|
46
|
-
gracePeriodActive = false;
|
|
47
|
-
safeLog('debug', 'Grace period ended, server will now respond to shutdown signals');
|
|
48
|
-
gracePeriodTimer = null;
|
|
49
|
-
}, GRACE_PERIOD_MS);
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Cancel the grace period if needed
|
|
53
|
-
*/
|
|
54
|
-
export function cancelGracePeriod() {
|
|
55
|
-
if (gracePeriodTimer) {
|
|
56
|
-
clearTimeout(gracePeriodTimer);
|
|
57
|
-
gracePeriodTimer = null;
|
|
58
|
-
}
|
|
59
|
-
gracePeriodActive = false;
|
|
60
|
-
safeLog('debug', 'Grace period canceled, server will now respond to shutdown signals');
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Check if the server should ignore shutdown signals
|
|
64
|
-
* @returns true if shutdown signals should be ignored
|
|
65
|
-
*/
|
|
66
|
-
export function shouldIgnoreShutdown() {
|
|
67
|
-
// Ignore shutdown if explicitly configured via environment variable
|
|
68
|
-
if (process.env.FORCE_KEEP_ALIVE === 'true') {
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
// Ignore shutdown during the grace period after startup
|
|
72
|
-
if (gracePeriodActive) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
// Ignore shutdown if the server is doing critical work
|
|
76
|
-
if (serverBusyState) {
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
// Otherwise, allow normal shutdown
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Check if grace period is currently active
|
|
84
|
-
*/
|
|
85
|
-
export function isGracePeriodActive() {
|
|
86
|
-
return gracePeriodActive;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Check if server is currently in busy state
|
|
90
|
-
*/
|
|
91
|
-
export function isServerBusy() {
|
|
92
|
-
return serverBusyState;
|
|
93
|
-
}
|
package/build/server.log
DELETED
|
File without changes
|