@littlebearapps/outlook-assistant 3.7.1 → 3.7.4
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 +35 -18
- package/advanced/index.js +26 -3
- package/auth/token-storage.js +18 -6
- package/auth/tools.js +121 -4
- package/calendar/index.js +27 -1
- package/categories/index.js +67 -28
- package/contacts/index.js +118 -27
- package/email/attachments.js +13 -3
- package/email/conversations.js +4 -7
- package/email/delta.js +20 -6
- package/email/export.js +26 -3
- package/email/index.js +38 -4
- package/email/list.js +6 -1
- package/email/mail-tips.js +26 -2
- package/email/search.js +82 -25
- package/folder/create.js +6 -1
- package/folder/index.js +10 -1
- package/index.js +33 -0
- package/llms.txt +8 -2
- package/package.json +1 -1
- package/rules/create.js +5 -1
- package/rules/index.js +19 -1
- package/rules/update.js +11 -3
- package/settings/index.js +70 -2
- package/utils/response-formatter.js +27 -0
- package/utils/schema-coerce.js +248 -0
package/categories/index.js
CHANGED
|
@@ -116,9 +116,9 @@ async function handleListCategories(args) {
|
|
|
116
116
|
output.push('|----------|-------|-----|');
|
|
117
117
|
categories.forEach((cat) => {
|
|
118
118
|
const colorName = COLOR_NAMES[cat.color] || cat.color;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
output.push(`| ${cat.displayName} | ${colorName} | ${
|
|
119
|
+
// F-9: emit full IDs at standard verbosity. Truncated IDs were
|
|
120
|
+
// unusable downstream (the ellipsis became part of the copy).
|
|
121
|
+
output.push(`| ${cat.displayName} | ${colorName} | ${cat.id} |`);
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -255,7 +255,9 @@ async function handleCreateCategory(args) {
|
|
|
255
255
|
* Update category handler
|
|
256
256
|
*/
|
|
257
257
|
async function handleUpdateCategory(args) {
|
|
258
|
-
|
|
258
|
+
// F-34: accept `categoryId` as a deprecated alias for `id`.
|
|
259
|
+
const id = args.id || args.categoryId;
|
|
260
|
+
const { displayName, color } = args;
|
|
259
261
|
|
|
260
262
|
if (!id) {
|
|
261
263
|
return {
|
|
@@ -298,34 +300,45 @@ async function handleUpdateCategory(args) {
|
|
|
298
300
|
if (displayName) updateData.displayName = displayName;
|
|
299
301
|
if (color) updateData.color = color;
|
|
300
302
|
|
|
301
|
-
|
|
303
|
+
await callGraphAPI(
|
|
302
304
|
accessToken,
|
|
303
305
|
'PATCH',
|
|
304
306
|
`me/outlook/masterCategories/${id}`,
|
|
305
307
|
updateData
|
|
306
308
|
);
|
|
307
309
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
310
|
+
// F-35: Graph silently drops master-category color updates on
|
|
311
|
+
// some account types. Re-fetch the category and diff requested
|
|
312
|
+
// values against what Graph actually stored, so the caller
|
|
313
|
+
// doesn't get a misleading "Category updated!" when nothing
|
|
314
|
+
// changed.
|
|
315
|
+
const fresh = await callGraphAPI(
|
|
316
|
+
accessToken,
|
|
317
|
+
'GET',
|
|
318
|
+
`me/outlook/masterCategories/${id}`
|
|
319
|
+
);
|
|
312
320
|
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
321
|
+
const warnings = [];
|
|
322
|
+
if (color && fresh.color !== color) {
|
|
323
|
+
warnings.push(
|
|
324
|
+
`Requested color \`${color}\` but Graph stored \`${fresh.color}\` (master-category colors may be immutable on this account type).`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (displayName && fresh.displayName !== displayName) {
|
|
328
|
+
warnings.push(
|
|
329
|
+
`Requested name \`${displayName}\` but Graph stored \`${fresh.displayName}\`.`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const colorName = COLOR_NAMES[fresh.color] || fresh.color;
|
|
334
|
+
let text = `Category updated!\n\n**Name**: ${fresh.displayName}\n**Color**: ${colorName} (${fresh.color})\n**ID**: ${fresh.id || id}`;
|
|
335
|
+
if (warnings.length > 0) {
|
|
336
|
+
text += `\n\n**⚠ Warning**:\n${warnings.map((w) => `- ${w}`).join('\n')}`;
|
|
337
|
+
}
|
|
318
338
|
|
|
319
339
|
return {
|
|
320
|
-
content: [
|
|
321
|
-
|
|
322
|
-
type: 'text',
|
|
323
|
-
text: `Category updated!\n\n**Name**: ${updatedName}\n**Color**: ${colorName} (${updatedColor})\n**ID**: ${response.id || id}`,
|
|
324
|
-
},
|
|
325
|
-
],
|
|
326
|
-
_meta: {
|
|
327
|
-
category: formatCategory(updatedCategory),
|
|
328
|
-
},
|
|
340
|
+
content: [{ type: 'text', text }],
|
|
341
|
+
_meta: { category: formatCategory(fresh) },
|
|
329
342
|
};
|
|
330
343
|
} catch (error) {
|
|
331
344
|
if (error.message === 'Authentication required') {
|
|
@@ -353,7 +366,8 @@ async function handleUpdateCategory(args) {
|
|
|
353
366
|
* Delete category handler
|
|
354
367
|
*/
|
|
355
368
|
async function handleDeleteCategory(args) {
|
|
356
|
-
|
|
369
|
+
// F-34: accept `categoryId` as a deprecated alias for `id`.
|
|
370
|
+
const id = args.id || args.categoryId;
|
|
357
371
|
|
|
358
372
|
if (!id) {
|
|
359
373
|
return {
|
|
@@ -824,8 +838,9 @@ const categoriesTools = [
|
|
|
824
838
|
properties: {
|
|
825
839
|
action: {
|
|
826
840
|
type: 'string',
|
|
827
|
-
enum: ['list', 'create', 'update', 'delete'],
|
|
828
|
-
description:
|
|
841
|
+
enum: ['list', 'create', 'update', 'set', 'delete'],
|
|
842
|
+
description:
|
|
843
|
+
"Action to perform (default: list). 'set' is a deprecated alias for 'update'.",
|
|
829
844
|
},
|
|
830
845
|
// list params
|
|
831
846
|
outputVerbosity: {
|
|
@@ -850,7 +865,12 @@ const categoriesTools = [
|
|
|
850
865
|
type: 'string',
|
|
851
866
|
description: 'Category ID (action=update/delete, required)',
|
|
852
867
|
},
|
|
868
|
+
categoryId: {
|
|
869
|
+
type: 'string',
|
|
870
|
+
description: 'DEPRECATED: alias for `id`. Will be removed in v3.8.0.',
|
|
871
|
+
},
|
|
853
872
|
},
|
|
873
|
+
additionalProperties: false,
|
|
854
874
|
required: [],
|
|
855
875
|
},
|
|
856
876
|
handler: async (args) => {
|
|
@@ -858,13 +878,22 @@ const categoriesTools = [
|
|
|
858
878
|
switch (action) {
|
|
859
879
|
case 'create':
|
|
860
880
|
return handleCreateCategory(args);
|
|
881
|
+
case 'set': // deprecated alias
|
|
861
882
|
case 'update':
|
|
862
883
|
return handleUpdateCategory(args);
|
|
863
884
|
case 'delete':
|
|
864
885
|
return handleDeleteCategory(args);
|
|
865
886
|
case 'list':
|
|
866
|
-
default:
|
|
867
887
|
return handleListCategories(args);
|
|
888
|
+
default:
|
|
889
|
+
return {
|
|
890
|
+
content: [
|
|
891
|
+
{
|
|
892
|
+
type: 'text',
|
|
893
|
+
text: `Unknown action '${action}'. Valid actions: list, create, update, delete.`,
|
|
894
|
+
},
|
|
895
|
+
],
|
|
896
|
+
};
|
|
868
897
|
}
|
|
869
898
|
},
|
|
870
899
|
},
|
|
@@ -901,6 +930,7 @@ const categoriesTools = [
|
|
|
901
930
|
'set (replace all), add (append), remove (remove specific). Default: set',
|
|
902
931
|
},
|
|
903
932
|
},
|
|
933
|
+
additionalProperties: false,
|
|
904
934
|
required: ['categories'],
|
|
905
935
|
},
|
|
906
936
|
handler: handleApplyCategory,
|
|
@@ -945,6 +975,7 @@ const categoriesTools = [
|
|
|
945
975
|
'Where to put emails from this sender (action=set, default: focused)',
|
|
946
976
|
},
|
|
947
977
|
},
|
|
978
|
+
additionalProperties: false,
|
|
948
979
|
required: [],
|
|
949
980
|
},
|
|
950
981
|
handler: async (args) => {
|
|
@@ -955,8 +986,16 @@ const categoriesTools = [
|
|
|
955
986
|
case 'delete':
|
|
956
987
|
return handleSetFocusedInboxOverride(args);
|
|
957
988
|
case 'list':
|
|
958
|
-
default:
|
|
959
989
|
return handleGetFocusedInboxOverrides(args);
|
|
990
|
+
default:
|
|
991
|
+
return {
|
|
992
|
+
content: [
|
|
993
|
+
{
|
|
994
|
+
type: 'text',
|
|
995
|
+
text: `Unknown action '${action}'. Valid actions: list, set, delete.`,
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
};
|
|
960
999
|
}
|
|
961
1000
|
},
|
|
962
1001
|
},
|
package/contacts/index.js
CHANGED
|
@@ -76,13 +76,10 @@ function formatContact(contact, verbosity = 'standard') {
|
|
|
76
76
|
lines.push(`**Phone**: ${phones.join(' | ')}`);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.join(' at ');
|
|
84
|
-
lines.push(`**Company**: ${company}`);
|
|
85
|
-
}
|
|
79
|
+
// Job title and company info — F-40: previously squashed into a
|
|
80
|
+
// single 'Company' label which mislabeled jobTitle-only contacts.
|
|
81
|
+
if (contact.jobTitle) lines.push(`**Job Title**: ${contact.jobTitle}`);
|
|
82
|
+
if (contact.companyName) lines.push(`**Company**: ${contact.companyName}`);
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
// Full verbosity extras
|
|
@@ -130,6 +127,8 @@ async function handleListContacts(args) {
|
|
|
130
127
|
const verbosity = args.outputVerbosity || 'standard';
|
|
131
128
|
const folder = args.folder || null; // null = default contacts folder
|
|
132
129
|
|
|
130
|
+
const skip = args.skip || 0;
|
|
131
|
+
|
|
133
132
|
try {
|
|
134
133
|
const accessToken = await ensureAuthenticated();
|
|
135
134
|
|
|
@@ -142,7 +141,9 @@ async function handleListContacts(args) {
|
|
|
142
141
|
$select: fields.join(','),
|
|
143
142
|
$top: count,
|
|
144
143
|
$orderby: 'displayName',
|
|
144
|
+
$count: 'true', // Surface true total so callers know if more pages exist
|
|
145
145
|
};
|
|
146
|
+
if (skip > 0) queryParams.$skip = skip;
|
|
146
147
|
|
|
147
148
|
const response = await callGraphAPI(
|
|
148
149
|
accessToken,
|
|
@@ -152,10 +153,29 @@ async function handleListContacts(args) {
|
|
|
152
153
|
queryParams
|
|
153
154
|
);
|
|
154
155
|
const contacts = response.value || [];
|
|
156
|
+
const totalAvailable = response['@odata.count'];
|
|
157
|
+
const hasMore = Boolean(response['@odata.nextLink']);
|
|
155
158
|
|
|
156
159
|
const output = [];
|
|
157
160
|
output.push(`# Contacts\n`);
|
|
158
|
-
|
|
161
|
+
if (typeof totalAvailable === 'number') {
|
|
162
|
+
output.push(
|
|
163
|
+
`**Showing**: ${contacts.length} of ${totalAvailable}${skip > 0 ? ` (offset ${skip})` : ''}`
|
|
164
|
+
);
|
|
165
|
+
} else {
|
|
166
|
+
output.push(`**Showing**: ${contacts.length}`);
|
|
167
|
+
}
|
|
168
|
+
// F-22: surface pagination cue when more results exist so callers
|
|
169
|
+
// know to ask for the next page instead of assuming "Total: 50"
|
|
170
|
+
// is the entire address book.
|
|
171
|
+
if (
|
|
172
|
+
hasMore ||
|
|
173
|
+
(totalAvailable && contacts.length + skip < totalAvailable)
|
|
174
|
+
) {
|
|
175
|
+
output.push(
|
|
176
|
+
`**More available**: pass \`skip: ${skip + contacts.length}\` to fetch the next page (or \`count\` to raise the page size up to 100).`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
159
179
|
output.push('');
|
|
160
180
|
|
|
161
181
|
contacts.forEach((contact) => {
|
|
@@ -164,7 +184,13 @@ async function handleListContacts(args) {
|
|
|
164
184
|
|
|
165
185
|
return {
|
|
166
186
|
content: [{ type: 'text', text: output.join('\n') }],
|
|
167
|
-
_meta: {
|
|
187
|
+
_meta: {
|
|
188
|
+
count: contacts.length,
|
|
189
|
+
...(typeof totalAvailable === 'number' && {
|
|
190
|
+
totalAvailable,
|
|
191
|
+
}),
|
|
192
|
+
hasMore,
|
|
193
|
+
},
|
|
168
194
|
};
|
|
169
195
|
} catch (error) {
|
|
170
196
|
if (error.message === 'Authentication required') {
|
|
@@ -340,13 +366,32 @@ async function handleGetContact(args) {
|
|
|
340
366
|
* Create contact handler
|
|
341
367
|
*/
|
|
342
368
|
async function handleCreateContact(args) {
|
|
343
|
-
const {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
369
|
+
const {
|
|
370
|
+
displayName,
|
|
371
|
+
firstName,
|
|
372
|
+
lastName,
|
|
373
|
+
email,
|
|
374
|
+
emails,
|
|
375
|
+
mobilePhone,
|
|
376
|
+
companyName,
|
|
377
|
+
jobTitle,
|
|
378
|
+
notes,
|
|
379
|
+
} = args;
|
|
380
|
+
|
|
381
|
+
// F-39: derive displayName from firstName/lastName if not provided.
|
|
382
|
+
const resolvedDisplayName =
|
|
383
|
+
displayName ||
|
|
384
|
+
[firstName, lastName].filter(Boolean).join(' ') ||
|
|
385
|
+
email ||
|
|
386
|
+
(Array.isArray(emails) && emails[0]);
|
|
387
|
+
|
|
388
|
+
if (!resolvedDisplayName && !email && !(emails && emails.length > 0)) {
|
|
347
389
|
return {
|
|
348
390
|
content: [
|
|
349
|
-
{
|
|
391
|
+
{
|
|
392
|
+
type: 'text',
|
|
393
|
+
text: 'At least displayName, firstName/lastName, email, or emails is required.',
|
|
394
|
+
},
|
|
350
395
|
],
|
|
351
396
|
};
|
|
352
397
|
}
|
|
@@ -356,24 +401,39 @@ async function handleCreateContact(args) {
|
|
|
356
401
|
|
|
357
402
|
const contactData = {};
|
|
358
403
|
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
404
|
+
if (resolvedDisplayName) contactData.displayName = resolvedDisplayName;
|
|
405
|
+
|
|
406
|
+
// F-39: prefer explicit givenName/surname when supplied; otherwise
|
|
407
|
+
// derive from displayName if it's a multi-word string.
|
|
408
|
+
if (firstName) contactData.givenName = firstName;
|
|
409
|
+
if (lastName) contactData.surname = lastName;
|
|
410
|
+
if (
|
|
411
|
+
!contactData.givenName &&
|
|
412
|
+
!contactData.surname &&
|
|
413
|
+
displayName &&
|
|
414
|
+
displayName.includes(' ')
|
|
415
|
+
) {
|
|
416
|
+
const parts = displayName.split(' ');
|
|
417
|
+
contactData.givenName = parts[0];
|
|
418
|
+
contactData.surname = parts.slice(1).join(' ');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Email: accept either `email` (single) or `emails` (array).
|
|
422
|
+
const allEmails = [];
|
|
423
|
+
if (email) allEmails.push(email);
|
|
424
|
+
if (Array.isArray(emails)) allEmails.push(...emails);
|
|
425
|
+
if (allEmails.length > 0) {
|
|
426
|
+
contactData.emailAddresses = allEmails.map((addr) => ({
|
|
427
|
+
address: addr,
|
|
428
|
+
name: resolvedDisplayName || addr,
|
|
429
|
+
}));
|
|
364
430
|
}
|
|
431
|
+
|
|
365
432
|
if (mobilePhone) contactData.mobilePhone = mobilePhone;
|
|
366
433
|
if (companyName) contactData.companyName = companyName;
|
|
367
434
|
if (jobTitle) contactData.jobTitle = jobTitle;
|
|
368
435
|
if (notes) contactData.personalNotes = notes;
|
|
369
436
|
|
|
370
|
-
// Parse name into given/surname if provided
|
|
371
|
-
if (displayName && displayName.includes(' ')) {
|
|
372
|
-
const parts = displayName.split(' ');
|
|
373
|
-
contactData.givenName = parts[0];
|
|
374
|
-
contactData.surname = parts.slice(1).join(' ');
|
|
375
|
-
}
|
|
376
|
-
|
|
377
437
|
const contact = await callGraphAPI(
|
|
378
438
|
accessToken,
|
|
379
439
|
'POST',
|
|
@@ -640,6 +700,11 @@ const contactsTools = [
|
|
|
640
700
|
description:
|
|
641
701
|
'Number of results (action=list default: 50, action=search default: 25)',
|
|
642
702
|
},
|
|
703
|
+
skip: {
|
|
704
|
+
type: 'integer',
|
|
705
|
+
description:
|
|
706
|
+
'Pagination offset for action=list (default: 0). Use the value suggested by the previous page response.',
|
|
707
|
+
},
|
|
643
708
|
folder: {
|
|
644
709
|
type: 'string',
|
|
645
710
|
description: 'Contact folder ID (action=list)',
|
|
@@ -666,10 +731,26 @@ const contactsTools = [
|
|
|
666
731
|
type: 'string',
|
|
667
732
|
description: 'Full name (action=create/update)',
|
|
668
733
|
},
|
|
734
|
+
firstName: {
|
|
735
|
+
type: 'string',
|
|
736
|
+
description:
|
|
737
|
+
'Given name (action=create/update). Maps to Graph `givenName`. If displayName not provided, will be combined with lastName.',
|
|
738
|
+
},
|
|
739
|
+
lastName: {
|
|
740
|
+
type: 'string',
|
|
741
|
+
description:
|
|
742
|
+
'Surname (action=create/update). Maps to Graph `surname`.',
|
|
743
|
+
},
|
|
669
744
|
email: {
|
|
670
745
|
type: 'string',
|
|
671
746
|
description: 'Primary email address (action=create/update)',
|
|
672
747
|
},
|
|
748
|
+
emails: {
|
|
749
|
+
type: 'array',
|
|
750
|
+
items: { type: 'string' },
|
|
751
|
+
description:
|
|
752
|
+
'Multiple email addresses (action=create/update). First entry is primary.',
|
|
753
|
+
},
|
|
673
754
|
mobilePhone: {
|
|
674
755
|
type: 'string',
|
|
675
756
|
description: 'Mobile phone number (action=create/update)',
|
|
@@ -687,6 +768,7 @@ const contactsTools = [
|
|
|
687
768
|
description: 'Personal notes (action=create/update)',
|
|
688
769
|
},
|
|
689
770
|
},
|
|
771
|
+
additionalProperties: false,
|
|
690
772
|
required: [],
|
|
691
773
|
},
|
|
692
774
|
handler: async (args) => {
|
|
@@ -703,8 +785,16 @@ const contactsTools = [
|
|
|
703
785
|
case 'delete':
|
|
704
786
|
return handleDeleteContact(args);
|
|
705
787
|
case 'list':
|
|
706
|
-
default:
|
|
707
788
|
return handleListContacts(args);
|
|
789
|
+
default:
|
|
790
|
+
return {
|
|
791
|
+
content: [
|
|
792
|
+
{
|
|
793
|
+
type: 'text',
|
|
794
|
+
text: `Unknown action '${action}'. Valid actions: list, search, get, create, update, delete.`,
|
|
795
|
+
},
|
|
796
|
+
],
|
|
797
|
+
};
|
|
708
798
|
}
|
|
709
799
|
},
|
|
710
800
|
},
|
|
@@ -729,6 +819,7 @@ const contactsTools = [
|
|
|
729
819
|
description: 'Maximum results to return (default: 25, max: 50)',
|
|
730
820
|
},
|
|
731
821
|
},
|
|
822
|
+
additionalProperties: false,
|
|
732
823
|
required: ['query'],
|
|
733
824
|
},
|
|
734
825
|
handler: handleSearchPeople,
|
package/email/attachments.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const _https = require('https'); // Reserved for future use
|
|
6
6
|
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
7
8
|
const path = require('path');
|
|
8
9
|
const _config = require('../config'); // Reserved for future use
|
|
9
10
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
@@ -110,7 +111,12 @@ async function handleListAttachments(args) {
|
|
|
110
111
|
* @returns {object} - MCP response with download result
|
|
111
112
|
*/
|
|
112
113
|
async function handleDownloadAttachment(args) {
|
|
113
|
-
|
|
114
|
+
// F-19: accept both `outputDir` (canonical) and `savePath` (legacy
|
|
115
|
+
// alias). Previously the silent-ignore-unknown-param behaviour
|
|
116
|
+
// dropped `outputDir` and fell through to cwd, polluting the source
|
|
117
|
+
// tree with downloaded files.
|
|
118
|
+
const { messageId, attachmentId } = args;
|
|
119
|
+
const savePath = args.outputDir || args.savePath;
|
|
114
120
|
|
|
115
121
|
if (!messageId || !attachmentId) {
|
|
116
122
|
return {
|
|
@@ -167,8 +173,12 @@ async function handleDownloadAttachment(args) {
|
|
|
167
173
|
};
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
// Determine save location
|
|
171
|
-
|
|
176
|
+
// Determine save location. F-19: default to os.tmpdir() instead
|
|
177
|
+
// of cwd so attachments don't silently land in the source tree
|
|
178
|
+
// when the caller forgets to pass outputDir. Auto-create the
|
|
179
|
+
// target directory.
|
|
180
|
+
const outputDir = savePath || os.tmpdir();
|
|
181
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
172
182
|
const outputPath = path.join(outputDir, filename);
|
|
173
183
|
|
|
174
184
|
// Decode base64 and save to file
|
package/email/conversations.js
CHANGED
|
@@ -372,7 +372,10 @@ async function handleGetConversation(args) {
|
|
|
372
372
|
async function handleExportConversation(args) {
|
|
373
373
|
const conversationId = args.conversationId;
|
|
374
374
|
const format = (args.format || 'markdown').toLowerCase();
|
|
375
|
-
|
|
375
|
+
// F-29: default outputDir to os.tmpdir() to match target=message
|
|
376
|
+
// behaviour. Previously conversation export rejected calls without
|
|
377
|
+
// outputDir, inconsistent with the other export targets.
|
|
378
|
+
const outputDir = args.outputDir || require('os').tmpdir();
|
|
376
379
|
const _includeAttachments = args.includeAttachments !== false;
|
|
377
380
|
const order = args.order || 'chronological';
|
|
378
381
|
|
|
@@ -382,12 +385,6 @@ async function handleExportConversation(args) {
|
|
|
382
385
|
};
|
|
383
386
|
}
|
|
384
387
|
|
|
385
|
-
if (!outputDir) {
|
|
386
|
-
return {
|
|
387
|
-
content: [{ type: 'text', text: 'Output directory is required.' }],
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
388
|
const validFormats = ['eml', 'mbox', 'markdown', 'json', 'html', 'csv'];
|
|
392
389
|
if (!validFormats.includes(format)) {
|
|
393
390
|
return {
|
package/email/delta.js
CHANGED
|
@@ -90,6 +90,11 @@ async function handleListEmailsDelta(args) {
|
|
|
90
90
|
const isInitialSync = !deltaToken;
|
|
91
91
|
const hasMoreChanges = Boolean(nextLink);
|
|
92
92
|
const newDeltaToken = deltaLink || nextLink;
|
|
93
|
+
// F-15: nextLink is a continuation token (more pages of the same
|
|
94
|
+
// sync), not a delta token. The real delta token only emits once
|
|
95
|
+
// the initial sync finishes paging. Distinguish them in output so
|
|
96
|
+
// callers know what they're storing.
|
|
97
|
+
const tokenIsContinuation = !deltaLink && Boolean(nextLink);
|
|
93
98
|
|
|
94
99
|
// Format output based on verbosity
|
|
95
100
|
let resultText;
|
|
@@ -101,7 +106,10 @@ async function handleListEmailsDelta(args) {
|
|
|
101
106
|
resultText += `| Type | ${isInitialSync ? 'Initial' : 'Incremental'} |\n`;
|
|
102
107
|
resultText += `| More | ${hasMoreChanges ? 'Yes' : 'No'} |\n`;
|
|
103
108
|
if (newDeltaToken) {
|
|
104
|
-
|
|
109
|
+
const label = tokenIsContinuation
|
|
110
|
+
? 'Continuation Token (more pages — call again to keep paging)'
|
|
111
|
+
: 'Delta Token (save for next sync call)';
|
|
112
|
+
resultText += `\n**${label}**:\n\`\`\`\n${newDeltaToken}\n\`\`\`\n`;
|
|
105
113
|
}
|
|
106
114
|
} else {
|
|
107
115
|
resultText = `## Delta Sync ${isInitialSync ? '(Initial)' : '(Incremental)'}\n\n`;
|
|
@@ -142,14 +150,19 @@ async function handleListEmailsDelta(args) {
|
|
|
142
150
|
|
|
143
151
|
// Pagination info
|
|
144
152
|
if (hasMoreChanges) {
|
|
145
|
-
resultText += `\n### More
|
|
146
|
-
resultText += `
|
|
153
|
+
resultText += `\n### More Pages Available\n`;
|
|
154
|
+
resultText += `This page returned a continuation token. Call \`search-emails deltaMode=true deltaToken=<token>\` again to fetch the next page. The real delta token only emits once paging completes.\n`;
|
|
147
155
|
}
|
|
148
156
|
|
|
149
|
-
//
|
|
157
|
+
// Token (delta or continuation)
|
|
150
158
|
if (newDeltaToken) {
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
if (tokenIsContinuation) {
|
|
160
|
+
resultText += `\n### Continuation Token\n`;
|
|
161
|
+
resultText += `**More pages remain. Pass this back to keep paging:**\n\`\`\`\n${newDeltaToken}\n\`\`\`\n`;
|
|
162
|
+
} else {
|
|
163
|
+
resultText += `\n### Delta Token\n`;
|
|
164
|
+
resultText += `**Save this token for next sync call:**\n\`\`\`\n${newDeltaToken}\n\`\`\`\n`;
|
|
165
|
+
}
|
|
153
166
|
}
|
|
154
167
|
}
|
|
155
168
|
|
|
@@ -167,6 +180,7 @@ async function handleListEmailsDelta(args) {
|
|
|
167
180
|
hasMoreChanges: hasMoreChanges,
|
|
168
181
|
changesSummary: changesSummary,
|
|
169
182
|
deltaToken: newDeltaToken,
|
|
183
|
+
tokenType: tokenIsContinuation ? 'continuation' : 'delta',
|
|
170
184
|
},
|
|
171
185
|
};
|
|
172
186
|
} catch (error) {
|
package/email/export.js
CHANGED
|
@@ -37,7 +37,10 @@ const EXPORT_FORMATS = {
|
|
|
37
37
|
async function handleExportEmail(args) {
|
|
38
38
|
const emailId = args.id;
|
|
39
39
|
const format = (args.format || EXPORT_FORMATS.MARKDOWN).toLowerCase();
|
|
40
|
-
|
|
40
|
+
// F-27: accept `outputDir` (canonical) and `savePath` (legacy alias).
|
|
41
|
+
// Previously single-message exports ignored outputDir entirely and
|
|
42
|
+
// hardcoded os.tmpdir(), inconsistent with target=messages.
|
|
43
|
+
const savePath = args.outputDir || args.savePath;
|
|
41
44
|
const includeAttachments = args.includeAttachments !== false;
|
|
42
45
|
|
|
43
46
|
if (!emailId) {
|
|
@@ -116,17 +119,31 @@ async function handleExportEmail(args) {
|
|
|
116
119
|
} else if (format === EXPORT_FORMATS.CSV) {
|
|
117
120
|
// CSV export - email metadata
|
|
118
121
|
content = formatEmailsAsCSV(email);
|
|
122
|
+
} else if (format === 'mbox' || format === 'html') {
|
|
123
|
+
// F-26: clarify that mbox/html are conversation-only formats so
|
|
124
|
+
// callers don't infer the format itself is unsupported.
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: `Format '${format}' is only supported for target=conversation. For target=message use one of: ${Object.values(EXPORT_FORMATS).join(', ')}.`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
119
133
|
} else {
|
|
120
134
|
return {
|
|
121
135
|
content: [
|
|
122
136
|
{
|
|
123
137
|
type: 'text',
|
|
124
|
-
text: `Unknown format: ${format}. Supported: ${Object.values(EXPORT_FORMATS).join(', ')}
|
|
138
|
+
text: `Unknown format: ${format}. Supported for target=message: ${Object.values(EXPORT_FORMATS).join(', ')}.`,
|
|
125
139
|
},
|
|
126
140
|
],
|
|
127
141
|
};
|
|
128
142
|
}
|
|
129
143
|
|
|
144
|
+
// Auto-create the parent directory so callers don't have to pre-mkdir.
|
|
145
|
+
fs.mkdirSync(path.dirname(finalPath), { recursive: true });
|
|
146
|
+
|
|
130
147
|
// Save main file
|
|
131
148
|
fs.writeFileSync(finalPath, content, 'utf8');
|
|
132
149
|
|
|
@@ -207,7 +224,13 @@ async function handleExportEmail(args) {
|
|
|
207
224
|
*/
|
|
208
225
|
async function handleBatchExportEmails(args) {
|
|
209
226
|
const emailIds = args.emailIds || [];
|
|
210
|
-
|
|
227
|
+
// F-28: accept `query` as a top-level string alias for
|
|
228
|
+
// `searchQuery: { subject }`. Lets callers use the same `query`
|
|
229
|
+
// word they already know from search-emails.
|
|
230
|
+
const searchQuery = { ...(args.searchQuery || {}) };
|
|
231
|
+
if (args.query && !searchQuery.subject) {
|
|
232
|
+
searchQuery.subject = args.query;
|
|
233
|
+
}
|
|
211
234
|
const format = (args.format || EXPORT_FORMATS.MARKDOWN).toLowerCase();
|
|
212
235
|
const outputDir = args.outputDir;
|
|
213
236
|
const includeAttachments = args.includeAttachments === true; // Default false for batch
|