@aaronsb/jira-cloud-mcp 0.1.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.
@@ -0,0 +1,84 @@
1
+ import { BaseFormatter } from './base-formatter.js';
2
+ export class IssueFormatter {
3
+ /**
4
+ * Format an issue response with the standard structure and optional expansions
5
+ * @param issue The issue data
6
+ * @param options Expansion options
7
+ * @param transitions Optional transitions data (if requested)
8
+ * @returns A formatted issue response
9
+ */
10
+ static formatIssue(issue, options = {}, transitions) {
11
+ // Create metadata with available expansions
12
+ const metadata = this.createIssueMetadata(issue, options, transitions);
13
+ // Create summary with status and suggested actions
14
+ const summary = this.createIssueSummary(issue, transitions);
15
+ return BaseFormatter.formatResponse(issue, metadata, summary);
16
+ }
17
+ /**
18
+ * Create metadata for an issue response
19
+ */
20
+ static createIssueMetadata(issue, options, transitions) {
21
+ // Determine which expansions are available but not included
22
+ const availableExpansions = [];
23
+ if (!options.comments && issue.comments === undefined) {
24
+ availableExpansions.push('comments');
25
+ }
26
+ if (!options.transitions && transitions === undefined) {
27
+ availableExpansions.push('transitions');
28
+ }
29
+ if (!options.attachments && issue.attachments === undefined) {
30
+ availableExpansions.push('attachments');
31
+ }
32
+ if (!options.related_issues) {
33
+ availableExpansions.push('related_issues');
34
+ }
35
+ if (!options.history) {
36
+ availableExpansions.push('history');
37
+ }
38
+ // Create related entities map
39
+ const related = {};
40
+ if (issue.parent) {
41
+ related.parent = issue.parent;
42
+ }
43
+ // Extract related issues from issue links
44
+ const relatedIssues = issue.issueLinks
45
+ .map(link => link.outward || link.inward)
46
+ .filter((key) => key !== null);
47
+ if (relatedIssues.length > 0) {
48
+ related.linked_issues = relatedIssues;
49
+ }
50
+ return BaseFormatter.createMetadata({
51
+ expansions: availableExpansions,
52
+ related
53
+ });
54
+ }
55
+ /**
56
+ * Create a summary for an issue response
57
+ */
58
+ static createIssueSummary(issue, transitions) {
59
+ const suggestedActions = [];
60
+ // Add suggested actions based on available transitions
61
+ if (transitions && transitions.length > 0) {
62
+ // Common transitions to suggest
63
+ const commonTransitions = ['Done', 'In Progress', 'To Do', 'Closed', 'Resolved'];
64
+ for (const transitionName of commonTransitions) {
65
+ const transition = transitions.find(t => t.name === transitionName);
66
+ if (transition) {
67
+ suggestedActions.push({
68
+ text: `Move to ${transitionName}`,
69
+ action_id: transition.id
70
+ });
71
+ }
72
+ }
73
+ }
74
+ // Add assignment suggestion if not assigned
75
+ if (!issue.assignee) {
76
+ suggestedActions.push({
77
+ text: 'Assign to team member'
78
+ });
79
+ }
80
+ return BaseFormatter.createSummary({
81
+ suggested_actions: suggestedActions
82
+ });
83
+ }
84
+ }
@@ -0,0 +1,55 @@
1
+ import { BaseFormatter } from './base-formatter.js';
2
+ export class ProjectFormatter {
3
+ /**
4
+ * Format a project response with the standard structure and optional expansions
5
+ * @param project The project data
6
+ * @param options Expansion options
7
+ * @returns A formatted project response
8
+ */
9
+ static formatProject(project, options = {}) {
10
+ // Create metadata with available expansions
11
+ const metadata = this.createProjectMetadata(project, options);
12
+ // Create summary with status counts
13
+ const summary = this.createProjectSummary(project);
14
+ return BaseFormatter.formatResponse(project, metadata, summary);
15
+ }
16
+ /**
17
+ * Create metadata for a project response
18
+ */
19
+ static createProjectMetadata(project, options) {
20
+ // Determine which expansions are available but not included
21
+ const availableExpansions = [];
22
+ if (!options.boards) {
23
+ availableExpansions.push('boards');
24
+ }
25
+ if (!options.components) {
26
+ availableExpansions.push('components');
27
+ }
28
+ if (!options.versions) {
29
+ availableExpansions.push('versions');
30
+ }
31
+ if (!options.recent_issues) {
32
+ availableExpansions.push('recent_issues');
33
+ }
34
+ return BaseFormatter.createMetadata({
35
+ expansions: availableExpansions
36
+ });
37
+ }
38
+ /**
39
+ * Create a summary for a project response
40
+ */
41
+ static createProjectSummary(project) {
42
+ const suggestedActions = [
43
+ {
44
+ text: `View all issues in ${project.key}`
45
+ },
46
+ {
47
+ text: `Create issue in ${project.key}`
48
+ }
49
+ ];
50
+ return BaseFormatter.createSummary({
51
+ status_counts: project.status_counts,
52
+ suggested_actions: suggestedActions
53
+ });
54
+ }
55
+ }
@@ -0,0 +1,62 @@
1
+ import { BaseFormatter } from './base-formatter.js';
2
+ export class SearchFormatter {
3
+ /**
4
+ * Format a search response with the standard structure and optional expansions
5
+ * @param searchResult The search result data
6
+ * @param options Expansion options
7
+ * @returns A formatted search response
8
+ */
9
+ static formatSearchResult(searchResult, options = {}) {
10
+ // Create metadata with available expansions and pagination
11
+ const metadata = this.createSearchMetadata(searchResult, options);
12
+ // Create summary with status counts
13
+ const summary = this.createSearchSummary(searchResult);
14
+ return BaseFormatter.formatResponse(searchResult, metadata, summary);
15
+ }
16
+ /**
17
+ * Create metadata for a search response
18
+ */
19
+ static createSearchMetadata(searchResult, options) {
20
+ // Determine which expansions are available but not included
21
+ const availableExpansions = [];
22
+ if (!options.issue_details) {
23
+ availableExpansions.push('issue_details');
24
+ }
25
+ if (!options.transitions) {
26
+ availableExpansions.push('transitions');
27
+ }
28
+ if (!options.comments_preview) {
29
+ availableExpansions.push('comments_preview');
30
+ }
31
+ return BaseFormatter.createMetadata({
32
+ expansions: availableExpansions,
33
+ pagination: {
34
+ startAt: searchResult.pagination.startAt,
35
+ maxResults: searchResult.pagination.maxResults,
36
+ total: searchResult.pagination.total
37
+ }
38
+ });
39
+ }
40
+ /**
41
+ * Create a summary for a search response
42
+ */
43
+ static createSearchSummary(searchResult) {
44
+ // Count issues by status
45
+ const statusCounts = {};
46
+ for (const issue of searchResult.issues) {
47
+ const status = issue.status;
48
+ statusCounts[status] = (statusCounts[status] || 0) + 1;
49
+ }
50
+ const suggestedActions = [];
51
+ // Add pagination actions if there are more results
52
+ if (searchResult.pagination.hasMore) {
53
+ suggestedActions.push({
54
+ text: 'Load more results'
55
+ });
56
+ }
57
+ return BaseFormatter.createSummary({
58
+ status_counts: statusCounts,
59
+ suggested_actions: suggestedActions
60
+ });
61
+ }
62
+ }
@@ -0,0 +1,111 @@
1
+ import { BaseFormatter } from './base-formatter.js';
2
+ export class SprintFormatter {
3
+ /**
4
+ * Format a sprint response with the standard structure and optional expansions
5
+ * @param sprint The sprint data
6
+ * @param options Expansion options
7
+ * @returns A formatted sprint response
8
+ */
9
+ static formatSprint(sprint, options = {}) {
10
+ // Create metadata with available expansions
11
+ const metadata = this.createSprintMetadata(sprint, options);
12
+ // Create summary
13
+ const summary = this.createSprintSummary(sprint);
14
+ return BaseFormatter.formatResponse(sprint, metadata, summary);
15
+ }
16
+ /**
17
+ * Format a list of sprints
18
+ * @param sprints Array of sprint data
19
+ * @param pagination Pagination information
20
+ * @returns A formatted response with sprints array
21
+ */
22
+ static formatSprintList(sprints, pagination) {
23
+ // Create metadata
24
+ const metadata = {};
25
+ if (pagination) {
26
+ metadata.pagination = {
27
+ startAt: pagination.startAt,
28
+ maxResults: pagination.maxResults,
29
+ total: pagination.total,
30
+ hasMore: pagination.startAt + pagination.maxResults < pagination.total,
31
+ };
32
+ }
33
+ // Create summary with status counts
34
+ const statusCounts = {
35
+ future: 0,
36
+ active: 0,
37
+ closed: 0,
38
+ };
39
+ sprints.forEach(sprint => {
40
+ if (sprint.state && statusCounts[sprint.state] !== undefined) {
41
+ statusCounts[sprint.state]++;
42
+ }
43
+ });
44
+ const summary = {
45
+ status_counts: statusCounts,
46
+ suggested_actions: [
47
+ { text: 'Create new sprint', action_id: 'create_sprint' },
48
+ ],
49
+ };
50
+ return BaseFormatter.formatResponse(sprints, metadata, summary);
51
+ }
52
+ /**
53
+ * Create metadata for a sprint response
54
+ */
55
+ static createSprintMetadata(sprint, options) {
56
+ // Determine which expansions are available but not included
57
+ const availableExpansions = [];
58
+ if (!options.issues && !sprint.issues) {
59
+ availableExpansions.push('issues');
60
+ }
61
+ if (!options.report && !sprint.report) {
62
+ availableExpansions.push('report');
63
+ }
64
+ // Create related entities map
65
+ const related = {
66
+ board: sprint.boardId.toString(),
67
+ };
68
+ return BaseFormatter.createMetadata({
69
+ expansions: availableExpansions,
70
+ related
71
+ });
72
+ }
73
+ /**
74
+ * Create a summary for a sprint response
75
+ */
76
+ static createSprintSummary(sprint) {
77
+ const suggestedActions = [];
78
+ // Add suggested actions based on sprint state
79
+ switch (sprint.state) {
80
+ case 'future':
81
+ suggestedActions.push({ text: 'Start Sprint', action_id: 'start_sprint' });
82
+ suggestedActions.push({ text: 'Add Issues to Sprint', action_id: 'add_issues' });
83
+ suggestedActions.push({ text: 'Edit Sprint', action_id: 'update_sprint' });
84
+ break;
85
+ case 'active':
86
+ suggestedActions.push({ text: 'Complete Sprint', action_id: 'complete_sprint' });
87
+ suggestedActions.push({ text: 'Add Issues to Sprint', action_id: 'add_issues' });
88
+ suggestedActions.push({ text: 'Remove Issues from Sprint', action_id: 'remove_issues' });
89
+ suggestedActions.push({ text: 'Edit Sprint', action_id: 'update_sprint' });
90
+ break;
91
+ case 'closed':
92
+ suggestedActions.push({ text: 'View Sprint Report', action_id: 'view_report' });
93
+ suggestedActions.push({ text: 'Create New Sprint', action_id: 'create_sprint' });
94
+ break;
95
+ }
96
+ // Add status counts if issues are available
97
+ const statusCounts = {};
98
+ if (sprint.issues && sprint.issues.length > 0) {
99
+ sprint.issues.forEach(issue => {
100
+ if (!statusCounts[issue.status]) {
101
+ statusCounts[issue.status] = 0;
102
+ }
103
+ statusCounts[issue.status]++;
104
+ });
105
+ }
106
+ return BaseFormatter.createSummary({
107
+ status_counts: Object.keys(statusCounts).length > 0 ? statusCounts : undefined,
108
+ suggested_actions: suggestedActions
109
+ });
110
+ }
111
+ }
@@ -0,0 +1,343 @@
1
+ import MarkdownIt from 'markdown-it';
2
+ export class TextProcessor {
3
+ static md = new MarkdownIt();
4
+ static markdownToAdf(markdown) {
5
+ const tokens = TextProcessor.md.parse(markdown, {});
6
+ const content = [];
7
+ let currentListItems = [];
8
+ let isInList = false;
9
+ let listType = null;
10
+ for (const token of tokens) {
11
+ switch (token.type) {
12
+ case 'heading_open':
13
+ // Start a new heading block
14
+ content.push({
15
+ type: 'heading',
16
+ attrs: { level: parseInt(token.tag.slice(1)) },
17
+ content: []
18
+ });
19
+ break;
20
+ case 'heading_close':
21
+ break;
22
+ case 'paragraph_open':
23
+ if (!isInList) {
24
+ content.push({
25
+ type: 'paragraph',
26
+ content: []
27
+ });
28
+ }
29
+ break;
30
+ case 'paragraph_close':
31
+ break;
32
+ case 'bullet_list_open':
33
+ case 'ordered_list_open':
34
+ isInList = true;
35
+ listType = token.type === 'bullet_list_open' ? 'bulletList' : 'orderedList';
36
+ currentListItems = [];
37
+ break;
38
+ case 'bullet_list_close':
39
+ case 'ordered_list_close':
40
+ if (currentListItems.length > 0) {
41
+ content.push({
42
+ type: listType,
43
+ content: currentListItems
44
+ });
45
+ }
46
+ isInList = false;
47
+ listType = null;
48
+ currentListItems = [];
49
+ break;
50
+ case 'list_item_open':
51
+ currentListItems.push({
52
+ type: 'listItem',
53
+ content: [{
54
+ type: 'paragraph',
55
+ content: []
56
+ }]
57
+ });
58
+ break;
59
+ case 'list_item_close':
60
+ break;
61
+ case 'inline':
62
+ const lastBlock = isInList
63
+ ? currentListItems[currentListItems.length - 1].content[0]
64
+ : content[content.length - 1];
65
+ if (!lastBlock)
66
+ continue;
67
+ let currentText = '';
68
+ let marks = [];
69
+ for (let i = 0; i < token.children.length; i++) {
70
+ const child = token.children[i];
71
+ if (child.type === 'text') {
72
+ if (currentText && marks.length > 0) {
73
+ lastBlock.content.push({
74
+ type: 'text',
75
+ text: currentText,
76
+ marks
77
+ });
78
+ currentText = '';
79
+ marks = [];
80
+ }
81
+ currentText = child.content;
82
+ }
83
+ else if (child.type === 'strong_open') {
84
+ if (currentText) {
85
+ lastBlock.content.push({
86
+ type: 'text',
87
+ text: currentText
88
+ });
89
+ currentText = '';
90
+ }
91
+ marks.push({ type: 'strong' });
92
+ }
93
+ else if (child.type === 'em_open') {
94
+ if (currentText) {
95
+ lastBlock.content.push({
96
+ type: 'text',
97
+ text: currentText
98
+ });
99
+ currentText = '';
100
+ }
101
+ marks.push({ type: 'em' });
102
+ }
103
+ else if (child.type === 'link_open') {
104
+ if (currentText) {
105
+ lastBlock.content.push({
106
+ type: 'text',
107
+ text: currentText
108
+ });
109
+ currentText = '';
110
+ }
111
+ marks.push({
112
+ type: 'link',
113
+ attrs: {
114
+ href: child.attrs[0][1]
115
+ }
116
+ });
117
+ }
118
+ else if (child.type === 'code_inline') {
119
+ if (currentText) {
120
+ lastBlock.content.push({
121
+ type: 'text',
122
+ text: currentText
123
+ });
124
+ currentText = '';
125
+ }
126
+ lastBlock.content.push({
127
+ type: 'text',
128
+ text: child.content,
129
+ marks: [{ type: 'code' }]
130
+ });
131
+ }
132
+ }
133
+ if (currentText) {
134
+ lastBlock.content.push({
135
+ type: 'text',
136
+ text: currentText,
137
+ ...(marks.length > 0 && { marks })
138
+ });
139
+ }
140
+ break;
141
+ case 'hr':
142
+ content.push({
143
+ type: 'rule'
144
+ });
145
+ break;
146
+ case 'hardbreak':
147
+ const lastContent = content[content.length - 1];
148
+ if (lastContent && lastContent.content) {
149
+ lastContent.content.push({
150
+ type: 'hardBreak'
151
+ });
152
+ }
153
+ break;
154
+ }
155
+ }
156
+ return {
157
+ type: 'doc',
158
+ version: 1,
159
+ content
160
+ };
161
+ }
162
+ static extractTextFromAdf(node) {
163
+ if (!node)
164
+ return '';
165
+ if (node.type === 'text') {
166
+ return node.text || '';
167
+ }
168
+ if (node.type === 'mention') {
169
+ return `@${node.attrs?.text?.replace('@', '') || ''}`;
170
+ }
171
+ if (node.type === 'hardBreak' || node.type === 'paragraph') {
172
+ return '\n';
173
+ }
174
+ if (node.content) {
175
+ return node.content
176
+ .map((child) => TextProcessor.extractTextFromAdf(child))
177
+ .join('')
178
+ .replace(/\n{3,}/g, '\n\n'); // Normalize multiple newlines
179
+ }
180
+ return '';
181
+ }
182
+ static isFieldPopulated(value) {
183
+ if (value === null || value === undefined)
184
+ return false;
185
+ if (typeof value === 'string' && value.trim() === '')
186
+ return false;
187
+ if (Array.isArray(value) && value.length === 0)
188
+ return false;
189
+ if (typeof value === 'object' && Object.keys(value).length === 0)
190
+ return false;
191
+ return true;
192
+ }
193
+ static shouldExcludeField(fieldId, fieldValue) {
194
+ // Exclude system metadata and UI-specific fields
195
+ const excludePatterns = [
196
+ 'avatar', 'icon', 'self', 'thumbnail', 'timetracking', 'worklog',
197
+ 'watches', 'subtasks', 'attachment', 'aggregateprogress', 'progress',
198
+ 'votes', '_links', 'accountId', 'emailAddress', 'active', 'timeZone',
199
+ 'accountType', '_expands', 'groupIds', 'portalId', 'serviceDeskId',
200
+ 'issueTypeId', 'renderedFields', 'names', 'id', 'expand', 'schema',
201
+ 'operations', 'editmeta', 'changelog', 'versionedRepresentations',
202
+ 'fieldsToInclude', 'properties', 'updateAuthor', 'jsdPublic', 'mediaType',
203
+ 'maxResults', 'total', 'startAt', 'iconUrls', 'issuerestrictions',
204
+ 'shouldDisplay', 'nonEditableReason', 'hasEpicLinkFieldDependency',
205
+ 'showField', 'statusDate', 'statusCategory', 'collection', 'localId',
206
+ 'attrs', 'marks', 'layout', 'version', 'type', 'content', 'table',
207
+ 'tableRow', 'tableCell', 'mediaSingle', 'media', 'heading', 'paragraph',
208
+ 'bulletList', 'listItem', 'orderedList', 'rule', 'inlineCard', 'hardBreak',
209
+ 'workRatio', 'parentLink', 'restrictTo', 'timeToResolution',
210
+ 'timeToFirstResponse', 'slaForInitialResponse'
211
+ ];
212
+ // Also exclude email signature related fields and meaningless values
213
+ if (typeof fieldValue === 'string') {
214
+ // Email signature patterns
215
+ const emailPatterns = [
216
+ 'CAUTION:', 'From:', 'Sent:', 'To:', 'Subject:',
217
+ 'Book time to meet with me', 'Best-', 'Best regards',
218
+ 'Kind regards', 'Regards,', 'Mobile', 'Phone', 'Tel:',
219
+ 'www.', 'http://', 'https://', '@.*\\.com$', '^M:',
220
+ 'LLC', 'Inc.', 'Ltd.', 'ForefrontDermatology.com',
221
+ 'Mobile:', 'Office:', 'Direct:'
222
+ ];
223
+ // Check for email patterns
224
+ if (emailPatterns.some(pattern => pattern.startsWith('^') || pattern.endsWith('$')
225
+ ? new RegExp(pattern).test(fieldValue)
226
+ : fieldValue.includes(pattern))) {
227
+ return true;
228
+ }
229
+ // Exclude meaningless values
230
+ if (fieldValue === '-1' ||
231
+ fieldValue === 'false false' ||
232
+ fieldValue === '0' ||
233
+ fieldValue === 'true, ' ||
234
+ fieldValue === '.' ||
235
+ /^\s*$/.test(fieldValue)) {
236
+ return true;
237
+ }
238
+ }
239
+ // Exclude fields that are just punctuation or very short text
240
+ if (typeof fieldValue === 'string' &&
241
+ (fieldValue.trim().length <= 1 ||
242
+ fieldValue.trim() === '.' ||
243
+ fieldValue.trim() === '-' ||
244
+ fieldValue.trim() === '_')) {
245
+ return true;
246
+ }
247
+ return excludePatterns.some(pattern => fieldId.toLowerCase().includes(pattern.toLowerCase()));
248
+ }
249
+ static formatFieldValue(value, fieldName) {
250
+ if (value === null || value === undefined)
251
+ return '';
252
+ // Handle arrays
253
+ if (Array.isArray(value)) {
254
+ // Special handling for comments
255
+ if (fieldName === 'Comment' || fieldName === 'comments') {
256
+ return value
257
+ .map(comment => {
258
+ const author = comment.author?.displayName || 'Unknown';
259
+ let body = '';
260
+ // Handle rich text content
261
+ if (comment.body?.content) {
262
+ body = TextProcessor.extractTextFromAdf(comment.body);
263
+ }
264
+ else {
265
+ body = String(comment.body || '');
266
+ }
267
+ // Clean up email signatures and formatting from body
268
+ body = body
269
+ .replace(/^[\s\S]*?From:[\s\S]*?Sent:[\s\S]*?To:[\s\S]*?Subject:[\s\S]*?\n/gm, '')
270
+ .replace(/^>.*$/gm, '')
271
+ .replace(/_{3,}|-{3,}|={3,}/g, '')
272
+ .replace(/(?:(?:https?|ftp):\/\/|\b(?:[a-z\d]+\.))(?:(?:[^\s()<>]+|\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))?\))+(?:\((?:[^\s()<>]+|(?:\(?:[^\s()<>]+\)))?\)|[^\s`!()[\]{};:'".,<>?«»""'']))?/g, '')
273
+ .replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '')
274
+ .replace(/(?:^|\s)(?:Best regards|Kind regards|Regards|Best|Thanks|Thank you|Cheers),.*/gs, '')
275
+ .replace(/(?:Mobile|Tel|Phone|Office|Direct):\s*[\d\s.+-]+/g, '')
276
+ .replace(/\n{3,}/g, '\n\n')
277
+ .trim();
278
+ if (!body)
279
+ return '';
280
+ const created = new Date(comment.created).toLocaleString();
281
+ return `${author} (${created}):\n${body}`;
282
+ })
283
+ .filter(comment => comment)
284
+ .join('\n\n');
285
+ }
286
+ return value
287
+ .map(item => TextProcessor.formatFieldValue(item))
288
+ .filter(item => item)
289
+ .join(', ');
290
+ }
291
+ // Handle objects
292
+ if (typeof value === 'object') {
293
+ // Handle user objects
294
+ if (value.displayName) {
295
+ return value.displayName;
296
+ }
297
+ // Handle request type
298
+ if (value.requestType?.name) {
299
+ const desc = value.requestType.description ?
300
+ ': ' + value.requestType.description.split('.')[0] + '.' : '';
301
+ return `${value.requestType.name}${desc}`;
302
+ }
303
+ // Handle status objects
304
+ if (value.status && value.statusCategory) {
305
+ return `${value.status} (${value.statusCategory})`;
306
+ }
307
+ // Handle rich text content
308
+ if (value.content) {
309
+ return TextProcessor.extractTextFromAdf(value);
310
+ }
311
+ // Handle simple name/value objects
312
+ if (value.name) {
313
+ return value.name;
314
+ }
315
+ if (value.value) {
316
+ return value.value;
317
+ }
318
+ // For other objects, try to extract meaningful values
319
+ const meaningful = Object.entries(value)
320
+ .filter(([_k, v]) => !TextProcessor.shouldExcludeField(_k, v) &&
321
+ v !== null &&
322
+ v !== undefined &&
323
+ !_k.startsWith('_'))
324
+ .map(([_k, v]) => TextProcessor.formatFieldValue(v))
325
+ .filter(v => v)
326
+ .join(' ');
327
+ return meaningful || '';
328
+ }
329
+ // Format dates
330
+ if (fieldName && (fieldName.toLowerCase().includes('date') ||
331
+ fieldName.toLowerCase().includes('created') ||
332
+ fieldName.toLowerCase().includes('updated'))) {
333
+ try {
334
+ return new Date(value).toLocaleString();
335
+ }
336
+ catch {
337
+ return String(value);
338
+ }
339
+ }
340
+ // Handle primitive values
341
+ return String(value);
342
+ }
343
+ }