@littlebearapps/outlook-assistant 3.3.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 (52) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +422 -0
  4. package/advanced/index.js +652 -0
  5. package/auth/index.js +32 -0
  6. package/auth/oauth-server.js +233 -0
  7. package/auth/token-manager.js +105 -0
  8. package/auth/token-storage.js +359 -0
  9. package/auth/tools.js +159 -0
  10. package/calendar/accept.js +72 -0
  11. package/calendar/cancel.js +72 -0
  12. package/calendar/create.js +115 -0
  13. package/calendar/decline.js +72 -0
  14. package/calendar/delete.js +67 -0
  15. package/calendar/index.js +130 -0
  16. package/calendar/list.js +108 -0
  17. package/categories/index.js +955 -0
  18. package/config.js +95 -0
  19. package/contacts/index.js +754 -0
  20. package/email/attachments.js +365 -0
  21. package/email/conversations.js +666 -0
  22. package/email/delta.js +210 -0
  23. package/email/export.js +572 -0
  24. package/email/folder-utils.js +192 -0
  25. package/email/headers.js +344 -0
  26. package/email/index.js +537 -0
  27. package/email/list.js +136 -0
  28. package/email/mark-as-read.js +114 -0
  29. package/email/mime.js +286 -0
  30. package/email/read.js +161 -0
  31. package/email/search.js +628 -0
  32. package/email/send.js +169 -0
  33. package/folder/create.js +137 -0
  34. package/folder/delete.js +108 -0
  35. package/folder/index.js +112 -0
  36. package/folder/list.js +289 -0
  37. package/folder/move.js +186 -0
  38. package/folder/stats.js +322 -0
  39. package/index.js +162 -0
  40. package/llms.txt +76 -0
  41. package/outlook-auth-server.js +384 -0
  42. package/package.json +97 -0
  43. package/rules/create.js +273 -0
  44. package/rules/index.js +276 -0
  45. package/rules/list.js +216 -0
  46. package/settings/index.js +678 -0
  47. package/utils/field-presets.js +311 -0
  48. package/utils/graph-api.js +268 -0
  49. package/utils/mock-data.js +154 -0
  50. package/utils/odata-helpers.js +33 -0
  51. package/utils/response-formatter.js +457 -0
  52. package/utils/safety.js +123 -0
@@ -0,0 +1,955 @@
1
+ /**
2
+ * Categories module for Outlook Assistant server
3
+ *
4
+ * Manages Outlook master categories and Focused Inbox overrides.
5
+ */
6
+ const { callGraphAPI } = require('../utils/graph-api');
7
+ const { ensureAuthenticated } = require('../auth');
8
+
9
+ // Category color presets (Outlook uses these names)
10
+ const CATEGORY_COLORS = [
11
+ 'preset0',
12
+ 'preset1',
13
+ 'preset2',
14
+ 'preset3',
15
+ 'preset4',
16
+ 'preset5',
17
+ 'preset6',
18
+ 'preset7',
19
+ 'preset8',
20
+ 'preset9',
21
+ 'preset10',
22
+ 'preset11',
23
+ 'preset12',
24
+ 'preset13',
25
+ 'preset14',
26
+ 'preset15',
27
+ 'preset16',
28
+ 'preset17',
29
+ 'preset18',
30
+ 'preset19',
31
+ 'preset20',
32
+ 'preset21',
33
+ 'preset22',
34
+ 'preset23',
35
+ 'preset24',
36
+ ];
37
+
38
+ // Map preset numbers to human-readable colors
39
+ const COLOR_NAMES = {
40
+ preset0: 'Red',
41
+ preset1: 'Orange',
42
+ preset2: 'Brown',
43
+ preset3: 'Yellow',
44
+ preset4: 'Green',
45
+ preset5: 'Teal',
46
+ preset6: 'Olive',
47
+ preset7: 'Blue',
48
+ preset8: 'Purple',
49
+ preset9: 'Cranberry',
50
+ preset10: 'Steel',
51
+ preset11: 'DarkSteel',
52
+ preset12: 'Gray',
53
+ preset13: 'DarkGray',
54
+ preset14: 'Black',
55
+ preset15: 'DarkRed',
56
+ preset16: 'DarkOrange',
57
+ preset17: 'DarkBrown',
58
+ preset18: 'DarkYellow',
59
+ preset19: 'DarkGreen',
60
+ preset20: 'DarkTeal',
61
+ preset21: 'DarkOlive',
62
+ preset22: 'DarkBlue',
63
+ preset23: 'DarkPurple',
64
+ preset24: 'DarkCranberry',
65
+ };
66
+
67
+ /**
68
+ * Format a category for display
69
+ */
70
+ function formatCategory(category) {
71
+ const colorName = COLOR_NAMES[category.color] || category.color;
72
+ return {
73
+ id: category.id,
74
+ displayName: category.displayName,
75
+ color: category.color,
76
+ colorName: colorName,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * List master categories handler
82
+ */
83
+ async function handleListCategories(args) {
84
+ const outputVerbosity = args.outputVerbosity || 'standard';
85
+
86
+ try {
87
+ const accessToken = await ensureAuthenticated();
88
+
89
+ const response = await callGraphAPI(
90
+ accessToken,
91
+ 'GET',
92
+ 'me/outlook/masterCategories'
93
+ );
94
+
95
+ const categories = response.value || [];
96
+
97
+ if (categories.length === 0) {
98
+ return {
99
+ content: [
100
+ {
101
+ type: 'text',
102
+ text: 'No categories found. Use manage-category with action=create to create your first category.',
103
+ },
104
+ ],
105
+ };
106
+ }
107
+
108
+ // Format output based on verbosity
109
+ let output = [];
110
+ output.push(`# Master Categories (${categories.length})\n`);
111
+
112
+ if (outputVerbosity === 'minimal') {
113
+ output.push(categories.map((c) => `- ${c.displayName}`).join('\n'));
114
+ } else {
115
+ output.push('| Category | Color | ID |');
116
+ output.push('|----------|-------|-----|');
117
+ categories.forEach((cat) => {
118
+ const colorName = COLOR_NAMES[cat.color] || cat.color;
119
+ const idDisplay =
120
+ outputVerbosity === 'full' ? cat.id : cat.id.substring(0, 8) + '...';
121
+ output.push(`| ${cat.displayName} | ${colorName} | ${idDisplay} |`);
122
+ });
123
+ }
124
+
125
+ return {
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: output.join('\n'),
130
+ },
131
+ ],
132
+ _meta: {
133
+ count: categories.length,
134
+ categories: categories.map(formatCategory),
135
+ },
136
+ };
137
+ } catch (error) {
138
+ if (error.message === 'Authentication required') {
139
+ return {
140
+ content: [
141
+ {
142
+ type: 'text',
143
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
144
+ },
145
+ ],
146
+ };
147
+ }
148
+ return {
149
+ content: [
150
+ {
151
+ type: 'text',
152
+ text: `Error listing categories: ${error.message}`,
153
+ },
154
+ ],
155
+ };
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Create category handler
161
+ */
162
+ async function handleCreateCategory(args) {
163
+ const { displayName, color } = args;
164
+
165
+ if (!displayName) {
166
+ return {
167
+ content: [
168
+ {
169
+ type: 'text',
170
+ text: 'Category name (displayName) is required.',
171
+ },
172
+ ],
173
+ };
174
+ }
175
+
176
+ // Validate color if provided
177
+ if (color && !CATEGORY_COLORS.includes(color)) {
178
+ return {
179
+ content: [
180
+ {
181
+ type: 'text',
182
+ text: `Invalid color. Valid options: ${CATEGORY_COLORS.join(', ')}\n\nColor names: ${Object.entries(
183
+ COLOR_NAMES
184
+ )
185
+ .map(([k, v]) => `${k}=${v}`)
186
+ .join(', ')}`,
187
+ },
188
+ ],
189
+ };
190
+ }
191
+
192
+ try {
193
+ const accessToken = await ensureAuthenticated();
194
+
195
+ const categoryData = {
196
+ displayName: displayName,
197
+ color: color || 'preset0', // Default to red
198
+ };
199
+
200
+ const response = await callGraphAPI(
201
+ accessToken,
202
+ 'POST',
203
+ 'me/outlook/masterCategories',
204
+ categoryData
205
+ );
206
+
207
+ const colorName = COLOR_NAMES[response.color] || response.color;
208
+
209
+ return {
210
+ content: [
211
+ {
212
+ type: 'text',
213
+ text: `Category created!\n\n**Name**: ${response.displayName}\n**Color**: ${colorName} (${response.color})\n**ID**: ${response.id}`,
214
+ },
215
+ ],
216
+ _meta: {
217
+ category: formatCategory(response),
218
+ },
219
+ };
220
+ } catch (error) {
221
+ if (error.message === 'Authentication required') {
222
+ return {
223
+ content: [
224
+ {
225
+ type: 'text',
226
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
227
+ },
228
+ ],
229
+ };
230
+ }
231
+
232
+ if (error.message.includes('already exists')) {
233
+ return {
234
+ content: [
235
+ {
236
+ type: 'text',
237
+ text: `A category named "${displayName}" already exists.`,
238
+ },
239
+ ],
240
+ };
241
+ }
242
+
243
+ return {
244
+ content: [
245
+ {
246
+ type: 'text',
247
+ text: `Error creating category: ${error.message}`,
248
+ },
249
+ ],
250
+ };
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Update category handler
256
+ */
257
+ async function handleUpdateCategory(args) {
258
+ const { id, displayName, color } = args;
259
+
260
+ if (!id) {
261
+ return {
262
+ content: [
263
+ {
264
+ type: 'text',
265
+ text: 'Category ID is required. Use manage-category with action=list to find category IDs.',
266
+ },
267
+ ],
268
+ };
269
+ }
270
+
271
+ if (!displayName && !color) {
272
+ return {
273
+ content: [
274
+ {
275
+ type: 'text',
276
+ text: 'At least one of displayName or color must be provided.',
277
+ },
278
+ ],
279
+ };
280
+ }
281
+
282
+ // Validate color if provided
283
+ if (color && !CATEGORY_COLORS.includes(color)) {
284
+ return {
285
+ content: [
286
+ {
287
+ type: 'text',
288
+ text: `Invalid color. Valid options: ${CATEGORY_COLORS.join(', ')}`,
289
+ },
290
+ ],
291
+ };
292
+ }
293
+
294
+ try {
295
+ const accessToken = await ensureAuthenticated();
296
+
297
+ const updateData = {};
298
+ if (displayName) updateData.displayName = displayName;
299
+ if (color) updateData.color = color;
300
+
301
+ const response = await callGraphAPI(
302
+ accessToken,
303
+ 'PATCH',
304
+ `me/outlook/masterCategories/${id}`,
305
+ updateData
306
+ );
307
+
308
+ const colorName = COLOR_NAMES[response.color] || response.color;
309
+
310
+ return {
311
+ content: [
312
+ {
313
+ type: 'text',
314
+ text: `Category updated!\n\n**Name**: ${response.displayName}\n**Color**: ${colorName} (${response.color})\n**ID**: ${response.id}`,
315
+ },
316
+ ],
317
+ _meta: {
318
+ category: formatCategory(response),
319
+ },
320
+ };
321
+ } catch (error) {
322
+ if (error.message === 'Authentication required') {
323
+ return {
324
+ content: [
325
+ {
326
+ type: 'text',
327
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
328
+ },
329
+ ],
330
+ };
331
+ }
332
+ return {
333
+ content: [
334
+ {
335
+ type: 'text',
336
+ text: `Error updating category: ${error.message}`,
337
+ },
338
+ ],
339
+ };
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Delete category handler
345
+ */
346
+ async function handleDeleteCategory(args) {
347
+ const { id } = args;
348
+
349
+ if (!id) {
350
+ return {
351
+ content: [
352
+ {
353
+ type: 'text',
354
+ text: 'Category ID is required. Use manage-category with action=list to find category IDs.',
355
+ },
356
+ ],
357
+ };
358
+ }
359
+
360
+ try {
361
+ const accessToken = await ensureAuthenticated();
362
+
363
+ await callGraphAPI(
364
+ accessToken,
365
+ 'DELETE',
366
+ `me/outlook/masterCategories/${id}`
367
+ );
368
+
369
+ return {
370
+ content: [
371
+ {
372
+ type: 'text',
373
+ text: `Category deleted successfully.`,
374
+ },
375
+ ],
376
+ };
377
+ } catch (error) {
378
+ if (error.message === 'Authentication required') {
379
+ return {
380
+ content: [
381
+ {
382
+ type: 'text',
383
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
384
+ },
385
+ ],
386
+ };
387
+ }
388
+
389
+ if (error.message.includes('not found') || error.message.includes('404')) {
390
+ return {
391
+ content: [
392
+ {
393
+ type: 'text',
394
+ text: `Category not found. Use manage-category with action=list to see available categories.`,
395
+ },
396
+ ],
397
+ };
398
+ }
399
+
400
+ return {
401
+ content: [
402
+ {
403
+ type: 'text',
404
+ text: `Error deleting category: ${error.message}`,
405
+ },
406
+ ],
407
+ };
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Apply category to message(s) handler
413
+ */
414
+ async function handleApplyCategory(args) {
415
+ const { messageId, messageIds, categories, action } = args;
416
+
417
+ // Support single ID or array
418
+ const ids = messageIds || (messageId ? [messageId] : []);
419
+
420
+ if (ids.length === 0) {
421
+ return {
422
+ content: [
423
+ {
424
+ type: 'text',
425
+ text: 'Message ID (messageId) or IDs (messageIds) required.',
426
+ },
427
+ ],
428
+ };
429
+ }
430
+
431
+ if (!categories || !Array.isArray(categories) || categories.length === 0) {
432
+ return {
433
+ content: [
434
+ {
435
+ type: 'text',
436
+ text: 'Categories array is required. Provide category display names.',
437
+ },
438
+ ],
439
+ };
440
+ }
441
+
442
+ const applyAction = action || 'set'; // 'set', 'add', 'remove'
443
+
444
+ try {
445
+ const accessToken = await ensureAuthenticated();
446
+
447
+ const results = [];
448
+ const errors = [];
449
+
450
+ for (const id of ids) {
451
+ try {
452
+ let newCategories = categories;
453
+
454
+ // If adding or removing, get current categories first
455
+ if (applyAction === 'add' || applyAction === 'remove') {
456
+ const current = await callGraphAPI(
457
+ accessToken,
458
+ 'GET',
459
+ `me/messages/${id}`,
460
+ null,
461
+ { $select: 'categories' }
462
+ );
463
+
464
+ const currentCategories = current.categories || [];
465
+
466
+ if (applyAction === 'add') {
467
+ newCategories = [...new Set([...currentCategories, ...categories])];
468
+ } else if (applyAction === 'remove') {
469
+ newCategories = currentCategories.filter(
470
+ (c) => !categories.includes(c)
471
+ );
472
+ }
473
+ }
474
+
475
+ await callGraphAPI(accessToken, 'PATCH', `me/messages/${id}`, {
476
+ categories: newCategories,
477
+ });
478
+
479
+ results.push({ id, success: true, categories: newCategories });
480
+ } catch (err) {
481
+ errors.push({ id, error: err.message });
482
+ }
483
+ }
484
+
485
+ let output = [];
486
+
487
+ if (results.length > 0) {
488
+ output.push(
489
+ `Categories ${applyAction === 'remove' ? 'removed from' : 'applied to'} ${results.length} message(s)\n`
490
+ );
491
+
492
+ if (ids.length <= 5) {
493
+ results.forEach((r) => {
494
+ output.push(
495
+ `- ${r.id.substring(0, 20)}...: [${r.categories.join(', ')}]`
496
+ );
497
+ });
498
+ }
499
+ }
500
+
501
+ if (errors.length > 0) {
502
+ output.push(`\n${errors.length} error(s):`);
503
+ errors.forEach((e) => {
504
+ output.push(`- ${e.id.substring(0, 20)}...: ${e.error}`);
505
+ });
506
+ }
507
+
508
+ return {
509
+ content: [
510
+ {
511
+ type: 'text',
512
+ text: output.join('\n'),
513
+ },
514
+ ],
515
+ _meta: {
516
+ successful: results.length,
517
+ failed: errors.length,
518
+ action: applyAction,
519
+ results,
520
+ errors,
521
+ },
522
+ };
523
+ } catch (error) {
524
+ if (error.message === 'Authentication required') {
525
+ return {
526
+ content: [
527
+ {
528
+ type: 'text',
529
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
530
+ },
531
+ ],
532
+ };
533
+ }
534
+ return {
535
+ content: [
536
+ {
537
+ type: 'text',
538
+ text: `Error applying categories: ${error.message}`,
539
+ },
540
+ ],
541
+ };
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Get Focused Inbox overrides handler
547
+ */
548
+ async function handleGetFocusedInboxOverrides(args) {
549
+ const outputVerbosity = args.outputVerbosity || 'standard';
550
+
551
+ try {
552
+ const accessToken = await ensureAuthenticated();
553
+
554
+ const response = await callGraphAPI(
555
+ accessToken,
556
+ 'GET',
557
+ 'me/inferenceClassification/overrides'
558
+ );
559
+
560
+ const overrides = response.value || [];
561
+
562
+ if (overrides.length === 0) {
563
+ return {
564
+ content: [
565
+ {
566
+ type: 'text',
567
+ text: 'No Focused Inbox overrides configured.\n\nUse manage-focused-inbox with action=set to always show emails from specific senders in Focused or Other.',
568
+ },
569
+ ],
570
+ };
571
+ }
572
+
573
+ let output = [];
574
+ output.push(`# Focused Inbox Overrides (${overrides.length})\n`);
575
+
576
+ // Group by classification
577
+ const focused = overrides.filter((o) => o.classifyAs === 'focused');
578
+ const other = overrides.filter((o) => o.classifyAs === 'other');
579
+
580
+ if (focused.length > 0) {
581
+ output.push('## Always Focused');
582
+ focused.forEach((o) => {
583
+ const addr = o.senderEmailAddress;
584
+ if (outputVerbosity === 'minimal') {
585
+ output.push(`- ${addr.address}`);
586
+ } else {
587
+ output.push(`- **${addr.name || addr.address}** <${addr.address}>`);
588
+ if (outputVerbosity === 'full') {
589
+ output.push(` - ID: ${o.id}`);
590
+ }
591
+ }
592
+ });
593
+ output.push('');
594
+ }
595
+
596
+ if (other.length > 0) {
597
+ output.push('## Always Other');
598
+ other.forEach((o) => {
599
+ const addr = o.senderEmailAddress;
600
+ if (outputVerbosity === 'minimal') {
601
+ output.push(`- ${addr.address}`);
602
+ } else {
603
+ output.push(`- **${addr.name || addr.address}** <${addr.address}>`);
604
+ if (outputVerbosity === 'full') {
605
+ output.push(` - ID: ${o.id}`);
606
+ }
607
+ }
608
+ });
609
+ }
610
+
611
+ return {
612
+ content: [
613
+ {
614
+ type: 'text',
615
+ text: output.join('\n'),
616
+ },
617
+ ],
618
+ _meta: {
619
+ count: overrides.length,
620
+ focused: focused.length,
621
+ other: other.length,
622
+ overrides: overrides,
623
+ },
624
+ };
625
+ } catch (error) {
626
+ if (error.message === 'Authentication required') {
627
+ return {
628
+ content: [
629
+ {
630
+ type: 'text',
631
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
632
+ },
633
+ ],
634
+ };
635
+ }
636
+ return {
637
+ content: [
638
+ {
639
+ type: 'text',
640
+ text: `Error getting Focused Inbox overrides: ${error.message}`,
641
+ },
642
+ ],
643
+ };
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Set Focused Inbox override handler
649
+ */
650
+ async function handleSetFocusedInboxOverride(args) {
651
+ const { emailAddress, name, classifyAs } = args;
652
+ // Note: args.action is used by the consolidated dispatcher and also checked here for 'delete'
653
+ const overrideAction = args.action;
654
+
655
+ if (!emailAddress) {
656
+ return {
657
+ content: [
658
+ {
659
+ type: 'text',
660
+ text: 'Email address is required.',
661
+ },
662
+ ],
663
+ };
664
+ }
665
+
666
+ const classification = classifyAs || 'focused';
667
+ if (!['focused', 'other'].includes(classification)) {
668
+ return {
669
+ content: [
670
+ {
671
+ type: 'text',
672
+ text: "classifyAs must be 'focused' or 'other'.",
673
+ },
674
+ ],
675
+ };
676
+ }
677
+
678
+ try {
679
+ const accessToken = await ensureAuthenticated();
680
+
681
+ // Check if override already exists
682
+ const existingResponse = await callGraphAPI(
683
+ accessToken,
684
+ 'GET',
685
+ 'me/inferenceClassification/overrides'
686
+ );
687
+
688
+ const existing = (existingResponse.value || []).find(
689
+ (o) =>
690
+ o.senderEmailAddress.address.toLowerCase() ===
691
+ emailAddress.toLowerCase()
692
+ );
693
+
694
+ // Handle delete action
695
+ if (overrideAction === 'delete') {
696
+ if (!existing) {
697
+ return {
698
+ content: [
699
+ {
700
+ type: 'text',
701
+ text: `No override found for ${emailAddress}.`,
702
+ },
703
+ ],
704
+ };
705
+ }
706
+
707
+ await callGraphAPI(
708
+ accessToken,
709
+ 'DELETE',
710
+ `me/inferenceClassification/overrides/${existing.id}`
711
+ );
712
+
713
+ return {
714
+ content: [
715
+ {
716
+ type: 'text',
717
+ text: `Removed override for ${emailAddress}. Emails will now follow normal Focused Inbox rules.`,
718
+ },
719
+ ],
720
+ };
721
+ }
722
+
723
+ // Create or update override
724
+ const overrideData = {
725
+ classifyAs: classification,
726
+ senderEmailAddress: {
727
+ address: emailAddress,
728
+ name: name || emailAddress,
729
+ },
730
+ };
731
+
732
+ let response;
733
+ if (existing) {
734
+ // Update existing
735
+ response = await callGraphAPI(
736
+ accessToken,
737
+ 'PATCH',
738
+ `me/inferenceClassification/overrides/${existing.id}`,
739
+ overrideData
740
+ );
741
+ } else {
742
+ // Create new
743
+ response = await callGraphAPI(
744
+ accessToken,
745
+ 'POST',
746
+ 'me/inferenceClassification/overrides',
747
+ overrideData
748
+ );
749
+ }
750
+
751
+ const actionWord = existing ? 'Updated' : 'Created';
752
+ const destination =
753
+ classification === 'focused' ? 'Focused inbox' : 'Other';
754
+
755
+ return {
756
+ content: [
757
+ {
758
+ type: 'text',
759
+ text: `${actionWord} override!\n\nEmails from **${response.senderEmailAddress.name || emailAddress}** <${emailAddress}> will always go to **${destination}**.`,
760
+ },
761
+ ],
762
+ _meta: {
763
+ action: existing ? 'updated' : 'created',
764
+ override: response,
765
+ },
766
+ };
767
+ } catch (error) {
768
+ if (error.message === 'Authentication required') {
769
+ return {
770
+ content: [
771
+ {
772
+ type: 'text',
773
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
774
+ },
775
+ ],
776
+ };
777
+ }
778
+ return {
779
+ content: [
780
+ {
781
+ type: 'text',
782
+ text: `Error setting Focused Inbox override: ${error.message}`,
783
+ },
784
+ ],
785
+ };
786
+ }
787
+ }
788
+
789
+ // Consolidated tool definitions (7 → 3)
790
+ const categoriesTools = [
791
+ {
792
+ name: 'manage-category',
793
+ description:
794
+ 'Manage master categories. action=list (default) lists categories. action=create creates a category. action=update changes name/color. action=delete removes a category.',
795
+ annotations: {
796
+ title: 'Master Categories',
797
+ readOnlyHint: false,
798
+ destructiveHint: false,
799
+ openWorldHint: false,
800
+ },
801
+ inputSchema: {
802
+ type: 'object',
803
+ properties: {
804
+ action: {
805
+ type: 'string',
806
+ enum: ['list', 'create', 'update', 'delete'],
807
+ description: 'Action to perform (default: list)',
808
+ },
809
+ // list params
810
+ outputVerbosity: {
811
+ type: 'string',
812
+ enum: ['minimal', 'standard', 'full'],
813
+ description: 'Output detail level (action=list, default: standard)',
814
+ },
815
+ // create/update params
816
+ displayName: {
817
+ type: 'string',
818
+ description:
819
+ 'Category name (action=create required, action=update optional)',
820
+ },
821
+ color: {
822
+ type: 'string',
823
+ enum: CATEGORY_COLORS,
824
+ description:
825
+ 'Color preset, e.g. preset0=Red, preset7=Blue (action=create/update)',
826
+ },
827
+ // update/delete params
828
+ id: {
829
+ type: 'string',
830
+ description: 'Category ID (action=update/delete, required)',
831
+ },
832
+ },
833
+ required: [],
834
+ },
835
+ handler: async (args) => {
836
+ const action = args.action || 'list';
837
+ switch (action) {
838
+ case 'create':
839
+ return handleCreateCategory(args);
840
+ case 'update':
841
+ return handleUpdateCategory(args);
842
+ case 'delete':
843
+ return handleDeleteCategory(args);
844
+ case 'list':
845
+ default:
846
+ return handleListCategories(args);
847
+ }
848
+ },
849
+ },
850
+ {
851
+ name: 'apply-category',
852
+ description: 'Apply, add, or remove categories on email message(s)',
853
+ annotations: {
854
+ title: 'Apply Categories',
855
+ readOnlyHint: false,
856
+ destructiveHint: false,
857
+ openWorldHint: false,
858
+ },
859
+ inputSchema: {
860
+ type: 'object',
861
+ properties: {
862
+ messageId: {
863
+ type: 'string',
864
+ description: 'Single message ID to categorise',
865
+ },
866
+ messageIds: {
867
+ type: 'array',
868
+ items: { type: 'string' },
869
+ description: 'Array of message IDs to categorise (batch operation)',
870
+ },
871
+ categories: {
872
+ type: 'array',
873
+ items: { type: 'string' },
874
+ description: 'Category display names to apply/remove (required)',
875
+ },
876
+ action: {
877
+ type: 'string',
878
+ enum: ['set', 'add', 'remove'],
879
+ description:
880
+ 'set (replace all), add (append), remove (remove specific). Default: set',
881
+ },
882
+ },
883
+ required: ['categories'],
884
+ },
885
+ handler: handleApplyCategory,
886
+ },
887
+ {
888
+ name: 'manage-focused-inbox',
889
+ description:
890
+ 'Manage Focused Inbox overrides. action=list (default) shows overrides. action=set creates/updates an override. action=delete removes an override.',
891
+ annotations: {
892
+ title: 'Focused Inbox',
893
+ readOnlyHint: false,
894
+ destructiveHint: false,
895
+ openWorldHint: false,
896
+ },
897
+ inputSchema: {
898
+ type: 'object',
899
+ properties: {
900
+ action: {
901
+ type: 'string',
902
+ enum: ['list', 'set', 'delete'],
903
+ description: 'Action to perform (default: list)',
904
+ },
905
+ // list params
906
+ outputVerbosity: {
907
+ type: 'string',
908
+ enum: ['minimal', 'standard', 'full'],
909
+ description: 'Output detail level (action=list, default: standard)',
910
+ },
911
+ // set/delete params
912
+ emailAddress: {
913
+ type: 'string',
914
+ description: 'Sender email address (action=set/delete, required)',
915
+ },
916
+ name: {
917
+ type: 'string',
918
+ description: 'Sender display name (action=set)',
919
+ },
920
+ classifyAs: {
921
+ type: 'string',
922
+ enum: ['focused', 'other'],
923
+ description:
924
+ 'Where to put emails from this sender (action=set, default: focused)',
925
+ },
926
+ },
927
+ required: [],
928
+ },
929
+ handler: async (args) => {
930
+ const action = args.action || 'list';
931
+ switch (action) {
932
+ case 'set':
933
+ return handleSetFocusedInboxOverride(args);
934
+ case 'delete':
935
+ return handleSetFocusedInboxOverride(args);
936
+ case 'list':
937
+ default:
938
+ return handleGetFocusedInboxOverrides(args);
939
+ }
940
+ },
941
+ },
942
+ ];
943
+
944
+ module.exports = {
945
+ categoriesTools,
946
+ handleListCategories,
947
+ handleCreateCategory,
948
+ handleUpdateCategory,
949
+ handleDeleteCategory,
950
+ handleApplyCategory,
951
+ handleGetFocusedInboxOverrides,
952
+ handleSetFocusedInboxOverride,
953
+ CATEGORY_COLORS,
954
+ COLOR_NAMES,
955
+ };