@littlebearapps/outlook-assistant 3.12.1 → 3.14.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.
- package/.env.example +27 -3
- package/README.md +108 -33
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/client-config.js +142 -0
- package/auth/index.js +4 -2
- package/auth/oauth-server.js +12 -2
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +46 -33
- package/auth/tools.js +223 -93
- 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 +335 -0
- package/calendar/update.js +42 -86
- package/categories/index.js +59 -264
- package/config.js +36 -2
- package/contacts/index.js +72 -128
- package/email/attachments.js +42 -124
- package/email/conversations.js +44 -78
- package/email/delta.js +10 -34
- package/email/draft.js +140 -96
- package/email/export.js +141 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +85 -109
- 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 +14 -49
- package/email/read.js +16 -50
- package/email/search.js +46 -86
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +17 -16
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +6 -20
- package/index.js +23 -45
- package/llms-install.md +31 -7
- package/llms.txt +19 -10
- package/outlook-auth-server.js +10 -3
- package/package.json +6 -2
- package/request-handler.js +217 -116
- package/rules/create.js +27 -70
- package/rules/index.js +30 -92
- package/rules/list.js +5 -17
- package/rules/rule-builder.js +57 -20
- package/rules/update.js +26 -60
- package/server.js +37 -0
- package/settings/index.js +142 -143
- 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 +109 -25
- package/utils/server-instructions.js +62 -0
- package/utils/tool-error.js +33 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dryRun previews for calendar actions that email other people (#274):
|
|
3
|
+
* create-event and manage-event cancel/decline/delete.
|
|
4
|
+
*
|
|
5
|
+
* A preview may read (the event, the signed-in address) but never writes,
|
|
6
|
+
* and says exactly who would be emailed. "External" means an address whose
|
|
7
|
+
* domain differs from the signed-in user's. That address comes from GET /me;
|
|
8
|
+
* if that read fails, the organiser's address stands in on an event you
|
|
9
|
+
* organised. Otherwise the external count is reported as unknown, never
|
|
10
|
+
* guessed.
|
|
11
|
+
*/
|
|
12
|
+
const config = require('../config');
|
|
13
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
14
|
+
const { ensureAuthenticated } = require('../auth');
|
|
15
|
+
const { dryRunResult } = require('../utils/safety');
|
|
16
|
+
const handleListEvents = require('./list');
|
|
17
|
+
|
|
18
|
+
const { toUtcIso, formatLocal } = handleListEvents;
|
|
19
|
+
|
|
20
|
+
/** Most attendees listed by address; the rest are summarised. */
|
|
21
|
+
const MAX_LISTED = 25;
|
|
22
|
+
|
|
23
|
+
const RESTORE_NOTE =
|
|
24
|
+
"Graph doesn't document a guaranteed way to restore a deleted event, so don't count on getting it back.";
|
|
25
|
+
|
|
26
|
+
/** Lower-cased domain of an email address, or null. */
|
|
27
|
+
function emailDomain(address) {
|
|
28
|
+
if (typeof address !== 'string') return null;
|
|
29
|
+
const at = address.lastIndexOf('@');
|
|
30
|
+
return at > 0
|
|
31
|
+
? address
|
|
32
|
+
.slice(at + 1)
|
|
33
|
+
.trim()
|
|
34
|
+
.toLowerCase() || null
|
|
35
|
+
: null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The signed-in user's address from GET /me, or null if it can't be read. */
|
|
39
|
+
async function getOwnAddress(accessToken) {
|
|
40
|
+
try {
|
|
41
|
+
const me = await callGraphAPI(accessToken, 'GET', 'me', null, {
|
|
42
|
+
$select: 'mail,userPrincipalName',
|
|
43
|
+
});
|
|
44
|
+
return me?.mail || me?.userPrincipalName || null;
|
|
45
|
+
} catch (_error) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Own address, falling back to the organiser on an event you organised. */
|
|
51
|
+
async function ownAddressFor(accessToken, event) {
|
|
52
|
+
const own = await getOwnAddress(accessToken);
|
|
53
|
+
if (own) return own;
|
|
54
|
+
return event.isOrganizer ? event.organizer?.emailAddress?.address : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Split Graph attendees into people and rooms/resources, leaving out the
|
|
59
|
+
* signed-in user, and mark each person external or not.
|
|
60
|
+
* @returns {{people: Array<{address: string, external: (boolean|null)}>, resources: string[], external: (number|null)}}
|
|
61
|
+
*/
|
|
62
|
+
function summariseAttendees(attendees, ownAddress) {
|
|
63
|
+
const own = ownAddress ? ownAddress.toLowerCase() : null;
|
|
64
|
+
const ownDomain = emailDomain(own);
|
|
65
|
+
const people = [];
|
|
66
|
+
const resources = [];
|
|
67
|
+
for (const attendee of attendees || []) {
|
|
68
|
+
const address = attendee?.emailAddress?.address;
|
|
69
|
+
if (!address || address.toLowerCase() === own) continue;
|
|
70
|
+
if (attendee.type === 'resource') {
|
|
71
|
+
resources.push(address);
|
|
72
|
+
} else {
|
|
73
|
+
people.push({
|
|
74
|
+
address,
|
|
75
|
+
external: ownDomain ? emailDomain(address) !== ownDomain : null,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const external = ownDomain ? people.filter((p) => p.external).length : null;
|
|
80
|
+
return { people, resources, external };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function plural(count, word) {
|
|
84
|
+
return `${count} ${word}${count === 1 ? '' : 's'}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** "6 attendees (2 external)" */
|
|
88
|
+
function countPhrase({ people, external }) {
|
|
89
|
+
const ext =
|
|
90
|
+
external === null ? 'external count unknown' : `${external} external`;
|
|
91
|
+
return `${plural(people.length, 'attendee')} (${ext})`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Who would be emailed, by address (capped). */
|
|
95
|
+
function recipientLines({ people, resources }) {
|
|
96
|
+
const lines = [];
|
|
97
|
+
if (people.length > 0) {
|
|
98
|
+
lines.push('', 'Attendees:');
|
|
99
|
+
for (const person of people.slice(0, MAX_LISTED)) {
|
|
100
|
+
lines.push(`- ${person.address}${person.external ? ' (external)' : ''}`);
|
|
101
|
+
}
|
|
102
|
+
if (people.length > MAX_LISTED) {
|
|
103
|
+
lines.push(`- …and ${people.length - MAX_LISTED} more`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (resources.length > 0) {
|
|
107
|
+
lines.push(
|
|
108
|
+
'',
|
|
109
|
+
`Rooms and resources also notified: ${resources.join(', ')}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return lines;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function messageLine(comment) {
|
|
116
|
+
return typeof comment === 'string' && comment.trim() !== ''
|
|
117
|
+
? `Message: "${comment}"`
|
|
118
|
+
: 'No message (no `comment` given).';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** "on 3 Apr 2026, 9:00 am GMT+11:00" for an event read in UTC. */
|
|
122
|
+
function eventWhen(event) {
|
|
123
|
+
try {
|
|
124
|
+
const local = formatLocal(toUtcIso(event.start), config.DEFAULT_TIMEZONE);
|
|
125
|
+
if (local) return `on ${local}`;
|
|
126
|
+
} catch (_error) {
|
|
127
|
+
// Fall through to the raw value.
|
|
128
|
+
}
|
|
129
|
+
const start = event.start || {};
|
|
130
|
+
return start.dateTime
|
|
131
|
+
? `on ${start.dateTime} (${start.timeZone || 'UTC'})`
|
|
132
|
+
: 'at an unknown time';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The organiser's name and address, or null if Graph gave neither. */
|
|
136
|
+
function organiserLabel(event) {
|
|
137
|
+
const organiser = event.organizer?.emailAddress || {};
|
|
138
|
+
if (organiser.name && organiser.address) {
|
|
139
|
+
return `${organiser.name} <${organiser.address}>`;
|
|
140
|
+
}
|
|
141
|
+
return organiser.address || organiser.name || null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** "the organiser, Name <address>", or just "the organiser" if unknown. */
|
|
145
|
+
function theOrganiser(event) {
|
|
146
|
+
const label = organiserLabel(event);
|
|
147
|
+
return label ? `the organiser, ${label}` : 'the organiser';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** "'Subject' on …", with a fallback for an event that has no subject. */
|
|
151
|
+
function eventTitle(event) {
|
|
152
|
+
const subject = event.subject ? `'${event.subject}'` : '(no subject)';
|
|
153
|
+
return `${subject} ${eventWhen(event)}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Read the fields a preview needs, with times in UTC. */
|
|
157
|
+
function fetchEvent(accessToken, eventId) {
|
|
158
|
+
return callGraphAPI(
|
|
159
|
+
accessToken,
|
|
160
|
+
'GET',
|
|
161
|
+
`me/events/${eventId}`,
|
|
162
|
+
null,
|
|
163
|
+
{
|
|
164
|
+
$select: 'subject,start,end,isOrganizer,isCancelled,organizer,attendees',
|
|
165
|
+
},
|
|
166
|
+
{ Prefer: 'outlook.timezone="UTC"' }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Preview create-event: who would be invited.
|
|
172
|
+
* @param {object} event - The POST body create-event would send
|
|
173
|
+
*/
|
|
174
|
+
async function previewCreateEvent(event) {
|
|
175
|
+
const { subject, start, end } = event;
|
|
176
|
+
const attendees = event.attendees || [];
|
|
177
|
+
const own =
|
|
178
|
+
attendees.length > 0
|
|
179
|
+
? await getOwnAddress(await ensureAuthenticated())
|
|
180
|
+
: null;
|
|
181
|
+
const summary = summariseAttendees(attendees, own);
|
|
182
|
+
|
|
183
|
+
const when =
|
|
184
|
+
start.timeZone === end.timeZone
|
|
185
|
+
? `on ${start.dateTime} to ${end.dateTime} (${start.timeZone})`
|
|
186
|
+
: `on ${start.dateTime} (${start.timeZone}) to ${end.dateTime} (${end.timeZone})`;
|
|
187
|
+
const head = `Creates '${subject}' ${when} in your calendar`;
|
|
188
|
+
let lines;
|
|
189
|
+
if (summary.people.length > 0) {
|
|
190
|
+
lines = [`${head} and emails invitations to ${countPhrase(summary)}.`];
|
|
191
|
+
} else if (summary.resources.length > 0) {
|
|
192
|
+
lines = [`${head}. No people are invited.`];
|
|
193
|
+
} else {
|
|
194
|
+
lines = [`${head}. No attendees, so no invitations are sent.`];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return dryRunResult([...lines, ...recipientLines(summary)], {
|
|
198
|
+
action: 'create',
|
|
199
|
+
subject,
|
|
200
|
+
notified: summary.people.length,
|
|
201
|
+
external: summary.external,
|
|
202
|
+
event,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Preview manage-event cancel: who gets the cancellation. */
|
|
207
|
+
async function previewCancelEvent(accessToken, { eventId, comment }) {
|
|
208
|
+
const event = await fetchEvent(accessToken, eventId);
|
|
209
|
+
const meta = { action: 'cancel', eventId };
|
|
210
|
+
const title = eventTitle(event);
|
|
211
|
+
|
|
212
|
+
if (!event.isOrganizer) {
|
|
213
|
+
return dryRunResult(
|
|
214
|
+
[
|
|
215
|
+
`You aren't the organiser of ${title}, so Graph will refuse to cancel it and nobody is emailed.`,
|
|
216
|
+
'Use action=decline to tell the organiser, or action=delete to remove it from your calendar.',
|
|
217
|
+
],
|
|
218
|
+
{ ...meta, notified: 0 }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const summary = summariseAttendees(
|
|
223
|
+
event.attendees,
|
|
224
|
+
await ownAddressFor(accessToken, event)
|
|
225
|
+
);
|
|
226
|
+
const lines =
|
|
227
|
+
summary.people.length > 0
|
|
228
|
+
? [
|
|
229
|
+
`Cancels ${title} and emails a cancellation to ${countPhrase(summary)}.`,
|
|
230
|
+
messageLine(comment),
|
|
231
|
+
]
|
|
232
|
+
: [`Cancels ${title}. It has no attendees, so nobody is emailed.`];
|
|
233
|
+
|
|
234
|
+
return dryRunResult([...lines, ...recipientLines(summary)], {
|
|
235
|
+
...meta,
|
|
236
|
+
notified: summary.people.length,
|
|
237
|
+
external: summary.external,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Preview manage-event decline: whether the organiser is emailed. */
|
|
242
|
+
async function previewDeclineEvent(
|
|
243
|
+
accessToken,
|
|
244
|
+
{ eventId, comment, sendResponse }
|
|
245
|
+
) {
|
|
246
|
+
const event = await fetchEvent(accessToken, eventId);
|
|
247
|
+
const meta = { action: 'decline', eventId };
|
|
248
|
+
const title = eventTitle(event);
|
|
249
|
+
const organiser = theOrganiser(event);
|
|
250
|
+
|
|
251
|
+
if (event.isOrganizer) {
|
|
252
|
+
return dryRunResult(
|
|
253
|
+
[
|
|
254
|
+
`You organised this event (${title}), so Graph will refuse to decline it and nobody is emailed.`,
|
|
255
|
+
'Use action=cancel to cancel it for everyone, or action=delete to remove it.',
|
|
256
|
+
],
|
|
257
|
+
{ ...meta, notified: 0 }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (sendResponse === false) {
|
|
262
|
+
return dryRunResult(
|
|
263
|
+
`Declines ${title} without notifying ${organiser} (sendResponse=false).`,
|
|
264
|
+
{ ...meta, notified: 0 }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const ownDomain = emailDomain(await ownAddressFor(accessToken, event));
|
|
269
|
+
const organiserDomain = emailDomain(event.organizer?.emailAddress?.address);
|
|
270
|
+
let status = ' (external status unknown)';
|
|
271
|
+
if (ownDomain && organiserDomain) {
|
|
272
|
+
status = organiserDomain === ownDomain ? ' (internal)' : ' (external)';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return dryRunResult(
|
|
276
|
+
[
|
|
277
|
+
`Declines ${title} and emails your response to ${organiser}${status}.`,
|
|
278
|
+
messageLine(comment),
|
|
279
|
+
],
|
|
280
|
+
{ ...meta, notified: 1 }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Preview manage-event delete: who (if anyone) gets a cancellation. */
|
|
285
|
+
async function previewDeleteEvent(accessToken, { eventId }) {
|
|
286
|
+
const event = await fetchEvent(accessToken, eventId);
|
|
287
|
+
const meta = { action: 'delete', eventId };
|
|
288
|
+
const head = `Deletes ${eventTitle(event)} from your calendar`;
|
|
289
|
+
|
|
290
|
+
if (!event.isOrganizer) {
|
|
291
|
+
const organiser = organiserLabel(event)
|
|
292
|
+
? `${theOrganiser(event)},`
|
|
293
|
+
: theOrganiser(event);
|
|
294
|
+
return dryRunResult(
|
|
295
|
+
[
|
|
296
|
+
`${head}. Nobody is emailed: ${organiser} isn't told you won't attend (use action=decline for that).`,
|
|
297
|
+
RESTORE_NOTE,
|
|
298
|
+
],
|
|
299
|
+
{ ...meta, notified: 0 }
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const summary = summariseAttendees(
|
|
304
|
+
event.attendees,
|
|
305
|
+
await ownAddressFor(accessToken, event)
|
|
306
|
+
);
|
|
307
|
+
if (event.isCancelled || summary.people.length === 0) {
|
|
308
|
+
const why = event.isCancelled
|
|
309
|
+
? "it's already cancelled"
|
|
310
|
+
: 'it has no attendees';
|
|
311
|
+
return dryRunResult([`${head}. Nobody is emailed: ${why}.`, RESTORE_NOTE], {
|
|
312
|
+
...meta,
|
|
313
|
+
notified: 0,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return dryRunResult(
|
|
318
|
+
[
|
|
319
|
+
`${head} and emails a cancellation to ${countPhrase(summary)}.`,
|
|
320
|
+
'To word that cancellation yourself, use action=cancel with a `comment` instead.',
|
|
321
|
+
RESTORE_NOTE,
|
|
322
|
+
...recipientLines(summary),
|
|
323
|
+
],
|
|
324
|
+
{ ...meta, notified: summary.people.length, external: summary.external }
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = {
|
|
329
|
+
emailDomain,
|
|
330
|
+
summariseAttendees,
|
|
331
|
+
previewCreateEvent,
|
|
332
|
+
previewCancelEvent,
|
|
333
|
+
previewDeclineEvent,
|
|
334
|
+
previewDeleteEvent,
|
|
335
|
+
};
|
package/calendar/update.js
CHANGED
|
@@ -30,7 +30,13 @@
|
|
|
30
30
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
31
31
|
const { ensureAuthenticated } = require('../auth');
|
|
32
32
|
const { DEFAULT_TIMEZONE } = require('../config');
|
|
33
|
-
const {
|
|
33
|
+
const {
|
|
34
|
+
normaliseAttendees,
|
|
35
|
+
buildAttendees,
|
|
36
|
+
checkAttendeeAllowlist,
|
|
37
|
+
} = require('./attendees');
|
|
38
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
39
|
+
const { dryRunResult } = require('../utils/safety');
|
|
34
40
|
|
|
35
41
|
const SENSITIVITY_VALUES = new Set([
|
|
36
42
|
'normal',
|
|
@@ -72,14 +78,7 @@ async function handleUpdateEvent(args) {
|
|
|
72
78
|
} = args;
|
|
73
79
|
|
|
74
80
|
if (!eventId) {
|
|
75
|
-
return
|
|
76
|
-
content: [
|
|
77
|
-
{
|
|
78
|
-
type: 'text',
|
|
79
|
-
text: 'Event ID is required to update an event.',
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
};
|
|
81
|
+
return toolError('Event ID is required to update an event.');
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
// Build the patch body from only the fields the caller actually provided.
|
|
@@ -119,6 +118,12 @@ async function handleUpdateEvent(args) {
|
|
|
119
118
|
};
|
|
120
119
|
}
|
|
121
120
|
patch.attendees = buildAttendees(entries);
|
|
121
|
+
// Graph emails every attendee on the new list, so check them all.
|
|
122
|
+
const allowlistError = checkAttendeeAllowlist(patch.attendees, {
|
|
123
|
+
operation: 'update',
|
|
124
|
+
dryRun,
|
|
125
|
+
});
|
|
126
|
+
if (allowlistError) return allowlistError;
|
|
122
127
|
if (entries.some((entry) => !entry.type)) untypedAttendees = entries;
|
|
123
128
|
}
|
|
124
129
|
|
|
@@ -136,42 +141,27 @@ async function handleUpdateEvent(args) {
|
|
|
136
141
|
|
|
137
142
|
if (sensitivity !== undefined) {
|
|
138
143
|
if (!SENSITIVITY_VALUES.has(sensitivity)) {
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
type: 'text',
|
|
143
|
-
text: `Invalid sensitivity: '${sensitivity}'. Must be one of: ${[...SENSITIVITY_VALUES].join(', ')}.`,
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
};
|
|
144
|
+
return toolError(
|
|
145
|
+
`Invalid sensitivity: '${sensitivity}'. Must be one of: ${[...SENSITIVITY_VALUES].join(', ')}.`
|
|
146
|
+
);
|
|
147
147
|
}
|
|
148
148
|
patch.sensitivity = sensitivity;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
if (showAs !== undefined) {
|
|
152
152
|
if (!SHOW_AS_VALUES.has(showAs)) {
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
type: 'text',
|
|
157
|
-
text: `Invalid showAs: '${showAs}'. Must be one of: ${[...SHOW_AS_VALUES].join(', ')}.`,
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
};
|
|
153
|
+
return toolError(
|
|
154
|
+
`Invalid showAs: '${showAs}'. Must be one of: ${[...SHOW_AS_VALUES].join(', ')}.`
|
|
155
|
+
);
|
|
161
156
|
}
|
|
162
157
|
patch.showAs = showAs;
|
|
163
158
|
}
|
|
164
159
|
|
|
165
160
|
if (importance !== undefined) {
|
|
166
161
|
if (!IMPORTANCE_VALUES.has(importance)) {
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
type: 'text',
|
|
171
|
-
text: `Invalid importance: '${importance}'. Must be one of: ${[...IMPORTANCE_VALUES].join(', ')}.`,
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
};
|
|
162
|
+
return toolError(
|
|
163
|
+
`Invalid importance: '${importance}'. Must be one of: ${[...IMPORTANCE_VALUES].join(', ')}.`
|
|
164
|
+
);
|
|
175
165
|
}
|
|
176
166
|
patch.importance = importance;
|
|
177
167
|
}
|
|
@@ -184,27 +174,17 @@ async function handleUpdateEvent(args) {
|
|
|
184
174
|
if (reminderMinutesBeforeStart !== undefined) {
|
|
185
175
|
const reminder = Number(reminderMinutesBeforeStart);
|
|
186
176
|
if (!Number.isFinite(reminder) || reminder < 0) {
|
|
187
|
-
return
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
type: 'text',
|
|
191
|
-
text: `Invalid reminderMinutesBeforeStart: '${reminderMinutesBeforeStart}'. Must be a non-negative number.`,
|
|
192
|
-
},
|
|
193
|
-
],
|
|
194
|
-
};
|
|
177
|
+
return toolError(
|
|
178
|
+
`Invalid reminderMinutesBeforeStart: '${reminderMinutesBeforeStart}'. Must be a non-negative number.`
|
|
179
|
+
);
|
|
195
180
|
}
|
|
196
181
|
patch.reminderMinutesBeforeStart = reminder;
|
|
197
182
|
}
|
|
198
183
|
|
|
199
184
|
if (Object.keys(patch).length === 0) {
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
type: 'text',
|
|
204
|
-
text: 'No fields to update — provide at least one updatable field (subject, start, end, attendees, body, location, isOnlineMeeting, sensitivity, showAs, importance, categories, reminderMinutesBeforeStart).',
|
|
205
|
-
},
|
|
206
|
-
],
|
|
207
|
-
};
|
|
185
|
+
return toolError(
|
|
186
|
+
'No fields to update — provide at least one updatable field (subject, start, end, attendees, body, location, isOnlineMeeting, sensitivity, showAs, importance, categories, reminderMinutesBeforeStart).'
|
|
187
|
+
);
|
|
208
188
|
}
|
|
209
189
|
|
|
210
190
|
try {
|
|
@@ -228,28 +208,18 @@ async function handleUpdateEvent(args) {
|
|
|
228
208
|
|
|
229
209
|
// dryRun: show the caller what would be sent without changing anything.
|
|
230
210
|
if (dryRun) {
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
{
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
'```',
|
|
241
|
-
'',
|
|
242
|
-
`Fields that would change: ${Object.keys(patch).join(', ')}`,
|
|
243
|
-
].join('\n'),
|
|
244
|
-
},
|
|
211
|
+
return dryRunResult(
|
|
212
|
+
[
|
|
213
|
+
`Would PATCH \`me/events/${eventId}\` with:`,
|
|
214
|
+
'',
|
|
215
|
+
'```json',
|
|
216
|
+
JSON.stringify(patch, null, 2),
|
|
217
|
+
'```',
|
|
218
|
+
'',
|
|
219
|
+
`Fields that would change: ${Object.keys(patch).join(', ')}`,
|
|
245
220
|
],
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
dryRun: true,
|
|
249
|
-
patch,
|
|
250
|
-
fieldsChanged: Object.keys(patch),
|
|
251
|
-
},
|
|
252
|
-
};
|
|
221
|
+
{ eventId, patch, fieldsChanged: Object.keys(patch) }
|
|
222
|
+
);
|
|
253
223
|
}
|
|
254
224
|
|
|
255
225
|
accessToken = accessToken || (await ensureAuthenticated());
|
|
@@ -295,24 +265,10 @@ async function handleUpdateEvent(args) {
|
|
|
295
265
|
};
|
|
296
266
|
} catch (error) {
|
|
297
267
|
if (error.message === 'Authentication required') {
|
|
298
|
-
return
|
|
299
|
-
content: [
|
|
300
|
-
{
|
|
301
|
-
type: 'text',
|
|
302
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
303
|
-
},
|
|
304
|
-
],
|
|
305
|
-
};
|
|
268
|
+
return authRequiredError();
|
|
306
269
|
}
|
|
307
270
|
|
|
308
|
-
return {
|
|
309
|
-
content: [
|
|
310
|
-
{
|
|
311
|
-
type: 'text',
|
|
312
|
-
text: `Error updating event: ${error.message}`,
|
|
313
|
-
},
|
|
314
|
-
],
|
|
315
|
-
};
|
|
271
|
+
return toolError(`Error updating event: ${error.message}`);
|
|
316
272
|
}
|
|
317
273
|
}
|
|
318
274
|
|