@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.
Files changed (69) hide show
  1. package/.env.example +27 -3
  2. package/README.md +108 -33
  3. package/advanced/index.js +44 -174
  4. package/auth/auth-errors.js +23 -1
  5. package/auth/client-config.js +142 -0
  6. package/auth/index.js +4 -2
  7. package/auth/oauth-server.js +12 -2
  8. package/auth/token-manager.js +7 -3
  9. package/auth/token-storage.js +46 -33
  10. package/auth/tools.js +223 -93
  11. package/calendar/attendees.js +36 -0
  12. package/calendar/cancel.js +9 -25
  13. package/calendar/create.js +42 -48
  14. package/calendar/decline.js +10 -25
  15. package/calendar/delete.js +10 -25
  16. package/calendar/index.js +20 -37
  17. package/calendar/list.js +4 -16
  18. package/calendar/preview.js +335 -0
  19. package/calendar/update.js +42 -86
  20. package/categories/index.js +59 -264
  21. package/config.js +36 -2
  22. package/contacts/index.js +72 -128
  23. package/email/attachments.js +42 -124
  24. package/email/conversations.js +44 -78
  25. package/email/delta.js +10 -34
  26. package/email/draft.js +140 -96
  27. package/email/export.js +141 -110
  28. package/email/folder-utils.js +3 -2
  29. package/email/headers.js +11 -49
  30. package/email/index.js +85 -109
  31. package/email/list.js +4 -17
  32. package/email/mail-tips.js +86 -57
  33. package/email/mark-as-read.js +13 -49
  34. package/email/mime.js +14 -49
  35. package/email/read.js +16 -50
  36. package/email/search.js +46 -86
  37. package/email/send.js +82 -48
  38. package/folder/create.js +6 -25
  39. package/folder/delete.js +117 -38
  40. package/folder/index.js +17 -16
  41. package/folder/list.js +5 -17
  42. package/folder/move.js +13 -42
  43. package/folder/resolve.js +11 -6
  44. package/folder/stats.js +6 -20
  45. package/index.js +23 -45
  46. package/llms-install.md +31 -7
  47. package/llms.txt +19 -10
  48. package/outlook-auth-server.js +10 -3
  49. package/package.json +6 -2
  50. package/request-handler.js +217 -116
  51. package/rules/create.js +27 -70
  52. package/rules/index.js +30 -92
  53. package/rules/list.js +5 -17
  54. package/rules/rule-builder.js +57 -20
  55. package/rules/update.js +26 -60
  56. package/server.js +37 -0
  57. package/settings/index.js +142 -143
  58. package/tools.js +30 -0
  59. package/utils/field-presets.js +4 -2
  60. package/utils/graph-api.js +65 -22
  61. package/utils/logger.js +251 -0
  62. package/utils/mock-data.js +91 -2
  63. package/utils/read-only.js +59 -0
  64. package/utils/response-formatter.js +54 -15
  65. package/utils/risk-classes.js +324 -0
  66. package/utils/safe-write.js +372 -6
  67. package/utils/safety.js +109 -25
  68. package/utils/server-instructions.js +62 -0
  69. package/utils/tool-error.js +33 -0
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Risk-class map: the single source of truth for how risky each tool and
3
+ * action is (#270).
4
+ *
5
+ * Every tool, and every value of its `action` enum, is mapped to one class:
6
+ * - read no change anywhere
7
+ * - reversible changes only the user's own data, and can be undone
8
+ * - outward reaches other people (sends, invites, cancellations)
9
+ * - destructive deletes something that may not be recoverable
10
+ * - persistent keeps acting after the call (rules, forwarding, auto-replies)
11
+ *
12
+ * Tool annotations are derived from this map (see riskAnnotations), so the
13
+ * hints can't drift from the classes. The server instructions, read-only
14
+ * mode, the plugin hook and the skill's risk table build on it too. A test
15
+ * fails if any tool or action is unclassified, so new surfaces (OneDrive,
16
+ * To Do, Teams) must be classified on purpose.
17
+ *
18
+ * Per-tool flags:
19
+ * - untrustedContent results can carry content written by other people
20
+ * (email bodies, attachments, directory data), which
21
+ * makes the tool open-world (#92)
22
+ * - idempotent repeating a write with the same arguments has no
23
+ * further effect (reads are always idempotent)
24
+ * - defaultAction the action the handler runs when a call leaves
25
+ * `action` out (only where the schema makes it
26
+ * optional), so such calls classify correctly
27
+ * - requiresUserInteraction
28
+ * publish Claude's `anthropic/requiresUserInteraction`
29
+ * flag, so Claude clients always ask before running it.
30
+ * Only for single-purpose tools whose every call reaches
31
+ * other people; mixed read/write tools never get it (D1)
32
+ *
33
+ * Pure data with no requires, so any module can load it.
34
+ */
35
+
36
+ const RISK_CLASSES = [
37
+ 'read',
38
+ 'reversible',
39
+ 'outward',
40
+ 'destructive',
41
+ 'persistent',
42
+ ];
43
+
44
+ const TOOL_RISK = {
45
+ auth: {
46
+ // Sign-in only writes the local token file.
47
+ actions: {
48
+ status: 'read',
49
+ authenticate: 'reversible',
50
+ 'device-code-complete': 'reversible',
51
+ about: 'read',
52
+ },
53
+ defaultAction: 'status',
54
+ },
55
+
56
+ // Calendar
57
+ // Event subjects and previews come from external organisers.
58
+ 'list-events': { default: 'read', untrustedContent: true },
59
+ // Saving an event with attendees sends them invitations.
60
+ 'create-event': { default: 'outward', requiresUserInteraction: true },
61
+ 'manage-event': {
62
+ actions: {
63
+ // Organiser updates are sent to attendees.
64
+ update: 'outward',
65
+ decline: 'outward',
66
+ cancel: 'outward',
67
+ // Graph sends cancellations when an organiser deletes a meeting.
68
+ delete: 'outward',
69
+ },
70
+ },
71
+
72
+ // Email
73
+ 'search-emails': { default: 'read', untrustedContent: true },
74
+ 'read-email': { default: 'read', untrustedContent: true },
75
+ 'send-email': { default: 'outward', requiresUserInteraction: true },
76
+ draft: {
77
+ actions: {
78
+ create: 'reversible',
79
+ update: 'reversible',
80
+ send: 'outward',
81
+ delete: 'destructive',
82
+ // reply/reply-all/forward only create a draft; nothing is sent.
83
+ reply: 'reversible',
84
+ 'reply-all': 'reversible',
85
+ forward: 'reversible',
86
+ },
87
+ // Reply and forward drafts quote the original message.
88
+ untrustedContent: true,
89
+ },
90
+ 'update-email': {
91
+ actions: {
92
+ 'mark-read': 'reversible',
93
+ 'mark-unread': 'reversible',
94
+ flag: 'reversible',
95
+ unflag: 'reversible',
96
+ complete: 'reversible',
97
+ },
98
+ idempotent: true,
99
+ },
100
+ attachments: {
101
+ // download writes a new local file (never overwrites).
102
+ actions: { list: 'read', view: 'read', download: 'reversible' },
103
+ defaultAction: 'list',
104
+ untrustedContent: true,
105
+ },
106
+ // Writes local files, and with overwrite: true can replace an existing one.
107
+ export: { default: 'destructive', untrustedContent: true },
108
+ // Echoes recipients' out-of-office messages.
109
+ 'get-mail-tips': { default: 'read', untrustedContent: true },
110
+
111
+ // Folders
112
+ folders: {
113
+ actions: {
114
+ list: 'read',
115
+ create: 'reversible',
116
+ // Moves emails between folders.
117
+ move: 'reversible',
118
+ stats: 'read',
119
+ delete: 'destructive',
120
+ },
121
+ defaultAction: 'list',
122
+ },
123
+
124
+ // Rules: create/update/reorder change what happens to future mail,
125
+ // including forwarding it to other people.
126
+ 'manage-rules': {
127
+ actions: {
128
+ list: 'read',
129
+ create: 'persistent',
130
+ update: 'persistent',
131
+ reorder: 'persistent',
132
+ delete: 'destructive',
133
+ },
134
+ defaultAction: 'list',
135
+ },
136
+
137
+ // Contacts
138
+ 'manage-contact': {
139
+ actions: {
140
+ list: 'read',
141
+ search: 'read',
142
+ get: 'read',
143
+ create: 'reversible',
144
+ update: 'reversible',
145
+ delete: 'destructive',
146
+ },
147
+ defaultAction: 'list',
148
+ },
149
+ 'search-people': { default: 'read', untrustedContent: true },
150
+
151
+ // Categories
152
+ 'manage-category': {
153
+ actions: {
154
+ list: 'read',
155
+ create: 'reversible',
156
+ update: 'reversible',
157
+ set: 'reversible',
158
+ delete: 'destructive',
159
+ },
160
+ defaultAction: 'list',
161
+ },
162
+ 'apply-category': {
163
+ actions: { set: 'reversible', add: 'reversible', remove: 'reversible' },
164
+ defaultAction: 'set',
165
+ idempotent: true,
166
+ },
167
+ 'manage-focused-inbox': {
168
+ actions: { list: 'read', set: 'reversible', delete: 'destructive' },
169
+ defaultAction: 'list',
170
+ },
171
+
172
+ // Settings: auto-replies answer external senders until switched off.
173
+ 'mailbox-settings': {
174
+ actions: {
175
+ get: 'read',
176
+ 'set-auto-replies': 'persistent',
177
+ 'set-working-hours': 'reversible',
178
+ },
179
+ defaultAction: 'get',
180
+ idempotent: true,
181
+ },
182
+
183
+ // Advanced
184
+ 'access-shared-mailbox': { default: 'read', untrustedContent: true },
185
+ 'find-meeting-rooms': { default: 'read' },
186
+ };
187
+
188
+ /**
189
+ * Which calls genuinely honour `dryRun: true` (#274): `true` for a
190
+ * single-purpose tool, or the list of actions that return a preview and
191
+ * change nothing. The dispatcher refuses `dryRun: true` on every other call
192
+ * (including read actions) before the handler runs, so a "preview" can never
193
+ * really send, delete or change anything, and the plugin hook lets only these
194
+ * previews run without asking. A test checks this map against
195
+ * every tool whose schema has a `dryRun` property.
196
+ */
197
+ const DRY_RUN_ACTIONS = {
198
+ 'create-event': true,
199
+ 'manage-event': ['update', 'decline', 'cancel', 'delete'],
200
+ 'send-email': true,
201
+ draft: ['create'],
202
+ folders: ['delete'],
203
+ 'manage-rules': ['create', 'update'],
204
+ 'manage-contact': ['delete'],
205
+ 'mailbox-settings': ['set-auto-replies'],
206
+ };
207
+
208
+ /** Classes that make a tool destructive and need a human's confirmation. */
209
+ const HIGH_RISK = new Set(['outward', 'destructive', 'persistent']);
210
+ /** Classes whose effects reach people outside the mailbox. */
211
+ const REACHES_OTHERS = new Set(['outward', 'persistent']);
212
+
213
+ /**
214
+ * Every class a tool can take, across all its actions.
215
+ * @param {object} entry - TOOL_RISK entry
216
+ * @returns {string[]}
217
+ */
218
+ function classesOf(entry) {
219
+ return entry.actions ? Object.values(entry.actions) : [entry.default];
220
+ }
221
+
222
+ /**
223
+ * Risk class for a tool call.
224
+ * @param {string} toolName
225
+ * @param {string|null} [action] - the call's `action` argument, for action
226
+ * tools; when left out or null (handlers treat both alike), the tool's
227
+ * defaultAction (if any) is classified
228
+ * @returns {string|undefined} the class, or undefined if unclassified
229
+ */
230
+ function classify(toolName, action) {
231
+ const entry = TOOL_RISK[toolName];
232
+ if (!entry) return undefined;
233
+ if (!entry.actions) return entry.default;
234
+ const effective = effectiveAction(toolName, action);
235
+ return Object.hasOwn(entry.actions, effective)
236
+ ? entry.actions[effective]
237
+ : undefined;
238
+ }
239
+
240
+ /**
241
+ * The action a call runs: its `action` argument, or the tool's defaultAction
242
+ * when that is left out or null (handlers treat both alike).
243
+ * @param {string} toolName
244
+ * @param {string|null} [action]
245
+ * @returns {string|undefined}
246
+ */
247
+ function effectiveAction(toolName, action) {
248
+ return action ?? TOOL_RISK[toolName]?.defaultAction;
249
+ }
250
+
251
+ /**
252
+ * Whether a call honours `dryRun: true` (see DRY_RUN_ACTIONS). Resolves a
253
+ * missing or null action to the tool's defaultAction, like classify().
254
+ * @param {string} toolName
255
+ * @param {string|null} [action]
256
+ * @returns {boolean}
257
+ */
258
+ function supportsDryRun(toolName, action) {
259
+ const supported = DRY_RUN_ACTIONS[toolName];
260
+ if (supported === true) return true;
261
+ if (!Array.isArray(supported)) return false;
262
+ return supported.includes(effectiveAction(toolName, action));
263
+ }
264
+
265
+ /**
266
+ * MCP annotations for a tool, derived from its risk classes (#277). All four
267
+ * hints are always set explicitly.
268
+ * @param {string} toolName
269
+ * @param {string} title - display title
270
+ * @returns {{title: string, readOnlyHint: boolean, destructiveHint: boolean, idempotentHint: boolean, openWorldHint: boolean}}
271
+ */
272
+ function riskAnnotations(toolName, title) {
273
+ const entry = TOOL_RISK[toolName];
274
+ if (!entry) {
275
+ throw new Error(
276
+ `Tool '${toolName}' has no risk class; add it to utils/risk-classes.js`
277
+ );
278
+ }
279
+ const classes = classesOf(entry);
280
+ const readOnly = classes.every((c) => c === 'read');
281
+ return {
282
+ title,
283
+ readOnlyHint: readOnly,
284
+ destructiveHint: classes.some((c) => HIGH_RISK.has(c)),
285
+ idempotentHint: readOnly || entry.idempotent === true,
286
+ openWorldHint:
287
+ entry.untrustedContent === true ||
288
+ classes.some((c) => REACHES_OTHERS.has(c)),
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Top-level tool metadata: the spec's `title` plus derived annotations.
294
+ * Spread into a tool definition: `{ name, ...toolMetadata(name, title), ... }`.
295
+ * @param {string} toolName
296
+ * @param {string} title
297
+ * @returns {{title: string, annotations: object}}
298
+ */
299
+ function toolMetadata(toolName, title) {
300
+ return { title, annotations: riskAnnotations(toolName, title) };
301
+ }
302
+
303
+ /**
304
+ * Tool-level `_meta` for tools/list, derived from the map (#271). Currently
305
+ * only Claude's `anthropic/requiresUserInteraction`; other clients ignore it.
306
+ * @param {string} toolName
307
+ * @returns {object|undefined} the `_meta` object, or undefined for none
308
+ */
309
+ function riskMeta(toolName) {
310
+ const entry = TOOL_RISK[toolName];
311
+ if (!entry || entry.requiresUserInteraction !== true) return undefined;
312
+ return { 'anthropic/requiresUserInteraction': true };
313
+ }
314
+
315
+ module.exports = {
316
+ DRY_RUN_ACTIONS,
317
+ RISK_CLASSES,
318
+ TOOL_RISK,
319
+ classify,
320
+ riskAnnotations,
321
+ riskMeta,
322
+ supportsDryRun,
323
+ toolMetadata,
324
+ };