@littlebearapps/outlook-assistant 3.13.0 → 3.14.1
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/.env.example +30 -3
- package/README.md +67 -27
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/oauth-server.js +7 -1
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +28 -30
- package/auth/tools.js +61 -82
- package/calendar/attendees.js +36 -0
- package/calendar/cancel.js +9 -25
- package/calendar/create.js +42 -48
- package/calendar/decline.js +10 -25
- package/calendar/delete.js +10 -25
- package/calendar/index.js +20 -37
- package/calendar/list.js +4 -16
- package/calendar/preview.js +461 -0
- package/calendar/update.js +55 -83
- package/categories/index.js +68 -265
- package/config.js +29 -1
- package/contacts/index.js +72 -128
- package/email/attachments.js +43 -125
- package/email/conversations.js +44 -78
- package/email/delta.js +69 -46
- package/email/draft.js +170 -103
- package/email/export.js +145 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +86 -110
- package/email/list.js +4 -17
- package/email/mail-tips.js +86 -57
- package/email/mark-as-read.js +13 -49
- package/email/mime.js +39 -51
- package/email/read.js +16 -50
- package/email/search.js +47 -87
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +19 -17
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +18 -27
- package/index.js +39 -45
- package/llms-install.md +22 -4
- package/llms.txt +20 -11
- package/outlook-auth-server.js +10 -3
- package/package.json +4 -1
- package/request-handler.js +217 -116
- package/rules/create.js +28 -71
- package/rules/index.js +52 -93
- package/rules/list.js +7 -19
- package/rules/rule-builder.js +59 -22
- package/rules/update.js +27 -61
- package/server.js +41 -0
- package/settings/index.js +162 -145
- package/tools.js +30 -0
- package/utils/field-presets.js +4 -2
- package/utils/graph-api.js +65 -22
- package/utils/logger.js +251 -0
- package/utils/mock-data.js +91 -2
- package/utils/read-only.js +59 -0
- package/utils/response-formatter.js +54 -15
- package/utils/risk-classes.js +324 -0
- package/utils/safe-write.js +372 -6
- package/utils/safety.js +247 -42
- package/utils/server-instructions.js +73 -0
- package/utils/tool-error.js +33 -0
package/settings/index.js
CHANGED
|
@@ -6,6 +6,29 @@
|
|
|
6
6
|
*/
|
|
7
7
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
8
8
|
const { ensureAuthenticated } = require('../auth');
|
|
9
|
+
const { toolMetadata } = require('../utils/risk-classes');
|
|
10
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
11
|
+
const { dryRunResult, dryRunUnsupported } = require('../utils/safety');
|
|
12
|
+
const { DEFAULT_TIMEZONE } = require('../config');
|
|
13
|
+
const { toUtcIso, formatLocal } = require('../calendar/list');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A Graph dateTimeTimeZone as the UTC instant plus a labelled local time in
|
|
17
|
+
* the display timezone, e.g. "2026-10-05T01:45:00.000Z (5 Oct 2026, 12:45 pm
|
|
18
|
+
* GMT+11:00)" (#304). A zone Graph returns that can't be converted is shown
|
|
19
|
+
* as given, with its zone.
|
|
20
|
+
* @param {{dateTime: string, timeZone?: string}} dtz
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function formatScheduleTime(dtz) {
|
|
24
|
+
try {
|
|
25
|
+
const utc = toUtcIso(dtz);
|
|
26
|
+
const local = formatLocal(utc, DEFAULT_TIMEZONE);
|
|
27
|
+
return local ? `${utc} (${local})` : utc;
|
|
28
|
+
} catch (_error) {
|
|
29
|
+
return `${dtz?.dateTime} (${dtz?.timeZone || 'UTC'})`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
9
32
|
|
|
10
33
|
// Days of the week for working hours
|
|
11
34
|
const DAYS_OF_WEEK = [
|
|
@@ -62,12 +85,12 @@ function formatAutomaticReplies(settings) {
|
|
|
62
85
|
if (settings.status !== 'disabled') {
|
|
63
86
|
if (settings.scheduledStartDateTime) {
|
|
64
87
|
lines.push(
|
|
65
|
-
`**Scheduled Start**: ${
|
|
88
|
+
`**Scheduled Start**: ${formatScheduleTime(settings.scheduledStartDateTime)}`
|
|
66
89
|
);
|
|
67
90
|
}
|
|
68
91
|
if (settings.scheduledEndDateTime) {
|
|
69
92
|
lines.push(
|
|
70
|
-
`**Scheduled End**: ${
|
|
93
|
+
`**Scheduled End**: ${formatScheduleTime(settings.scheduledEndDateTime)}`
|
|
71
94
|
);
|
|
72
95
|
}
|
|
73
96
|
|
|
@@ -91,6 +114,86 @@ function formatAutomaticReplies(settings) {
|
|
|
91
114
|
return lines.join('\n');
|
|
92
115
|
}
|
|
93
116
|
|
|
117
|
+
/** Who gets the external reply, for each externalAudience value. */
|
|
118
|
+
const EXTERNAL_AUDIENCE_LABELS = {
|
|
119
|
+
none: 'nobody',
|
|
120
|
+
contactsOnly: 'only senders in your contacts',
|
|
121
|
+
all: 'all external senders',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Longest stretch of a reply message quoted in a preview. */
|
|
125
|
+
const REPLY_PREVIEW_CHARS = 100;
|
|
126
|
+
|
|
127
|
+
/** "get a 25-character reply: "…"" (or no message) for a preview. */
|
|
128
|
+
function describeReply(message, unchanged) {
|
|
129
|
+
const tag = unchanged ? ' (unchanged)' : '';
|
|
130
|
+
if (!message) return `— no reply message is set${tag}`;
|
|
131
|
+
const quoted =
|
|
132
|
+
message.length > REPLY_PREVIEW_CHARS
|
|
133
|
+
? `${message.substring(0, REPLY_PREVIEW_CHARS)}…`
|
|
134
|
+
: message;
|
|
135
|
+
return `get a ${message.length}-character reply${tag}: "${quoted}"`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** "scheduled, from A to B (UTC)" */
|
|
139
|
+
function describeSchedule(start, end) {
|
|
140
|
+
if (!start?.dateTime || !end?.dateTime) return 'scheduled';
|
|
141
|
+
return `scheduled, from ${formatScheduleTime(start)} to ${formatScheduleTime(end)}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* dryRun preview for set-auto-replies (#274): who would get an automatic
|
|
146
|
+
* reply, when, and how long each message is. Fields the call doesn't set
|
|
147
|
+
* keep their current values, so those are read first and marked unchanged.
|
|
148
|
+
* @param {object} changes - The automaticRepliesSetting the call would PATCH
|
|
149
|
+
* @param {object} current - The current automaticRepliesSetting
|
|
150
|
+
*/
|
|
151
|
+
function previewAutomaticReplies(changes, current) {
|
|
152
|
+
const effective = { ...current, ...changes };
|
|
153
|
+
const unchanged = (field) => !(field in changes);
|
|
154
|
+
const lines = [];
|
|
155
|
+
|
|
156
|
+
const status = effective.status;
|
|
157
|
+
const statusTag = unchanged('status') ? ' (unchanged)' : '';
|
|
158
|
+
if (status === 'disabled') {
|
|
159
|
+
lines.push(
|
|
160
|
+
`Status: off (disabled)${statusTag}. Nobody gets an automatic reply.`
|
|
161
|
+
);
|
|
162
|
+
return dryRunResult(lines, { settings: effective });
|
|
163
|
+
}
|
|
164
|
+
if (status === 'alwaysEnabled') {
|
|
165
|
+
lines.push(`Status: on now, with no end date (alwaysEnabled)${statusTag}.`);
|
|
166
|
+
} else {
|
|
167
|
+
lines.push(
|
|
168
|
+
`Status: ${describeSchedule(effective.scheduledStartDateTime, effective.scheduledEndDateTime)}${statusTag}.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
lines.push(
|
|
173
|
+
`Internal senders (your organisation): ${describeReply(effective.internalReplyMessage, unchanged('internalReplyMessage'))}`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const audience = effective.externalAudience;
|
|
177
|
+
const audienceTag = `externalAudience=${audience || 'unknown'}${unchanged('externalAudience') ? '; unchanged' : ''}`;
|
|
178
|
+
if (audience === 'none') {
|
|
179
|
+
lines.push(`External senders: nobody (${audienceTag}).`);
|
|
180
|
+
} else {
|
|
181
|
+
const who = EXTERNAL_AUDIENCE_LABELS[audience] || 'unknown audience';
|
|
182
|
+
lines.push(
|
|
183
|
+
`External senders: ${who} (${audienceTag}) ${describeReply(effective.externalReplyMessage, unchanged('externalReplyMessage'))}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (changes.status === 'alwaysEnabled') {
|
|
188
|
+
lines.push(
|
|
189
|
+
'',
|
|
190
|
+
'Note: Personal Outlook.com accounts only support scheduled replies, and Graph may leave them off. Pass `startDateTime` + `endDateTime` there instead.'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return dryRunResult(lines, { settings: effective });
|
|
195
|
+
}
|
|
196
|
+
|
|
94
197
|
/**
|
|
95
198
|
* Get mailbox settings handler
|
|
96
199
|
*/
|
|
@@ -189,23 +292,9 @@ async function handleGetMailboxSettings(args) {
|
|
|
189
292
|
};
|
|
190
293
|
} catch (error) {
|
|
191
294
|
if (error.message === 'Authentication required') {
|
|
192
|
-
return
|
|
193
|
-
content: [
|
|
194
|
-
{
|
|
195
|
-
type: 'text',
|
|
196
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
};
|
|
295
|
+
return authRequiredError();
|
|
200
296
|
}
|
|
201
|
-
return {
|
|
202
|
-
content: [
|
|
203
|
-
{
|
|
204
|
-
type: 'text',
|
|
205
|
-
text: `Error getting mailbox settings: ${error.message}`,
|
|
206
|
-
},
|
|
207
|
-
],
|
|
208
|
-
};
|
|
297
|
+
return toolError(`Error getting mailbox settings: ${error.message}`);
|
|
209
298
|
}
|
|
210
299
|
}
|
|
211
300
|
|
|
@@ -220,6 +309,7 @@ async function handleSetAutomaticReplies(args) {
|
|
|
220
309
|
internalReplyMessage,
|
|
221
310
|
externalReplyMessage,
|
|
222
311
|
externalAudience,
|
|
312
|
+
dryRun = false,
|
|
223
313
|
} = args;
|
|
224
314
|
|
|
225
315
|
try {
|
|
@@ -273,14 +363,9 @@ async function handleSetAutomaticReplies(args) {
|
|
|
273
363
|
// External audience
|
|
274
364
|
if (externalAudience) {
|
|
275
365
|
if (!['none', 'contactsOnly', 'all'].includes(externalAudience)) {
|
|
276
|
-
return
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
type: 'text',
|
|
280
|
-
text: "externalAudience must be 'none', 'contactsOnly', or 'all'.",
|
|
281
|
-
},
|
|
282
|
-
],
|
|
283
|
-
};
|
|
366
|
+
return toolError(
|
|
367
|
+
"externalAudience must be 'none', 'contactsOnly', or 'all'."
|
|
368
|
+
);
|
|
284
369
|
}
|
|
285
370
|
settings.externalAudience = externalAudience;
|
|
286
371
|
}
|
|
@@ -290,14 +375,20 @@ async function handleSetAutomaticReplies(args) {
|
|
|
290
375
|
// externalAudience without enabled/scheduled — previously the wrapper
|
|
291
376
|
// announced "Automatic replies updated!" with no actual state change.
|
|
292
377
|
if (Object.keys(settings).length === 0) {
|
|
293
|
-
return
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
378
|
+
return toolError(
|
|
379
|
+
'No automatic-reply settings were provided. To change state, pass `enabled: true|false` or `startDateTime` + `endDateTime`. To update messages or audience, pass `internalReplyMessage`, `externalReplyMessage`, or `externalAudience`.'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// dryRun: read the current setting to fill in what this call leaves
|
|
384
|
+
// alone, and say who would get a reply; change nothing.
|
|
385
|
+
if (dryRun) {
|
|
386
|
+
const current = await callGraphAPI(
|
|
387
|
+
accessToken,
|
|
388
|
+
'GET',
|
|
389
|
+
'me/mailboxSettings/automaticRepliesSetting'
|
|
390
|
+
);
|
|
391
|
+
return previewAutomaticReplies(settings, current || {});
|
|
301
392
|
}
|
|
302
393
|
|
|
303
394
|
// Apply settings
|
|
@@ -377,23 +468,9 @@ async function handleSetAutomaticReplies(args) {
|
|
|
377
468
|
};
|
|
378
469
|
} catch (error) {
|
|
379
470
|
if (error.message === 'Authentication required') {
|
|
380
|
-
return
|
|
381
|
-
content: [
|
|
382
|
-
{
|
|
383
|
-
type: 'text',
|
|
384
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
385
|
-
},
|
|
386
|
-
],
|
|
387
|
-
};
|
|
471
|
+
return authRequiredError();
|
|
388
472
|
}
|
|
389
|
-
return {
|
|
390
|
-
content: [
|
|
391
|
-
{
|
|
392
|
-
type: 'text',
|
|
393
|
-
text: `Error setting automatic replies: ${error.message}`,
|
|
394
|
-
},
|
|
395
|
-
],
|
|
396
|
-
};
|
|
473
|
+
return toolError(`Error setting automatic replies: ${error.message}`);
|
|
397
474
|
}
|
|
398
475
|
}
|
|
399
476
|
|
|
@@ -405,37 +482,22 @@ async function handleSetWorkingHours(args) {
|
|
|
405
482
|
|
|
406
483
|
// Validate inputs
|
|
407
484
|
if (!startTime && !endTime && !daysOfWeek && !timeZone) {
|
|
408
|
-
return
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
type: 'text',
|
|
412
|
-
text: 'At least one of startTime, endTime, daysOfWeek, or timeZone is required.',
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
};
|
|
485
|
+
return toolError(
|
|
486
|
+
'At least one of startTime, endTime, daysOfWeek, or timeZone is required.'
|
|
487
|
+
);
|
|
416
488
|
}
|
|
417
489
|
|
|
418
490
|
// Validate time format (HH:MM or HH:MM:SS)
|
|
419
491
|
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/;
|
|
420
492
|
if (startTime && !timeRegex.test(startTime)) {
|
|
421
|
-
return
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
type: 'text',
|
|
425
|
-
text: "startTime must be in HH:MM or HH:MM:SS format (e.g., '09:00' or '09:00:00').",
|
|
426
|
-
},
|
|
427
|
-
],
|
|
428
|
-
};
|
|
493
|
+
return toolError(
|
|
494
|
+
"startTime must be in HH:MM or HH:MM:SS format (e.g., '09:00' or '09:00:00')."
|
|
495
|
+
);
|
|
429
496
|
}
|
|
430
497
|
if (endTime && !timeRegex.test(endTime)) {
|
|
431
|
-
return
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
type: 'text',
|
|
435
|
-
text: "endTime must be in HH:MM or HH:MM:SS format (e.g., '17:00' or '17:00:00').",
|
|
436
|
-
},
|
|
437
|
-
],
|
|
438
|
-
};
|
|
498
|
+
return toolError(
|
|
499
|
+
"endTime must be in HH:MM or HH:MM:SS format (e.g., '17:00' or '17:00:00')."
|
|
500
|
+
);
|
|
439
501
|
}
|
|
440
502
|
|
|
441
503
|
// Validate days of week
|
|
@@ -444,14 +506,9 @@ async function handleSetWorkingHours(args) {
|
|
|
444
506
|
(d) => !DAYS_OF_WEEK.includes(d.toLowerCase())
|
|
445
507
|
);
|
|
446
508
|
if (invalidDays.length > 0) {
|
|
447
|
-
return
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
type: 'text',
|
|
451
|
-
text: `Invalid days: ${invalidDays.join(', ')}. Valid days: ${DAYS_OF_WEEK.join(', ')}`,
|
|
452
|
-
},
|
|
453
|
-
],
|
|
454
|
-
};
|
|
509
|
+
return toolError(
|
|
510
|
+
`Invalid days: ${invalidDays.join(', ')}. Valid days: ${DAYS_OF_WEEK.join(', ')}`
|
|
511
|
+
);
|
|
455
512
|
}
|
|
456
513
|
}
|
|
457
514
|
|
|
@@ -506,23 +563,9 @@ async function handleSetWorkingHours(args) {
|
|
|
506
563
|
};
|
|
507
564
|
} catch (error) {
|
|
508
565
|
if (error.message === 'Authentication required') {
|
|
509
|
-
return
|
|
510
|
-
content: [
|
|
511
|
-
{
|
|
512
|
-
type: 'text',
|
|
513
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
514
|
-
},
|
|
515
|
-
],
|
|
516
|
-
};
|
|
566
|
+
return authRequiredError();
|
|
517
567
|
}
|
|
518
|
-
return {
|
|
519
|
-
content: [
|
|
520
|
-
{
|
|
521
|
-
type: 'text',
|
|
522
|
-
text: `Error setting working hours: ${error.message}`,
|
|
523
|
-
},
|
|
524
|
-
],
|
|
525
|
-
};
|
|
568
|
+
return toolError(`Error setting working hours: ${error.message}`);
|
|
526
569
|
}
|
|
527
570
|
}
|
|
528
571
|
|
|
@@ -548,23 +591,9 @@ async function handleGetAutomaticReplies() {
|
|
|
548
591
|
};
|
|
549
592
|
} catch (error) {
|
|
550
593
|
if (error.message === 'Authentication required') {
|
|
551
|
-
return
|
|
552
|
-
content: [
|
|
553
|
-
{
|
|
554
|
-
type: 'text',
|
|
555
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
556
|
-
},
|
|
557
|
-
],
|
|
558
|
-
};
|
|
594
|
+
return authRequiredError();
|
|
559
595
|
}
|
|
560
|
-
return {
|
|
561
|
-
content: [
|
|
562
|
-
{
|
|
563
|
-
type: 'text',
|
|
564
|
-
text: `Error getting automatic replies: ${error.message}`,
|
|
565
|
-
},
|
|
566
|
-
],
|
|
567
|
-
};
|
|
596
|
+
return toolError(`Error getting automatic replies: ${error.message}`);
|
|
568
597
|
}
|
|
569
598
|
}
|
|
570
599
|
|
|
@@ -590,23 +619,9 @@ async function handleGetWorkingHours() {
|
|
|
590
619
|
};
|
|
591
620
|
} catch (error) {
|
|
592
621
|
if (error.message === 'Authentication required') {
|
|
593
|
-
return
|
|
594
|
-
content: [
|
|
595
|
-
{
|
|
596
|
-
type: 'text',
|
|
597
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
598
|
-
},
|
|
599
|
-
],
|
|
600
|
-
};
|
|
622
|
+
return authRequiredError();
|
|
601
623
|
}
|
|
602
|
-
return {
|
|
603
|
-
content: [
|
|
604
|
-
{
|
|
605
|
-
type: 'text',
|
|
606
|
-
text: `Error getting working hours: ${error.message}`,
|
|
607
|
-
},
|
|
608
|
-
],
|
|
609
|
-
};
|
|
624
|
+
return toolError(`Error getting working hours: ${error.message}`);
|
|
610
625
|
}
|
|
611
626
|
}
|
|
612
627
|
|
|
@@ -615,13 +630,8 @@ const settingsTools = [
|
|
|
615
630
|
{
|
|
616
631
|
name: 'mailbox-settings',
|
|
617
632
|
description:
|
|
618
|
-
'Read or update mailbox-level settings (idempotent — safe to retry; sets are PATCH-style and merge with existing state). action=`get` (default) returns settings — use `section` to filter (`language`, `timeZone`, `workingHours`, `automaticRepliesSetting`, or `all`). action=`set-auto-replies` configures out-of-office: `enabled` true/false, optional `startDateTime`/`endDateTime` (ISO 8601) for scheduled mode, `internalReplyMessage` and (optionally) `externalReplyMessage
|
|
619
|
-
|
|
620
|
-
title: 'Mailbox Settings',
|
|
621
|
-
readOnlyHint: false,
|
|
622
|
-
destructiveHint: false,
|
|
623
|
-
idempotentHint: true,
|
|
624
|
-
},
|
|
633
|
+
'Read or update mailbox-level settings (idempotent — safe to retry; sets are PATCH-style and merge with existing state). action=`get` (default) returns settings — use `section` to filter (`language`, `timeZone`, `workingHours`, `automaticRepliesSetting`, or `all`). action=`set-auto-replies` configures out-of-office: `enabled` true/false, optional `startDateTime`/`endDateTime` (ISO 8601) for scheduled mode, `internalReplyMessage` and (optionally) `externalReplyMessage` and `externalAudience` (none/contactsOnly/all); pass `dryRun: true` to preview who would get replies without changing anything. action=`set-working-hours` updates the schedule: `startTime`/`endTime` (HH:MM) and `daysOfWeek` (array of `monday`..`sunday`). Returns the updated settings object on set actions.',
|
|
634
|
+
...toolMetadata('mailbox-settings', 'Mailbox Settings'),
|
|
625
635
|
inputSchema: {
|
|
626
636
|
type: 'object',
|
|
627
637
|
properties: {
|
|
@@ -674,6 +684,11 @@ const settingsTools = [
|
|
|
674
684
|
enum: ['none', 'contactsOnly', 'all'],
|
|
675
685
|
description: 'Who receives external reply (action=set-auto-replies)',
|
|
676
686
|
},
|
|
687
|
+
dryRun: {
|
|
688
|
+
type: 'boolean',
|
|
689
|
+
description:
|
|
690
|
+
'Preview only (action=set-auto-replies): nothing is changed. Shows who would get automatic replies, the schedule and each message length. Other actions refuse dryRun and change nothing. Default false.',
|
|
691
|
+
},
|
|
677
692
|
// set-working-hours params
|
|
678
693
|
startTime: {
|
|
679
694
|
type: 'string',
|
|
@@ -705,6 +720,13 @@ const settingsTools = [
|
|
|
705
720
|
},
|
|
706
721
|
handler: async (args) => {
|
|
707
722
|
const action = args.action || 'get';
|
|
723
|
+
if (args.dryRun && action !== 'set-auto-replies') {
|
|
724
|
+
return dryRunUnsupported(
|
|
725
|
+
'mailbox-settings',
|
|
726
|
+
action,
|
|
727
|
+
'set-auto-replies'
|
|
728
|
+
);
|
|
729
|
+
}
|
|
708
730
|
switch (action) {
|
|
709
731
|
case 'set-auto-replies':
|
|
710
732
|
return handleSetAutomaticReplies(args);
|
|
@@ -713,14 +735,9 @@ const settingsTools = [
|
|
|
713
735
|
case 'get':
|
|
714
736
|
return handleGetMailboxSettings(args);
|
|
715
737
|
default:
|
|
716
|
-
return
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
type: 'text',
|
|
720
|
-
text: `Unknown action '${action}'. Valid actions: get, set-auto-replies, set-working-hours.`,
|
|
721
|
-
},
|
|
722
|
-
],
|
|
723
|
-
};
|
|
738
|
+
return toolError(
|
|
739
|
+
`Unknown action '${action}'. Valid actions: get, set-auto-replies, set-working-hours.`
|
|
740
|
+
);
|
|
724
741
|
}
|
|
725
742
|
},
|
|
726
743
|
},
|
package/tools.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry: every tool the server exposes, in listing order.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from index.js so tests can load the real tool list without
|
|
5
|
+
* starting the stdio transport. Add a new module's tools here (and classify
|
|
6
|
+
* them in utils/risk-classes.js).
|
|
7
|
+
*/
|
|
8
|
+
const { authTools } = require('./auth');
|
|
9
|
+
const { calendarTools } = require('./calendar');
|
|
10
|
+
const { emailTools } = require('./email');
|
|
11
|
+
const { folderTools } = require('./folder');
|
|
12
|
+
const { rulesTools } = require('./rules');
|
|
13
|
+
const { contactsTools } = require('./contacts');
|
|
14
|
+
const { categoriesTools } = require('./categories');
|
|
15
|
+
const { settingsTools } = require('./settings');
|
|
16
|
+
const { advancedTools } = require('./advanced');
|
|
17
|
+
|
|
18
|
+
const TOOLS = [
|
|
19
|
+
...authTools,
|
|
20
|
+
...calendarTools,
|
|
21
|
+
...emailTools,
|
|
22
|
+
...folderTools,
|
|
23
|
+
...rulesTools,
|
|
24
|
+
...contactsTools,
|
|
25
|
+
...categoriesTools,
|
|
26
|
+
...settingsTools,
|
|
27
|
+
...advancedTools,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
module.exports = { TOOLS };
|
package/utils/field-presets.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* and reduce response size/token usage.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const { log } = require('./logger');
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Field presets for different use cases
|
|
10
12
|
*/
|
|
@@ -255,7 +257,7 @@ const FOLDER_FIELDS = {
|
|
|
255
257
|
function getEmailFields(preset = 'list') {
|
|
256
258
|
const fields = FIELD_PRESETS[preset];
|
|
257
259
|
if (!fields) {
|
|
258
|
-
|
|
260
|
+
log.debug(`Unknown preset: ${preset}, falling back to 'list'`);
|
|
259
261
|
return FIELD_PRESETS.list.join(',');
|
|
260
262
|
}
|
|
261
263
|
return fields.join(',');
|
|
@@ -269,7 +271,7 @@ function getEmailFields(preset = 'list') {
|
|
|
269
271
|
function getFolderFields(preset = 'basic') {
|
|
270
272
|
const fields = FOLDER_FIELDS[preset];
|
|
271
273
|
if (!fields) {
|
|
272
|
-
|
|
274
|
+
log.debug(`Unknown folder preset: ${preset}, falling back to 'basic'`);
|
|
273
275
|
return FOLDER_FIELDS.basic.join(',');
|
|
274
276
|
}
|
|
275
277
|
return fields.join(',');
|
package/utils/graph-api.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const https = require('https');
|
|
5
5
|
const config = require('../config');
|
|
6
6
|
const mockData = require('./mock-data');
|
|
7
|
+
const { log, graphPathShape } = require('./logger');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Guard for caller-supplied full URLs (nextLink/deltaLink continuations).
|
|
@@ -270,7 +271,7 @@ function sleep(ms) {
|
|
|
270
271
|
* final response (2xx, or the last non-retried error status)
|
|
271
272
|
* @throws {Error} The network error of the final attempt
|
|
272
273
|
*/
|
|
273
|
-
async function
|
|
274
|
+
async function sendWithRetry(request) {
|
|
274
275
|
const method = String(request.method).toUpperCase();
|
|
275
276
|
const timeoutMs = request.timeoutMs || config.REQUEST_TIMEOUT_MS;
|
|
276
277
|
let networkRetryUsed = false;
|
|
@@ -316,7 +317,8 @@ async function requestWithRetry(request) {
|
|
|
316
317
|
}
|
|
317
318
|
}
|
|
318
319
|
|
|
319
|
-
|
|
320
|
+
log.increment('graphRetries');
|
|
321
|
+
log.debug(
|
|
320
322
|
`[GRAPH-API] ${method} ${networkError ? networkError.code : response.status}; ` +
|
|
321
323
|
`retry ${retry + 1}/${MAX_RETRIES} in ${delayMs} ms`
|
|
322
324
|
);
|
|
@@ -325,6 +327,36 @@ async function requestWithRetry(request) {
|
|
|
325
327
|
}
|
|
326
328
|
}
|
|
327
329
|
|
|
330
|
+
/**
|
|
331
|
+
* sendWithRetry, noting a final failure on the current tool call's log line
|
|
332
|
+
* as status (or network error code), method and a PII-free path shape (#278).
|
|
333
|
+
* @param {object} request - See sendWithRetry
|
|
334
|
+
* @returns {Promise<{status: number, headers: object, text: string}>}
|
|
335
|
+
*/
|
|
336
|
+
async function requestWithRetry(request) {
|
|
337
|
+
const method = String(request.method).toUpperCase();
|
|
338
|
+
try {
|
|
339
|
+
const response = await sendWithRetry(request);
|
|
340
|
+
if (response.status >= 400) {
|
|
341
|
+
log.note(
|
|
342
|
+
'graph',
|
|
343
|
+
`${response.status} ${method} ${graphPathShape(request.url)}`
|
|
344
|
+
);
|
|
345
|
+
log.debug(
|
|
346
|
+
`[GRAPH-API] ${method} ${request.url} failed with ${response.status}: ${response.text}`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return response;
|
|
350
|
+
} catch (error) {
|
|
351
|
+
log.note(
|
|
352
|
+
'graph',
|
|
353
|
+
`${error.code || 'network-error'} ${method} ${graphPathShape(request.url)}`
|
|
354
|
+
);
|
|
355
|
+
log.debug(`[GRAPH-API] ${method} ${request.url} failed:`, error);
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
328
360
|
/**
|
|
329
361
|
* Makes a request to the Microsoft Graph API
|
|
330
362
|
* In test mode (USE_TEST_MODE=true), routes to mock data instead of the real API.
|
|
@@ -357,7 +389,7 @@ async function callGraphAPI(
|
|
|
357
389
|
try {
|
|
358
390
|
finalUrl = buildGraphUrl(path, queryParams);
|
|
359
391
|
} catch (error) {
|
|
360
|
-
|
|
392
|
+
log.debug('Error calling Graph API:', error);
|
|
361
393
|
throw error;
|
|
362
394
|
}
|
|
363
395
|
|
|
@@ -422,12 +454,18 @@ async function callGraphAPI(
|
|
|
422
454
|
|
|
423
455
|
/**
|
|
424
456
|
* Calls Graph API with pagination support to retrieve all results up to maxCount
|
|
457
|
+
*
|
|
458
|
+
* Reports truncation (#279): `hasMore` is true when it stopped with more
|
|
459
|
+
* available (at maxCount with items trimmed or a further page, or because
|
|
460
|
+
* Graph repeated a nextLink). `@odata.count` is set only when every item was
|
|
461
|
+
* returned, so a page size is never presented as a total. `@odata.nextLink`
|
|
462
|
+
* is kept only when it continues exactly after the returned items.
|
|
425
463
|
* @param {string} accessToken - The access token for authentication
|
|
426
464
|
* @param {string} method - HTTP method (GET only for pagination)
|
|
427
465
|
* @param {string} path - API endpoint path
|
|
428
466
|
* @param {object} queryParams - Initial query parameters
|
|
429
467
|
* @param {number} maxCount - Maximum number of items to retrieve (0 = all)
|
|
430
|
-
* @returns {Promise<object
|
|
468
|
+
* @returns {Promise<{value: Array<object>, hasMore: boolean, '@odata.count'?: number, '@odata.nextLink'?: string}>}
|
|
431
469
|
* @throws {Error} If method is not 'GET'
|
|
432
470
|
* @throws {Error} If any page request fails for any other reason
|
|
433
471
|
*/
|
|
@@ -443,13 +481,14 @@ async function callGraphAPIPaginated(
|
|
|
443
481
|
}
|
|
444
482
|
|
|
445
483
|
const allItems = [];
|
|
446
|
-
|
|
484
|
+
const seenLinks = new Set();
|
|
447
485
|
let currentUrl = path;
|
|
448
486
|
let currentParams = { ...queryParams };
|
|
487
|
+
let nextLink;
|
|
488
|
+
let hasMore = false;
|
|
449
489
|
|
|
450
490
|
try {
|
|
451
|
-
|
|
452
|
-
// Make API call
|
|
491
|
+
for (;;) {
|
|
453
492
|
const response = await callGraphAPI(
|
|
454
493
|
accessToken,
|
|
455
494
|
method,
|
|
@@ -458,35 +497,39 @@ async function callGraphAPIPaginated(
|
|
|
458
497
|
currentParams
|
|
459
498
|
);
|
|
460
499
|
|
|
461
|
-
// Add items from this page
|
|
462
500
|
if (response.value && Array.isArray(response.value)) {
|
|
463
501
|
allItems.push(...response.value);
|
|
464
502
|
}
|
|
503
|
+
nextLink = response['@odata.nextLink'];
|
|
465
504
|
|
|
466
|
-
//
|
|
505
|
+
// Stop at the desired count; more remain if items were trimmed or
|
|
506
|
+
// there is another page.
|
|
467
507
|
if (maxCount > 0 && allItems.length >= maxCount) {
|
|
508
|
+
hasMore = allItems.length > maxCount || Boolean(nextLink);
|
|
509
|
+
if (allItems.length > maxCount) nextLink = undefined;
|
|
468
510
|
break;
|
|
469
511
|
}
|
|
470
|
-
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
currentUrl = nextLink;
|
|
477
|
-
currentParams = {}; // nextLink already contains all params
|
|
512
|
+
if (!nextLink) break;
|
|
513
|
+
// A repeated link would loop forever; stop and say there is more.
|
|
514
|
+
if (seenLinks.has(nextLink)) {
|
|
515
|
+
hasMore = true;
|
|
516
|
+
nextLink = undefined;
|
|
517
|
+
break;
|
|
478
518
|
}
|
|
479
|
-
|
|
519
|
+
seenLinks.add(nextLink);
|
|
520
|
+
currentUrl = nextLink; // the full nextLink URL carries every param
|
|
521
|
+
currentParams = {};
|
|
522
|
+
}
|
|
480
523
|
|
|
481
|
-
// Trim to exact count if needed
|
|
482
524
|
const finalItems = maxCount > 0 ? allItems.slice(0, maxCount) : allItems;
|
|
483
|
-
|
|
484
525
|
return {
|
|
485
526
|
value: finalItems,
|
|
486
|
-
|
|
527
|
+
hasMore,
|
|
528
|
+
...(!hasMore && { '@odata.count': finalItems.length }),
|
|
529
|
+
...(hasMore && nextLink && { '@odata.nextLink': nextLink }),
|
|
487
530
|
};
|
|
488
531
|
} catch (error) {
|
|
489
|
-
|
|
532
|
+
log.debug('Error during pagination:', error);
|
|
490
533
|
throw error;
|
|
491
534
|
}
|
|
492
535
|
}
|