@littlebearapps/outlook-assistant 3.8.2 → 3.9.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/README.md +5 -5
- package/advanced/index.js +3 -1
- package/calendar/index.js +1 -1
- package/calendar/list.js +114 -22
- package/categories/index.js +2 -1
- package/contacts/index.js +3 -1
- package/email/folder-utils.js +13 -13
- package/email/index.js +20 -7
- package/email/search.js +252 -151
- package/folder/create.js +39 -27
- package/folder/delete.js +26 -36
- package/folder/index.js +19 -7
- package/folder/list.js +51 -66
- package/folder/move.js +17 -17
- package/folder/resolve.js +315 -0
- package/folder/stats.js +12 -75
- package/llms.txt +5 -5
- package/package.json +6 -2
- package/request-handler.js +144 -0
- package/utils/graph-api.js +11 -1
package/folder/delete.js
CHANGED
|
@@ -3,26 +3,21 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Protected folder names that cannot be deleted
|
|
10
|
-
*/
|
|
11
|
-
const PROTECTED_FOLDERS = [
|
|
12
|
-
'inbox',
|
|
13
|
-
'drafts',
|
|
14
|
-
'sentitems',
|
|
15
|
-
'deleteditems',
|
|
16
|
-
'junkemail',
|
|
17
|
-
'archive',
|
|
18
|
-
'outbox',
|
|
19
|
-
];
|
|
6
|
+
const { resolveFolder, WELL_KNOWN } = require('./resolve');
|
|
20
7
|
|
|
21
8
|
/**
|
|
22
9
|
* Delete folder handler
|
|
10
|
+
*
|
|
11
|
+
* System folders are guarded by NAME/alias below — `WELL_KNOWN` covers the
|
|
12
|
+
* Graph names, display-name variants ("Sent Items", "Deleted Items") and short
|
|
13
|
+
* aliases ("sent"/"junk"/"spam"). A raw `folderId` that happens to point at a
|
|
14
|
+
* system folder is backstopped by Graph, which rejects deleting distinguished
|
|
15
|
+
* folders (mailFolder exposes no selectable `wellKnownName`, so we can't cheaply
|
|
16
|
+
* re-check protection on a resolved-by-ID folder).
|
|
17
|
+
*
|
|
23
18
|
* @param {object} args - Tool arguments
|
|
24
19
|
* @param {string} [args.folderId] - Folder ID to delete
|
|
25
|
-
* @param {string} [args.folderName] - Folder name to delete (resolved to ID)
|
|
20
|
+
* @param {string} [args.folderName] - Folder name/path to delete (resolved to ID)
|
|
26
21
|
* @returns {object} - MCP response
|
|
27
22
|
*/
|
|
28
23
|
async function handleDeleteFolder(args) {
|
|
@@ -39,8 +34,8 @@ async function handleDeleteFolder(args) {
|
|
|
39
34
|
};
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
//
|
|
43
|
-
if (folderName &&
|
|
37
|
+
// Name/alias guard for the common accidental case.
|
|
38
|
+
if (folderName && WELL_KNOWN[folderName.toLowerCase().trim()]) {
|
|
44
39
|
return {
|
|
45
40
|
content: [
|
|
46
41
|
{
|
|
@@ -54,32 +49,27 @@ async function handleDeleteFolder(args) {
|
|
|
54
49
|
try {
|
|
55
50
|
const accessToken = await ensureAuthenticated();
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
}
|
|
52
|
+
// Resolve (by name/path OR explicit ID) so nested folders are addressable
|
|
53
|
+
// and the confirmation can report the full path. (#216)
|
|
54
|
+
let resolved;
|
|
55
|
+
try {
|
|
56
|
+
resolved = await resolveFolder(accessToken, {
|
|
57
|
+
id: folderId,
|
|
58
|
+
name: folderName,
|
|
59
|
+
});
|
|
60
|
+
} catch (resolveError) {
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: 'text', text: resolveError.message }],
|
|
63
|
+
};
|
|
72
64
|
}
|
|
73
65
|
|
|
74
66
|
// Delete the folder
|
|
75
|
-
await callGraphAPI(accessToken, 'DELETE', `me/mailFolders/${
|
|
76
|
-
|
|
77
|
-
const displayName = folderName || resolvedId;
|
|
67
|
+
await callGraphAPI(accessToken, 'DELETE', `me/mailFolders/${resolved.id}`);
|
|
78
68
|
return {
|
|
79
69
|
content: [
|
|
80
70
|
{
|
|
81
71
|
type: 'text',
|
|
82
|
-
text: `Folder "${
|
|
72
|
+
text: `Folder "${resolved.path}" deleted successfully.`,
|
|
83
73
|
},
|
|
84
74
|
],
|
|
85
75
|
};
|
package/folder/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const folderTools = [
|
|
|
12
12
|
{
|
|
13
13
|
name: 'folders',
|
|
14
14
|
description:
|
|
15
|
-
|
|
15
|
+
"Manage mail folders (tool-level destructiveHint=true because `delete` permanently removes a folder; `list` and `stats` are read-only sub-actions despite the annotation). Folders can be addressed by name, by a slash-separated PATH for nested folders (e.g. `Triage/Delete`, `Inbox/Clients/Acme`, case-insensitive), or by explicit ID; `list` output includes each folder's full path and `[id: …]`. A bare name resolves a unique top-level folder first, then searches nested folders (ambiguous names return the candidates — disambiguate with a path or ID). action=`list` (default) returns the folder tree (toggle `includeItemCounts` for unread/total, `includeChildren` for hierarchy). action=`create` makes a new folder under the root, or under `parentFolder` (name/path) / `parentFolderId`, and returns its id. action=`move` relocates emails (`emailIds`) into `targetFolder` (name/path) or `targetFolderId`. action=`stats` returns counts (totalItemCount/unreadItemCount) for `folder` (name/path) or `folderId`, suitable for pagination planning. action=`delete` removes a folder (by `folderName`/path or `folderId`) and its contents — on Outlook.com the folder is moved to Deleted Items (recoverable until you empty it); M365/Exchange accounts may hard-delete per retention policy.",
|
|
16
16
|
annotations: {
|
|
17
17
|
title: 'Mail Folders',
|
|
18
18
|
readOnlyHint: false,
|
|
@@ -43,7 +43,13 @@ const folderTools = [
|
|
|
43
43
|
},
|
|
44
44
|
parentFolder: {
|
|
45
45
|
type: 'string',
|
|
46
|
-
description:
|
|
46
|
+
description:
|
|
47
|
+
'Parent folder name or path (e.g. "Clients/Acme"); default is root (action=create)',
|
|
48
|
+
},
|
|
49
|
+
parentFolderId: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description:
|
|
52
|
+
'Parent folder ID — alternative to parentFolder for unambiguous targeting (action=create)',
|
|
47
53
|
},
|
|
48
54
|
// move params
|
|
49
55
|
emailIds: {
|
|
@@ -53,7 +59,13 @@ const folderTools = [
|
|
|
53
59
|
},
|
|
54
60
|
targetFolder: {
|
|
55
61
|
type: 'string',
|
|
56
|
-
description:
|
|
62
|
+
description:
|
|
63
|
+
'Destination folder name or path, e.g. "Triage/Delete" (action=move; or use targetFolderId)',
|
|
64
|
+
},
|
|
65
|
+
targetFolderId: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description:
|
|
68
|
+
'Destination folder ID — alternative to targetFolder for unambiguous/nested targeting (action=move)',
|
|
57
69
|
},
|
|
58
70
|
sourceFolder: {
|
|
59
71
|
type: 'string',
|
|
@@ -63,22 +75,22 @@ const folderTools = [
|
|
|
63
75
|
folder: {
|
|
64
76
|
type: 'string',
|
|
65
77
|
description:
|
|
66
|
-
'Folder name (inbox, sent,
|
|
78
|
+
'Folder name or path (inbox, sent, "Triage/Delete", etc.). Default: inbox (action=stats)',
|
|
67
79
|
},
|
|
68
80
|
outputVerbosity: {
|
|
69
81
|
type: 'string',
|
|
70
82
|
enum: ['minimal', 'standard', 'full'],
|
|
71
83
|
description: 'Output detail level (action=stats, default: standard)',
|
|
72
84
|
},
|
|
73
|
-
// delete params
|
|
85
|
+
// delete/stats params
|
|
74
86
|
folderId: {
|
|
75
87
|
type: 'string',
|
|
76
|
-
description: 'Folder ID
|
|
88
|
+
description: 'Folder ID (action=stats/delete)',
|
|
77
89
|
},
|
|
78
90
|
folderName: {
|
|
79
91
|
type: 'string',
|
|
80
92
|
description:
|
|
81
|
-
'Folder name to delete — resolved to ID (action=delete). Cannot delete protected folders (Inbox, Drafts, Sent, etc.)',
|
|
93
|
+
'Folder name or path to delete — resolved to ID (action=delete). Cannot delete protected folders (Inbox, Drafts, Sent, etc.)',
|
|
82
94
|
},
|
|
83
95
|
},
|
|
84
96
|
additionalProperties: false,
|
package/folder/list.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* List folders functionality
|
|
3
3
|
*/
|
|
4
|
-
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
4
|
const { ensureAuthenticated } = require('../auth');
|
|
5
|
+
const { listChildFolders } = require('./resolve');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* List folders handler
|
|
@@ -74,73 +74,54 @@ async function handleListFolders(args) {
|
|
|
74
74
|
* @returns {Promise<Array>} - Array of folder objects with hierarchy
|
|
75
75
|
*/
|
|
76
76
|
async function getAllFoldersHierarchy(accessToken, includeItemCounts) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
77
|
+
// Determine select fields based on whether to include counts
|
|
78
|
+
const selectFields = includeItemCounts
|
|
79
|
+
? 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount'
|
|
80
|
+
: 'id,displayName,parentFolderId,childFolderCount';
|
|
81
|
+
|
|
82
|
+
// Full recursive, paginated walk so nested folders at ANY depth appear with
|
|
83
|
+
// their complete path (not just one level). (#216 review)
|
|
84
|
+
const top = await listChildFolders(accessToken, null, selectFields);
|
|
85
|
+
const all = [];
|
|
86
|
+
const visited = new Set();
|
|
87
|
+
const queue = top.map((folder) => ({
|
|
88
|
+
folder,
|
|
89
|
+
path: folder.displayName,
|
|
90
|
+
parentPath: null,
|
|
91
|
+
depth: 1,
|
|
92
|
+
isTopLevel: true,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < queue.length; i++) {
|
|
96
|
+
const { folder, path, parentPath, depth, isTopLevel } = queue[i];
|
|
97
|
+
if (visited.has(folder.id)) {
|
|
98
|
+
continue;
|
|
97
99
|
}
|
|
100
|
+
visited.add(folder.id);
|
|
101
|
+
all.push({ ...folder, path, parentFolder: parentPath, isTopLevel });
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
(f) => f.childFolderCount > 0
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const childFolderPromises = foldersWithChildren.map(async (folder) => {
|
|
103
|
+
if (folder.childFolderCount > 0 && depth < 20) {
|
|
104
|
+
let children;
|
|
105
105
|
try {
|
|
106
|
-
|
|
107
|
-
accessToken,
|
|
108
|
-
'GET',
|
|
109
|
-
`me/mailFolders/${folder.id}/childFolders`,
|
|
110
|
-
null,
|
|
111
|
-
{ $select: selectFields }
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// Add parent folder info to each child
|
|
115
|
-
const childFolders = childResponse.value || [];
|
|
116
|
-
childFolders.forEach((child) => {
|
|
117
|
-
child.parentFolder = folder.displayName;
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
return childFolders;
|
|
106
|
+
children = await listChildFolders(accessToken, folder.id, selectFields);
|
|
121
107
|
} catch (error) {
|
|
122
108
|
console.error(
|
|
123
109
|
`Error getting child folders for "${folder.displayName}": ${error.message}`
|
|
124
110
|
);
|
|
125
|
-
|
|
111
|
+
continue;
|
|
126
112
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Combine all folders
|
|
139
|
-
return [...topLevelFolders, ...allChildFolders];
|
|
140
|
-
} catch (error) {
|
|
141
|
-
console.error(`Error getting all folders: ${error.message}`);
|
|
142
|
-
throw error;
|
|
113
|
+
for (const child of children) {
|
|
114
|
+
queue.push({
|
|
115
|
+
folder: child,
|
|
116
|
+
path: `${path}/${child.displayName}`,
|
|
117
|
+
parentPath: path,
|
|
118
|
+
depth: depth + 1,
|
|
119
|
+
isTopLevel: false,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
143
123
|
}
|
|
124
|
+
return all;
|
|
144
125
|
}
|
|
145
126
|
|
|
146
127
|
/**
|
|
@@ -184,14 +165,13 @@ function formatFolderList(folders, includeItemCounts) {
|
|
|
184
165
|
return a.displayName.localeCompare(b.displayName);
|
|
185
166
|
});
|
|
186
167
|
|
|
187
|
-
// Format each folder
|
|
168
|
+
// Format each folder. Emit the full path and folder ID so callers can
|
|
169
|
+
// address nested folders directly — `folders move targetFolder="Parent/Child"`
|
|
170
|
+
// or `targetFolderId=...`. (#216)
|
|
188
171
|
const folderLines = sortedFolders.map((folder) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (folder.parentFolder) {
|
|
193
|
-
folderInfo += ` (in ${folder.parentFolder})`;
|
|
194
|
-
}
|
|
172
|
+
// Full path (computed during the recursive walk) so nested folders are
|
|
173
|
+
// addressable directly. (#216)
|
|
174
|
+
let folderInfo = folder.path || folder.displayName;
|
|
195
175
|
|
|
196
176
|
// Add item counts if requested
|
|
197
177
|
if (includeItemCounts) {
|
|
@@ -204,6 +184,8 @@ function formatFolderList(folders, includeItemCounts) {
|
|
|
204
184
|
}
|
|
205
185
|
}
|
|
206
186
|
|
|
187
|
+
folderInfo += ` [id: ${folder.id}]`;
|
|
188
|
+
|
|
207
189
|
return folderInfo;
|
|
208
190
|
});
|
|
209
191
|
|
|
@@ -269,6 +251,9 @@ function formatFolderHierarchy(folders, includeItemCounts) {
|
|
|
269
251
|
}
|
|
270
252
|
}
|
|
271
253
|
|
|
254
|
+
// Surface the folder ID so callers can address it directly. (#216)
|
|
255
|
+
line += ` [id: ${folder.id}]`;
|
|
256
|
+
|
|
272
257
|
// Add children
|
|
273
258
|
const childLines = folder.children
|
|
274
259
|
.map((childId) => formatSubtree(childId, level + 1))
|
package/folder/move.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
|
-
const {
|
|
6
|
+
const { resolveFolder } = require('./resolve');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Move emails handler
|
|
@@ -13,6 +13,7 @@ const { getFolderIdByName } = require('../email/folder-utils');
|
|
|
13
13
|
async function handleMoveEmails(args) {
|
|
14
14
|
const emailIds = args.emailIds || '';
|
|
15
15
|
const targetFolder = args.targetFolder || '';
|
|
16
|
+
const targetFolderId = args.targetFolderId || '';
|
|
16
17
|
const sourceFolder = args.sourceFolder || '';
|
|
17
18
|
|
|
18
19
|
if (!emailIds) {
|
|
@@ -26,12 +27,12 @@ async function handleMoveEmails(args) {
|
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
if (!targetFolder) {
|
|
30
|
+
if (!targetFolder && !targetFolderId) {
|
|
30
31
|
return {
|
|
31
32
|
content: [
|
|
32
33
|
{
|
|
33
34
|
type: 'text',
|
|
34
|
-
text: 'Target folder name
|
|
35
|
+
text: 'Target folder is required — pass `targetFolder` (name or "Parent/Child" path) or `targetFolderId`.',
|
|
35
36
|
},
|
|
36
37
|
],
|
|
37
38
|
};
|
|
@@ -62,7 +63,7 @@ async function handleMoveEmails(args) {
|
|
|
62
63
|
const result = await moveEmailsToFolder(
|
|
63
64
|
accessToken,
|
|
64
65
|
ids,
|
|
65
|
-
targetFolder,
|
|
66
|
+
{ name: targetFolder, id: targetFolderId },
|
|
66
67
|
sourceFolder
|
|
67
68
|
);
|
|
68
69
|
|
|
@@ -101,28 +102,27 @@ async function handleMoveEmails(args) {
|
|
|
101
102
|
* Move emails to a folder
|
|
102
103
|
* @param {string} accessToken - Access token
|
|
103
104
|
* @param {Array<string>} emailIds - Array of email IDs to move
|
|
104
|
-
* @param {string}
|
|
105
|
+
* @param {{name?: string, id?: string}} targetSpec - Target folder name/path or ID
|
|
105
106
|
* @param {string} sourceFolderName - Name of the source folder (optional)
|
|
106
107
|
* @returns {Promise<object>} - Result object with status and message
|
|
107
108
|
*/
|
|
108
109
|
async function moveEmailsToFolder(
|
|
109
110
|
accessToken,
|
|
110
111
|
emailIds,
|
|
111
|
-
|
|
112
|
+
targetSpec,
|
|
112
113
|
_sourceFolderName
|
|
113
114
|
) {
|
|
114
115
|
try {
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
success: false,
|
|
123
|
-
message: `Target folder "${targetFolderName}" not found. Please specify a valid folder name.`,
|
|
124
|
-
};
|
|
116
|
+
// Resolve the target folder (supports "Parent/Child" paths, aliases, and
|
|
117
|
+
// explicit IDs — nested folders are now addressable). (#216)
|
|
118
|
+
let target;
|
|
119
|
+
try {
|
|
120
|
+
target = await resolveFolder(accessToken, targetSpec);
|
|
121
|
+
} catch (resolveError) {
|
|
122
|
+
return { success: false, message: resolveError.message };
|
|
125
123
|
}
|
|
124
|
+
const targetFolderId = target.id;
|
|
125
|
+
const targetLabel = target.path;
|
|
126
126
|
|
|
127
127
|
// Track successful and failed moves
|
|
128
128
|
const results = {
|
|
@@ -152,7 +152,7 @@ async function moveEmailsToFolder(
|
|
|
152
152
|
let message = '';
|
|
153
153
|
|
|
154
154
|
if (results.successful.length > 0) {
|
|
155
|
-
message += `Successfully moved ${results.successful.length} email(s) to "${
|
|
155
|
+
message += `Successfully moved ${results.successful.length} email(s) to "${targetLabel}".`;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
if (results.failed.length > 0) {
|