@delorenj/mcp-server-trello 1.6.2 → 1.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.
@@ -1,699 +0,0 @@
1
- import axios from 'axios';
2
- import FormData from 'form-data';
3
- import { createTrelloRateLimiters } from './rate-limiter.js';
4
- import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
5
- import * as fs from 'fs/promises';
6
- import * as path from 'path';
7
- import { createReadStream } from 'fs';
8
- import { fileURLToPath } from 'url';
9
- // Path for storing active board/workspace configuration
10
- const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.trello-mcp');
11
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
12
- export class TrelloClient {
13
- constructor(config) {
14
- this.config = config;
15
- this.defaultBoardId = config.defaultBoardId;
16
- this.activeConfig = { ...config };
17
- // If boardId is provided in config, use it as the active board
18
- if (config.boardId && !this.activeConfig.boardId) {
19
- this.activeConfig.boardId = config.boardId;
20
- }
21
- // If defaultBoardId is provided but boardId is not, use defaultBoardId
22
- if (this.defaultBoardId && !this.activeConfig.boardId) {
23
- this.activeConfig.boardId = this.defaultBoardId;
24
- }
25
- this.axiosInstance = axios.create({
26
- baseURL: 'https://api.trello.com/1',
27
- params: {
28
- key: config.apiKey,
29
- token: config.token,
30
- },
31
- });
32
- this.rateLimiter = createTrelloRateLimiters();
33
- // Add rate limiting interceptor
34
- this.axiosInstance.interceptors.request.use(async (config) => {
35
- await this.rateLimiter.waitForAvailableToken();
36
- return config;
37
- });
38
- }
39
- /**
40
- * Load saved configuration from disk
41
- */
42
- async loadConfig() {
43
- try {
44
- await fs.mkdir(CONFIG_DIR, { recursive: true });
45
- const data = await fs.readFile(CONFIG_FILE, 'utf8');
46
- const savedConfig = JSON.parse(data);
47
- // Only update boardId and workspaceId, keep credentials from env
48
- if (savedConfig.boardId) {
49
- this.activeConfig.boardId = savedConfig.boardId;
50
- }
51
- if (savedConfig.workspaceId) {
52
- this.activeConfig.workspaceId = savedConfig.workspaceId;
53
- }
54
- }
55
- catch (error) {
56
- // File might not exist yet, that's okay
57
- if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
58
- throw error;
59
- }
60
- }
61
- }
62
- /**
63
- * Save current configuration to disk
64
- */
65
- async saveConfig() {
66
- try {
67
- await fs.mkdir(CONFIG_DIR, { recursive: true });
68
- const configToSave = {
69
- boardId: this.activeConfig.boardId,
70
- workspaceId: this.activeConfig.workspaceId,
71
- };
72
- await fs.writeFile(CONFIG_FILE, JSON.stringify(configToSave, null, 2));
73
- }
74
- catch (error) {
75
- // Failed to save configuration
76
- throw new Error('Failed to save configuration');
77
- }
78
- }
79
- /**
80
- * Get the current active board ID
81
- */
82
- get activeBoardId() {
83
- return this.activeConfig.boardId;
84
- }
85
- /**
86
- * Get the current active workspace ID
87
- */
88
- get activeWorkspaceId() {
89
- return this.activeConfig.workspaceId;
90
- }
91
- /**
92
- * Set the active board
93
- */
94
- async setActiveBoard(boardId) {
95
- // Verify the board exists
96
- const board = await this.getBoardById(boardId);
97
- this.activeConfig.boardId = boardId;
98
- await this.saveConfig();
99
- return board;
100
- }
101
- /**
102
- * Set the active workspace
103
- */
104
- async setActiveWorkspace(workspaceId) {
105
- // Verify the workspace exists
106
- const workspace = await this.getWorkspaceById(workspaceId);
107
- this.activeConfig.workspaceId = workspaceId;
108
- await this.saveConfig();
109
- return workspace;
110
- }
111
- async handleRequest(requestFn) {
112
- try {
113
- return await requestFn();
114
- }
115
- catch (error) {
116
- if (axios.isAxiosError(error)) {
117
- if (error.response?.status === 429) {
118
- // Rate limit exceeded, wait and retry
119
- await new Promise(resolve => setTimeout(resolve, 1000));
120
- return this.handleRequest(requestFn);
121
- }
122
- // Trello API Error
123
- // Customize error handling based on Trello's error structure if needed
124
- throw new McpError(ErrorCode.InternalError, `Trello API Error: ${error.response?.status} ${error.message}`, error.response?.data);
125
- }
126
- else {
127
- // Unexpected Error
128
- throw new McpError(ErrorCode.InternalError, 'An unexpected error occurred');
129
- }
130
- }
131
- }
132
- /**
133
- * List all boards the user has access to
134
- */
135
- async listBoards() {
136
- return this.handleRequest(async () => {
137
- const response = await this.axiosInstance.get('/members/me/boards');
138
- return response.data;
139
- });
140
- }
141
- /**
142
- * Get a specific board by ID
143
- */
144
- async getBoardById(boardId) {
145
- return this.handleRequest(async () => {
146
- const response = await this.axiosInstance.get(`/boards/${boardId}`);
147
- return response.data;
148
- });
149
- }
150
- /**
151
- * List all workspaces the user has access to
152
- */
153
- async listWorkspaces() {
154
- return this.handleRequest(async () => {
155
- const response = await this.axiosInstance.get('/members/me/organizations');
156
- return response.data;
157
- });
158
- }
159
- /**
160
- * Get a specific workspace by ID
161
- */
162
- async getWorkspaceById(workspaceId) {
163
- return this.handleRequest(async () => {
164
- const response = await this.axiosInstance.get(`/organizations/${workspaceId}`);
165
- return response.data;
166
- });
167
- }
168
- /**
169
- * List boards in a specific workspace
170
- */
171
- async listBoardsInWorkspace(workspaceId) {
172
- return this.handleRequest(async () => {
173
- const response = await this.axiosInstance.get(`/organizations/${workspaceId}/boards`);
174
- return response.data;
175
- });
176
- }
177
- /**
178
- * Create a new board
179
- */
180
- async createBoard(params) {
181
- return this.handleRequest(async () => {
182
- const response = await this.axiosInstance.post('/boards', {
183
- name: params.name,
184
- desc: params.desc,
185
- idOrganization: params.idOrganization ?? this.activeConfig.workspaceId,
186
- defaultLabels: params.defaultLabels,
187
- defaultLists: params.defaultLists,
188
- });
189
- return response.data;
190
- });
191
- }
192
- async getCardsByList(boardId, listId) {
193
- return this.handleRequest(async () => {
194
- const response = await this.axiosInstance.get(`/lists/${listId}/cards`);
195
- return response.data;
196
- });
197
- }
198
- async getLists(boardId) {
199
- const effectiveBoardId = boardId || this.activeConfig.boardId || this.defaultBoardId;
200
- if (!effectiveBoardId) {
201
- throw new McpError(ErrorCode.InvalidParams, 'boardId is required when no default board is configured');
202
- }
203
- return this.handleRequest(async () => {
204
- const response = await this.axiosInstance.get(`/boards/${effectiveBoardId}/lists`);
205
- return response.data;
206
- });
207
- }
208
- async getRecentActivity(boardId, limit = 10) {
209
- const effectiveBoardId = boardId || this.activeConfig.boardId || this.defaultBoardId;
210
- if (!effectiveBoardId) {
211
- throw new McpError(ErrorCode.InvalidParams, 'boardId is required when no default board is configured');
212
- }
213
- return this.handleRequest(async () => {
214
- const response = await this.axiosInstance.get(`/boards/${effectiveBoardId}/actions`, {
215
- params: { limit },
216
- });
217
- return response.data;
218
- });
219
- }
220
- async addCard(boardId, params) {
221
- return this.handleRequest(async () => {
222
- const response = await this.axiosInstance.post('/cards', {
223
- idList: params.listId,
224
- name: params.name,
225
- desc: params.description,
226
- due: params.dueDate,
227
- start: params.start,
228
- idLabels: params.labels,
229
- });
230
- return response.data;
231
- });
232
- }
233
- async updateCard(boardId, params) {
234
- return this.handleRequest(async () => {
235
- const response = await this.axiosInstance.put(`/cards/${params.cardId}`, {
236
- name: params.name,
237
- desc: params.description,
238
- due: params.dueDate,
239
- start: params.start,
240
- dueComplete: params.dueComplete,
241
- idLabels: params.labels,
242
- });
243
- return response.data;
244
- });
245
- }
246
- async archiveCard(boardId, cardId) {
247
- return this.handleRequest(async () => {
248
- const response = await this.axiosInstance.put(`/cards/${cardId}`, {
249
- closed: true,
250
- });
251
- return response.data;
252
- });
253
- }
254
- async moveCard(boardId, cardId, listId) {
255
- const effectiveBoardId = boardId || this.defaultBoardId;
256
- return this.handleRequest(async () => {
257
- const response = await this.axiosInstance.put(`/cards/${cardId}`, {
258
- idList: listId,
259
- ...(effectiveBoardId && { idBoard: effectiveBoardId }),
260
- });
261
- return response.data;
262
- });
263
- }
264
- async addList(boardId, name) {
265
- const effectiveBoardId = boardId || this.activeConfig.boardId || this.defaultBoardId;
266
- if (!effectiveBoardId) {
267
- throw new McpError(ErrorCode.InvalidParams, 'boardId is required when no default board is configured');
268
- }
269
- return this.handleRequest(async () => {
270
- const response = await this.axiosInstance.post('/lists', {
271
- name,
272
- idBoard: effectiveBoardId,
273
- });
274
- return response.data;
275
- });
276
- }
277
- async archiveList(boardId, listId) {
278
- return this.handleRequest(async () => {
279
- const response = await this.axiosInstance.put(`/lists/${listId}/closed`, {
280
- value: true,
281
- });
282
- return response.data;
283
- });
284
- }
285
- async getMyCards() {
286
- return this.handleRequest(async () => {
287
- const response = await this.axiosInstance.get('/members/me/cards');
288
- return response.data;
289
- });
290
- }
291
- async attachImageToCard(boardId, cardId, imageUrl, name) {
292
- // Simply delegate to attachFileToCard - it will auto-detect MIME type for images
293
- return this.attachFileToCard(boardId, cardId, imageUrl, name || 'Image Attachment', undefined);
294
- }
295
- async attachFileToCard(boardId, cardId, fileUrl, name, mimeType) {
296
- return this.handleRequest(async () => {
297
- // Check if fileUrl is a local file path (starts with file://)
298
- if (fileUrl.startsWith('file://')) {
299
- // Handle local file upload
300
- const localPath = fileURLToPath(fileUrl);
301
- let effectiveMimeType = mimeType;
302
- if (!effectiveMimeType) {
303
- const ext = path.extname(localPath).toLowerCase();
304
- effectiveMimeType = MIME_TYPES[ext] || 'application/octet-stream';
305
- }
306
- // Check if file exists
307
- try {
308
- await fs.access(localPath);
309
- }
310
- catch (error) {
311
- throw new McpError(ErrorCode.InvalidRequest, `File not found: ${localPath}`);
312
- }
313
- // Create form data for multipart upload
314
- const form = new FormData();
315
- const fileStream = createReadStream(localPath);
316
- const fileName = name || path.basename(localPath);
317
- form.append('file', fileStream, {
318
- filename: fileName,
319
- contentType: effectiveMimeType,
320
- });
321
- // Add name and mimeType to form
322
- form.append('name', fileName);
323
- form.append('mimeType', effectiveMimeType);
324
- // Upload file directly to Trello using the configured axios instance
325
- const response = await this.axiosInstance.post(`/cards/${cardId}/attachments`, form, {
326
- headers: {
327
- ...form.getHeaders(),
328
- },
329
- });
330
- return response.data;
331
- }
332
- else {
333
- // Handle URL attachment
334
- const remoteUrlPath = new URL(fileUrl).pathname;
335
- let effectiveMimeType = mimeType;
336
- if (!effectiveMimeType) {
337
- const ext = path.extname(remoteUrlPath).toLowerCase();
338
- effectiveMimeType = MIME_TYPES[ext] || 'application/octet-stream';
339
- }
340
- const response = await this.axiosInstance.post(`/cards/${cardId}/attachments`, {
341
- url: fileUrl,
342
- name: name || 'File Attachment',
343
- mimeType: effectiveMimeType,
344
- });
345
- return response.data;
346
- }
347
- });
348
- }
349
- async getCard(cardId, includeMarkdown = false) {
350
- return this.handleRequest(async () => {
351
- const response = await this.axiosInstance.get(`/cards/${cardId}`, {
352
- params: {
353
- attachments: true,
354
- checklists: 'all',
355
- checkItemStates: true,
356
- members: true,
357
- membersVoted: true,
358
- labels: true,
359
- actions: 'commentCard',
360
- actions_limit: 100,
361
- fields: 'all',
362
- customFieldItems: true,
363
- list: true,
364
- board: true,
365
- stickers: true,
366
- pluginData: true,
367
- },
368
- });
369
- const cardData = response.data;
370
- if (includeMarkdown) {
371
- return this.formatCardAsMarkdown(cardData);
372
- }
373
- return cardData;
374
- });
375
- }
376
- // Add Comment on Card
377
- async addCommentToCard(cardId, text) {
378
- return this.handleRequest(async () => {
379
- const response = await this.axiosInstance.post(`cards/${cardId}/actions/comments?text=${encodeURIComponent(text)}`);
380
- return response.data;
381
- });
382
- }
383
- // Update Comment
384
- async updateCommentOnCard(commentId, text) {
385
- return this.handleRequest(async () => {
386
- const response = await this.axiosInstance.put(`/actions/${commentId}?text=${encodeURIComponent(text)}`);
387
- if (response.status !== 200) {
388
- return false;
389
- }
390
- return true;
391
- });
392
- }
393
- // Delete Comment
394
- async deleteCommentFromCard(commentId) {
395
- return this.handleRequest(async () => {
396
- const response = await this.axiosInstance.delete(`/actions/${commentId}`);
397
- return response.status === 200;
398
- });
399
- }
400
- // Get Card Comments
401
- async getCardComments(cardId, limit = 100) {
402
- return this.handleRequest(async () => {
403
- const response = await this.axiosInstance.get(`/cards/${cardId}/actions`, {
404
- params: {
405
- filter: 'commentCard',
406
- limit: limit,
407
- },
408
- });
409
- return response.data;
410
- });
411
- }
412
- // Checklist methods
413
- async getChecklistItems(name, boardId) {
414
- const effectiveBoardId = boardId || this.activeConfig.boardId;
415
- if (!effectiveBoardId) {
416
- throw new McpError(ErrorCode.InvalidParams, 'No board ID provided and no active board set');
417
- }
418
- // Get all checklists from the board directly
419
- const response = await this.axiosInstance.get(`/boards/${effectiveBoardId}/checklists`);
420
- const allCheckItems = [];
421
- for (const checklist of response.data) {
422
- if (checklist.name.toLowerCase() === name.toLowerCase()) {
423
- const convertedItems = checklist.checkItems.map(item => this.convertToCheckListItem(item, checklist.id));
424
- allCheckItems.push(...convertedItems);
425
- }
426
- }
427
- return allCheckItems;
428
- }
429
- async addChecklistItem(text, checkListName, boardId) {
430
- const effectiveBoardId = boardId || this.activeConfig.boardId;
431
- if (!effectiveBoardId) {
432
- throw new McpError(ErrorCode.InvalidParams, 'No board ID provided and no active board set');
433
- }
434
- // Find the checklist by name using the more efficient endpoint
435
- const checklistsResponse = await this.axiosInstance.get(`/boards/${effectiveBoardId}/checklists`);
436
- const checklists = checklistsResponse.data;
437
- const targetChecklist = checklists.find(checklist => checklist.name.toLowerCase() === checkListName.toLowerCase());
438
- if (!targetChecklist) {
439
- throw new McpError(ErrorCode.InvalidParams, `Checklist "${checkListName}" not found`);
440
- }
441
- // Add the check item to the checklist
442
- const itemResponse = await this.axiosInstance.post(`/checklists/${targetChecklist.id}/checkItems`, {
443
- name: text,
444
- });
445
- return this.convertToCheckListItem(itemResponse.data, targetChecklist.id);
446
- }
447
- async findChecklistItemsByDescription(description, boardId) {
448
- const effectiveBoardId = boardId || this.activeConfig.boardId;
449
- if (!effectiveBoardId) {
450
- throw new McpError(ErrorCode.InvalidParams, 'No board ID provided and no active board set');
451
- }
452
- // Get all checklists from the board directly
453
- const response = await this.axiosInstance.get(`/boards/${effectiveBoardId}/checklists`);
454
- const matchingItems = [];
455
- const searchTerm = description.toLowerCase();
456
- for (const checklist of response.data) {
457
- for (const checkItem of checklist.checkItems) {
458
- if (checkItem.name.toLowerCase().includes(searchTerm)) {
459
- matchingItems.push(this.convertToCheckListItem(checkItem, checklist.id));
460
- }
461
- }
462
- }
463
- return matchingItems;
464
- }
465
- async getAcceptanceCriteria(boardId) {
466
- return this.getChecklistItems('Acceptance Criteria', boardId);
467
- }
468
- async getChecklistByName(name, boardId) {
469
- const effectiveBoardId = boardId || this.activeConfig.boardId;
470
- if (!effectiveBoardId) {
471
- throw new McpError(ErrorCode.InvalidParams, 'No board ID provided and no active board set');
472
- }
473
- // Get all checklists from the board directly
474
- const response = await this.axiosInstance.get(`/boards/${effectiveBoardId}/checklists`);
475
- const targetChecklist = response.data.find(checklist => checklist.name.toLowerCase() === name.toLowerCase());
476
- if (targetChecklist) {
477
- return this.convertToCheckList(targetChecklist);
478
- }
479
- return null;
480
- }
481
- formatCardAsMarkdown(card) {
482
- let markdown = '';
483
- // Title and basic info
484
- markdown += `# ${card.name}\n\n`;
485
- // Board and List context
486
- if (card.board && card.list) {
487
- markdown += `📍 **Board**: [${card.board.name}](${card.board.url}) > **List**: ${card.list.name}\n\n`;
488
- }
489
- // Labels
490
- if (card.labels && card.labels.length > 0) {
491
- markdown += `## 🏷️ Labels\n`;
492
- card.labels.forEach(label => {
493
- markdown += `- \`${label.color}\` ${label.name || '(no name)'}\n`;
494
- });
495
- markdown += '\n';
496
- }
497
- // Due date
498
- if (card.due) {
499
- const dueDate = new Date(card.due);
500
- const status = card.dueComplete ? '✅ Complete' : '⏰ Due';
501
- markdown += `## 📅 Due Date\n${status}: ${dueDate.toLocaleString()}\n\n`;
502
- }
503
- // Members
504
- if (card.members && card.members.length > 0) {
505
- markdown += `## 👥 Members\n`;
506
- card.members.forEach(member => {
507
- markdown += `- @${member.username} (${member.fullName})\n`;
508
- });
509
- markdown += '\n';
510
- }
511
- // Description
512
- if (card.desc) {
513
- markdown += `## 📝 Description\n`;
514
- markdown += `${card.desc}\n\n`;
515
- // Parse for inline images (Trello uses markdown-like syntax)
516
- // Look for patterns like ![alt text](image url)
517
- const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
518
- const images = card.desc.match(imageRegex);
519
- if (images) {
520
- markdown += `### Inline Images in Description\n`;
521
- images.forEach((img, index) => {
522
- const match = img.match(/!\[([^\]]*)\]\(([^)]+)\)/);
523
- if (match) {
524
- markdown += `${index + 1}. ${match[1] || 'Image'}: ${match[2]}\n`;
525
- }
526
- });
527
- markdown += '\n';
528
- }
529
- }
530
- // Checklists
531
- if (card.checklists && card.checklists.length > 0) {
532
- markdown += `## ✅ Checklists\n`;
533
- card.checklists.forEach(checklist => {
534
- const completed = checklist.checkItems.filter(item => item.state === 'complete').length;
535
- const total = checklist.checkItems.length;
536
- markdown += `### ${checklist.name} (${completed}/${total})\n`;
537
- // Sort by position
538
- const sortedItems = [...checklist.checkItems].sort((a, b) => a.pos - b.pos);
539
- sortedItems.forEach(item => {
540
- const checkbox = item.state === 'complete' ? '[x]' : '[ ]';
541
- markdown += `- ${checkbox} ${item.name}`;
542
- if (item.due) {
543
- const itemDue = new Date(item.due);
544
- markdown += ` (Due: ${itemDue.toLocaleDateString()})`;
545
- }
546
- if (item.idMember) {
547
- const member = card.members?.find(m => m.id === item.idMember);
548
- if (member) {
549
- markdown += ` - @${member.username}`;
550
- }
551
- }
552
- markdown += '\n';
553
- });
554
- markdown += '\n';
555
- });
556
- }
557
- // Attachments
558
- if (card.attachments && card.attachments.length > 0) {
559
- markdown += `## 📎 Attachments (${card.attachments.length})\n`;
560
- card.attachments.forEach((attachment, index) => {
561
- markdown += `### ${index + 1}. ${attachment.name}\n`;
562
- markdown += `- **URL**: ${attachment.url}\n`;
563
- if (attachment.fileName) {
564
- markdown += `- **File**: ${attachment.fileName}`;
565
- if (attachment.bytes) {
566
- const size = this.formatFileSize(attachment.bytes);
567
- markdown += ` (${size})`;
568
- }
569
- markdown += '\n';
570
- }
571
- if (attachment.mimeType) {
572
- markdown += `- **Type**: ${attachment.mimeType}\n`;
573
- }
574
- markdown += `- **Added**: ${new Date(attachment.date).toLocaleString()}\n`;
575
- // Image preview
576
- if (attachment.previews && attachment.previews.length > 0) {
577
- const preview = attachment.previews[0];
578
- markdown += `- **Preview**: ![${attachment.name}](${preview.url})\n`;
579
- }
580
- markdown += '\n';
581
- });
582
- }
583
- // Comments
584
- if (card.comments && card.comments.length > 0) {
585
- markdown += `## 💬 Comments (${card.comments.length})\n`;
586
- card.comments.forEach(comment => {
587
- const date = new Date(comment.date);
588
- markdown += `### ${comment.memberCreator.fullName} (@${comment.memberCreator.username}) - ${date.toLocaleString()}\n`;
589
- markdown += `${comment.data.text}\n\n`;
590
- });
591
- }
592
- // Statistics
593
- if (card.badges) {
594
- markdown += `## 📊 Statistics\n`;
595
- if (card.badges.checkItems > 0) {
596
- markdown += `- **Checklist Items**: ${card.badges.checkItemsChecked}/${card.badges.checkItems} completed\n`;
597
- }
598
- if (card.badges.comments > 0) {
599
- markdown += `- **Comments**: ${card.badges.comments}\n`;
600
- }
601
- if (card.badges.attachments > 0) {
602
- markdown += `- **Attachments**: ${card.badges.attachments}\n`;
603
- }
604
- if (card.badges.votes > 0) {
605
- markdown += `- **Votes**: ${card.badges.votes}\n`;
606
- }
607
- markdown += '\n';
608
- }
609
- // Links
610
- markdown += `## 🔗 Links\n`;
611
- markdown += `- **Card URL**: ${card.url}\n`;
612
- markdown += `- **Short URL**: ${card.shortUrl}\n\n`;
613
- // Metadata
614
- markdown += `---\n`;
615
- markdown += `*Last Activity: ${new Date(card.dateLastActivity).toLocaleString()}*\n`;
616
- markdown += `*Card ID: ${card.id}*\n`;
617
- return markdown;
618
- }
619
- formatFileSize(bytes) {
620
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
621
- if (bytes === 0)
622
- return '0 Bytes';
623
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
624
- return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i];
625
- }
626
- // Helper methods to convert between Trello types and MCP types
627
- convertToCheckListItem(trelloItem, parentCheckListId) {
628
- return {
629
- id: trelloItem.id,
630
- text: trelloItem.name,
631
- complete: trelloItem.state === 'complete',
632
- parentCheckListId,
633
- };
634
- }
635
- convertToCheckList(trelloChecklist) {
636
- const completedItems = trelloChecklist.checkItems.filter(item => item.state === 'complete').length;
637
- const totalItems = trelloChecklist.checkItems.length;
638
- const percentComplete = totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0;
639
- return {
640
- id: trelloChecklist.id,
641
- name: trelloChecklist.name,
642
- items: trelloChecklist.checkItems.map(item => this.convertToCheckListItem(item, trelloChecklist.id)),
643
- percentComplete,
644
- };
645
- }
646
- }
647
- const MIME_TYPES = Object.freeze({
648
- // Images
649
- '.jpg': 'image/jpeg',
650
- '.jpeg': 'image/jpeg',
651
- '.png': 'image/png',
652
- '.gif': 'image/gif',
653
- '.bmp': 'image/bmp',
654
- '.svg': 'image/svg+xml',
655
- '.webp': 'image/webp',
656
- '.ico': 'image/x-icon',
657
- // Documents
658
- '.pdf': 'application/pdf',
659
- '.doc': 'application/msword',
660
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
661
- '.xls': 'application/vnd.ms-excel',
662
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
663
- '.ppt': 'application/vnd.ms-powerpoint',
664
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
665
- // Text
666
- '.txt': 'text/plain',
667
- '.md': 'text/markdown',
668
- '.csv': 'text/csv',
669
- '.log': 'text/plain',
670
- // Code
671
- '.html': 'text/html',
672
- '.htm': 'text/html',
673
- '.css': 'text/css',
674
- '.js': 'application/javascript',
675
- '.mjs': 'application/javascript',
676
- '.ts': 'application/typescript',
677
- '.tsx': 'application/typescript',
678
- '.jsx': 'application/javascript',
679
- '.json': 'application/json',
680
- '.xml': 'application/xml',
681
- '.yaml': 'text/yaml',
682
- '.yml': 'text/yaml',
683
- // Archives
684
- '.zip': 'application/zip',
685
- '.tar': 'application/x-tar',
686
- '.gz': 'application/gzip',
687
- '.rar': 'application/vnd.rar',
688
- '.7z': 'application/x-7z-compressed',
689
- // Media
690
- '.mp3': 'audio/mpeg',
691
- '.wav': 'audio/wav',
692
- '.mp4': 'video/mp4',
693
- '.avi': 'video/x-msvideo',
694
- '.mov': 'video/quicktime',
695
- '.wmv': 'video/x-ms-wmv',
696
- '.flv': 'video/x-flv',
697
- '.webm': 'video/webm',
698
- });
699
- //# sourceMappingURL=trello-client.js.map