aiquila-mcp 0.3.10 → 0.3.11
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/dist/tools/apps/calendar.js +94 -9
- package/dist/tools/apps/mail.js +122 -30
- package/dist/tools/system/search.js +9 -2
- package/package.json +1 -1
|
@@ -464,6 +464,45 @@ function formatCalendar(cal) {
|
|
|
464
464
|
// ---------------------------------------------------------------------------
|
|
465
465
|
// CalDAV helpers
|
|
466
466
|
// ---------------------------------------------------------------------------
|
|
467
|
+
/**
|
|
468
|
+
* Resolve a calendar identifier (display name or URL slug) to its URL slug.
|
|
469
|
+
* Falls back to the input unchanged when no match is found or PROPFIND fails,
|
|
470
|
+
* so existing slug-based callers keep working.
|
|
471
|
+
*/
|
|
472
|
+
async function resolveCalendarSlug(nameOrSlug) {
|
|
473
|
+
const config = getNextcloudConfig();
|
|
474
|
+
const calDavUrl = `${config.url}/remote.php/dav/calendars/${config.user}/`;
|
|
475
|
+
const propfindBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
476
|
+
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
477
|
+
<d:prop><d:resourcetype /><d:displayname /></d:prop>
|
|
478
|
+
</d:propfind>`;
|
|
479
|
+
try {
|
|
480
|
+
const response = await fetchCalDAV(calDavUrl, {
|
|
481
|
+
method: 'PROPFIND',
|
|
482
|
+
body: propfindBody,
|
|
483
|
+
headers: { Depth: '1' },
|
|
484
|
+
});
|
|
485
|
+
if (!response.ok)
|
|
486
|
+
return nameOrSlug;
|
|
487
|
+
const text = await response.text();
|
|
488
|
+
const calendars = parseCalendars(text);
|
|
489
|
+
for (const cal of calendars) {
|
|
490
|
+
const slug = cal.url.split('/').filter(Boolean).pop() ?? '';
|
|
491
|
+
if (slug === nameOrSlug)
|
|
492
|
+
return nameOrSlug;
|
|
493
|
+
}
|
|
494
|
+
const lower = nameOrSlug.toLowerCase();
|
|
495
|
+
for (const cal of calendars) {
|
|
496
|
+
if (cal.displayName.toLowerCase() === lower) {
|
|
497
|
+
return cal.url.split('/').filter(Boolean).pop() ?? nameOrSlug;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
/* fall through — keep original input */
|
|
503
|
+
}
|
|
504
|
+
return nameOrSlug;
|
|
505
|
+
}
|
|
467
506
|
/**
|
|
468
507
|
* Assert that a calendar supports VEVENT. Throws a descriptive error if not,
|
|
469
508
|
* so create_event fails early with a useful message instead of a 403 from the server.
|
|
@@ -481,7 +520,9 @@ async function assertCalendarSupportsEvents(calendarUrl, calendarName) {
|
|
|
481
520
|
headers: { Depth: '0' },
|
|
482
521
|
});
|
|
483
522
|
if (response.status === 404) {
|
|
484
|
-
|
|
523
|
+
const err = new Error(`Calendar "${calendarName}" not found. Use list_calendars to find available calendar names.`);
|
|
524
|
+
err.code = 'CALENDAR_NOT_FOUND';
|
|
525
|
+
throw err;
|
|
485
526
|
}
|
|
486
527
|
if (!response.ok)
|
|
487
528
|
return; // Can't check — let the server reject if needed
|
|
@@ -504,7 +545,8 @@ async function assertCalendarSupportsEvents(calendarUrl, calendarName) {
|
|
|
504
545
|
*/
|
|
505
546
|
async function resolveEventByUid(calendarName, uid) {
|
|
506
547
|
const config = getNextcloudConfig();
|
|
507
|
-
|
|
548
|
+
let slug = calendarName;
|
|
549
|
+
const buildUrl = () => `${config.url}/remote.php/dav/calendars/${config.user}/${slug}/`;
|
|
508
550
|
const reportBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
509
551
|
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
510
552
|
<d:prop>
|
|
@@ -521,11 +563,22 @@ async function resolveEventByUid(calendarName, uid) {
|
|
|
521
563
|
</c:comp-filter>
|
|
522
564
|
</c:filter>
|
|
523
565
|
</c:calendar-query>`;
|
|
524
|
-
|
|
566
|
+
let response = await fetchCalDAV(buildUrl(), {
|
|
525
567
|
method: 'REPORT',
|
|
526
568
|
body: reportBody,
|
|
527
569
|
headers: { Depth: '1' },
|
|
528
570
|
});
|
|
571
|
+
if (response.status === 404) {
|
|
572
|
+
const resolved = await resolveCalendarSlug(calendarName);
|
|
573
|
+
if (resolved !== calendarName) {
|
|
574
|
+
slug = resolved;
|
|
575
|
+
response = await fetchCalDAV(buildUrl(), {
|
|
576
|
+
method: 'REPORT',
|
|
577
|
+
body: reportBody,
|
|
578
|
+
headers: { Depth: '1' },
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
529
582
|
if (!response.ok) {
|
|
530
583
|
const errorText = await response.text();
|
|
531
584
|
throw new Error(`CalDAV REPORT failed for calendar "${calendarName}": ${response.status} - ${errorText}`);
|
|
@@ -639,7 +692,8 @@ export const listEventsTool = {
|
|
|
639
692
|
handler: async (args) => {
|
|
640
693
|
try {
|
|
641
694
|
const config = getNextcloudConfig();
|
|
642
|
-
|
|
695
|
+
let slug = args.calendarName;
|
|
696
|
+
const buildUrl = () => `${config.url}/remote.php/dav/calendars/${config.user}/${slug}/`;
|
|
643
697
|
const limit = args.limit || 50;
|
|
644
698
|
// Default time range: today to 30 days from now
|
|
645
699
|
const now = new Date();
|
|
@@ -663,11 +717,22 @@ export const listEventsTool = {
|
|
|
663
717
|
</c:comp-filter>
|
|
664
718
|
</c:filter>
|
|
665
719
|
</c:calendar-query>`;
|
|
666
|
-
|
|
720
|
+
let response = await fetchCalDAV(buildUrl(), {
|
|
667
721
|
method: 'REPORT',
|
|
668
722
|
body: reportBody,
|
|
669
723
|
headers: { Depth: '1' },
|
|
670
724
|
});
|
|
725
|
+
if (response.status === 404) {
|
|
726
|
+
const resolved = await resolveCalendarSlug(args.calendarName);
|
|
727
|
+
if (resolved !== args.calendarName) {
|
|
728
|
+
slug = resolved;
|
|
729
|
+
response = await fetchCalDAV(buildUrl(), {
|
|
730
|
+
method: 'REPORT',
|
|
731
|
+
body: reportBody,
|
|
732
|
+
headers: { Depth: '1' },
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
671
736
|
if (!response.ok) {
|
|
672
737
|
const errorText = await response.text();
|
|
673
738
|
throw new Error(`CalDAV REPORT failed for calendar "${args.calendarName}": ${response.status} - ${errorText}`);
|
|
@@ -824,10 +889,30 @@ export const createEventTool = {
|
|
|
824
889
|
try {
|
|
825
890
|
const config = getNextcloudConfig();
|
|
826
891
|
const eventUid = `${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
827
|
-
|
|
892
|
+
let slug = args.calendarName;
|
|
893
|
+
const baseUrl = () => `${config.url}/remote.php/dav/calendars/${config.user}/${slug}/`;
|
|
894
|
+
// Validate the calendar supports VEVENT before building and sending the iCal payload.
|
|
895
|
+
// If the original name is a display name (not a slug), retry once after resolving.
|
|
896
|
+
try {
|
|
897
|
+
await assertCalendarSupportsEvents(baseUrl(), args.calendarName);
|
|
898
|
+
}
|
|
899
|
+
catch (err) {
|
|
900
|
+
if (err.code === 'CALENDAR_NOT_FOUND') {
|
|
901
|
+
const resolved = await resolveCalendarSlug(args.calendarName);
|
|
902
|
+
if (resolved !== args.calendarName) {
|
|
903
|
+
slug = resolved;
|
|
904
|
+
await assertCalendarSupportsEvents(baseUrl(), args.calendarName);
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
throw err;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
throw err;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const calendarBaseUrl = baseUrl();
|
|
828
915
|
const calDavUrl = `${calendarBaseUrl}${eventUid}.ics`;
|
|
829
|
-
// Validate the calendar supports VEVENT before building and sending the iCal payload
|
|
830
|
-
await assertCalendarSupportsEvents(calendarBaseUrl, args.calendarName);
|
|
831
916
|
const now = icalNow();
|
|
832
917
|
const isAllDay = args.dtstart.length === 8;
|
|
833
918
|
const tzid = !isAllDay ? args.tzid : undefined;
|
|
@@ -930,7 +1015,7 @@ export const createEventTool = {
|
|
|
930
1015
|
}
|
|
931
1016
|
// Verify the event was actually persisted
|
|
932
1017
|
try {
|
|
933
|
-
await resolveEventByUid(
|
|
1018
|
+
await resolveEventByUid(slug, eventUid);
|
|
934
1019
|
}
|
|
935
1020
|
catch {
|
|
936
1021
|
throw new Error(`Event creation appeared to succeed (HTTP ${response.status}) but the event ` +
|
package/dist/tools/apps/mail.js
CHANGED
|
@@ -2,13 +2,24 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { fetchMailAPI } from '../../client/mail.js';
|
|
4
4
|
import { fetchOCS } from '../../client/ocs.js';
|
|
5
|
+
// Invisible padding characters used in newsletter preheaders
|
|
6
|
+
// (U+034F, U+00AD, U+200B–U+200D, U+FEFF)
|
|
7
|
+
const INVISIBLE_CHARS_RE = /[\u034F\u00AD\u200B-\u200D\uFEFF]/g;
|
|
5
8
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
6
9
|
function stripHtml(html) {
|
|
7
10
|
let text = html;
|
|
8
11
|
// Remove style and script blocks
|
|
9
12
|
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
10
13
|
text = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
11
|
-
//
|
|
14
|
+
// Image-based newsletters carry content in alt attributes
|
|
15
|
+
text = text.replace(/<img[^>]+alt="([^"]{2,})"[^>]*\/?>/gi, (_, alt) => `\n${alt.trim()}\n`);
|
|
16
|
+
text = text.replace(/<img[^>]+alt='([^']{2,})'[^>]*\/?>/gi, (_, alt) => `\n${alt.trim()}\n`);
|
|
17
|
+
text = text.replace(/<img[^>]*\/?>/gi, '');
|
|
18
|
+
// Block-level structure
|
|
19
|
+
text = text.replace(/<\/h[1-6]>/gi, '\n\n');
|
|
20
|
+
text = text.replace(/<\/tr>/gi, '\n');
|
|
21
|
+
text = text.replace(/<td[^>]*>/gi, ' ');
|
|
22
|
+
text = text.replace(/<th[^>]*>/gi, ' ');
|
|
12
23
|
text = text.replace(/<br\s*\/?>/gi, '\n');
|
|
13
24
|
text = text.replace(/<\/p>/gi, '\n\n');
|
|
14
25
|
text = text.replace(/<\/div>/gi, '\n');
|
|
@@ -23,7 +34,15 @@ function stripHtml(html) {
|
|
|
23
34
|
text = text.replace(/"/g, '"');
|
|
24
35
|
text = text.replace(/'/g, "'");
|
|
25
36
|
text = text.replace(/ /g, ' ');
|
|
26
|
-
|
|
37
|
+
text = text.replace(/&#\d+;/g, '');
|
|
38
|
+
// Strip invisible preheader padding
|
|
39
|
+
text = text.replace(INVISIBLE_CHARS_RE, '');
|
|
40
|
+
// Trim per-line and drop blanks before collapsing
|
|
41
|
+
text = text
|
|
42
|
+
.split('\n')
|
|
43
|
+
.map((line) => line.trim())
|
|
44
|
+
.filter((line) => line.length > 0)
|
|
45
|
+
.join('\n');
|
|
27
46
|
text = text.replace(/\n{3,}/g, '\n\n');
|
|
28
47
|
return text.trim();
|
|
29
48
|
}
|
|
@@ -80,17 +99,20 @@ const listMailboxesTool = {
|
|
|
80
99
|
}),
|
|
81
100
|
handler: async (args) => {
|
|
82
101
|
try {
|
|
83
|
-
const response = await fetchMailAPI(`/
|
|
102
|
+
const response = await fetchMailAPI(`/mailboxes?accountId=${args.accountId}`);
|
|
84
103
|
if (!response.ok) {
|
|
85
104
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
86
105
|
}
|
|
87
|
-
const
|
|
106
|
+
const data = (await response.json());
|
|
107
|
+
const mailboxes = Array.isArray(data) ? data : (data.mailboxes ?? []);
|
|
88
108
|
if (mailboxes.length === 0) {
|
|
89
109
|
return { content: [{ type: 'text', text: 'No mailboxes found.' }] };
|
|
90
110
|
}
|
|
91
111
|
const lines = mailboxes.map((m) => {
|
|
112
|
+
const id = m.databaseId ?? m.id;
|
|
113
|
+
const name = m.displayName ?? m.name;
|
|
92
114
|
const unread = m.unread > 0 ? ` (${m.unread} unread)` : '';
|
|
93
|
-
return `• ${
|
|
115
|
+
return `• ${name}${unread} — ID: ${id}`;
|
|
94
116
|
});
|
|
95
117
|
return {
|
|
96
118
|
content: [{ type: 'text', text: `Mailboxes:\n${lines.join('\n')}` }],
|
|
@@ -128,19 +150,24 @@ const listMessagesTool = {
|
|
|
128
150
|
}),
|
|
129
151
|
handler: async (args) => {
|
|
130
152
|
try {
|
|
131
|
-
const
|
|
153
|
+
const queryParts = [`mailboxId=${args.mailboxId}`];
|
|
132
154
|
if (args.limit)
|
|
133
|
-
|
|
155
|
+
queryParts.push(`limit=${args.limit}`);
|
|
134
156
|
if (args.cursor)
|
|
135
|
-
|
|
157
|
+
queryParts.push(`cursor=${args.cursor}`);
|
|
136
158
|
if (args.filter && args.filter !== 'all')
|
|
137
|
-
|
|
138
|
-
const response = await
|
|
139
|
-
|
|
159
|
+
queryParts.push(`filter=${args.filter}`);
|
|
160
|
+
const response = await fetchMailAPI(`/messages?${queryParts.join('&')}`);
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
163
|
+
}
|
|
164
|
+
const data = (await response.json());
|
|
165
|
+
const messages = Array.isArray(data) ? data : (data.messages ?? data.data ?? []);
|
|
140
166
|
if (!messages || messages.length === 0) {
|
|
141
167
|
return { content: [{ type: 'text', text: 'No messages found.' }] };
|
|
142
168
|
}
|
|
143
169
|
const lines = messages.map((msg) => {
|
|
170
|
+
const id = msg.databaseId ?? msg.id;
|
|
144
171
|
const flags = [];
|
|
145
172
|
if (!msg.flags.seen)
|
|
146
173
|
flags.push('UNREAD');
|
|
@@ -148,15 +175,16 @@ const listMessagesTool = {
|
|
|
148
175
|
flags.push('starred');
|
|
149
176
|
if (msg.flags.answered)
|
|
150
177
|
flags.push('replied');
|
|
151
|
-
if (msg.hasAttachments)
|
|
178
|
+
if (msg.flags?.hasAttachments ?? msg.hasAttachments)
|
|
152
179
|
flags.push('attachment');
|
|
153
180
|
const flagStr = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
|
|
154
181
|
const from = formatRecipients(msg.from);
|
|
155
|
-
return `• ${msg.subject}${flagStr}\n From: ${from} | ${formatDate(msg.dateInt)} | ID: ${
|
|
182
|
+
return `• ${msg.subject}${flagStr}\n From: ${from} | ${formatDate(msg.dateInt)} | ID: ${id}`;
|
|
156
183
|
});
|
|
157
184
|
let text = `Messages (${messages.length}):\n${lines.join('\n')}`;
|
|
158
185
|
if (messages.length >= (args.limit || 20)) {
|
|
159
|
-
const
|
|
186
|
+
const last = messages[messages.length - 1];
|
|
187
|
+
const lastId = last.databaseId ?? last.id;
|
|
160
188
|
text += `\n\nMore messages available. Use cursor: ${lastId} to load next page.`;
|
|
161
189
|
}
|
|
162
190
|
return { content: [{ type: 'text', text }] };
|
|
@@ -182,29 +210,25 @@ const readMessageTool = {
|
|
|
182
210
|
}),
|
|
183
211
|
handler: async (args) => {
|
|
184
212
|
try {
|
|
185
|
-
//
|
|
186
|
-
const metaResponse = await fetchOCS(`/ocs/v2.php/apps/mail/api/v1/message/${args.messageId}`);
|
|
187
|
-
const meta = metaResponse.ocs.data;
|
|
188
|
-
// Fetch body via Mail REST API
|
|
213
|
+
// Mail 5.x: /messages/{id}/body returns both metadata and body in one call.
|
|
189
214
|
const bodyResponse = await fetchMailAPI(`/messages/${args.messageId}/body`);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const bodyData = (await bodyResponse.json());
|
|
193
|
-
const rawBody = bodyData.body || bodyData.data || '';
|
|
194
|
-
bodyText = stripHtml(rawBody);
|
|
215
|
+
if (!bodyResponse.ok) {
|
|
216
|
+
throw new Error(`HTTP ${bodyResponse.status}: ${await bodyResponse.text()}`);
|
|
195
217
|
}
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const
|
|
218
|
+
const data = (await bodyResponse.json());
|
|
219
|
+
const rawBody = data.body || '';
|
|
220
|
+
const bodyText = rawBody ? stripHtml(rawBody) : '(no body)';
|
|
221
|
+
const from = formatRecipients(data.from);
|
|
222
|
+
const to = formatRecipients(data.to);
|
|
223
|
+
const cc = formatRecipients(data.cc || []);
|
|
224
|
+
const date = data.dateInt ? formatDate(data.dateInt) : 'Unknown';
|
|
225
|
+
const subject = data.subject || '(No subject)';
|
|
202
226
|
let text = `Subject: ${subject}\nFrom: ${from}\nTo: ${to}`;
|
|
203
227
|
if (cc)
|
|
204
228
|
text += `\nCc: ${cc}`;
|
|
205
229
|
text += `\nDate: ${date}`;
|
|
206
230
|
text += `\n${'─'.repeat(60)}\n${bodyText}`;
|
|
207
|
-
const attachments =
|
|
231
|
+
const attachments = data.attachments;
|
|
208
232
|
if (attachments && attachments.length > 0) {
|
|
209
233
|
text += `\n${'─'.repeat(60)}\nAttachments:`;
|
|
210
234
|
for (const att of attachments) {
|
|
@@ -227,6 +251,73 @@ const readMessageTool = {
|
|
|
227
251
|
}
|
|
228
252
|
},
|
|
229
253
|
};
|
|
254
|
+
const searchMessagesTool = {
|
|
255
|
+
name: 'mail_search_messages',
|
|
256
|
+
description: 'Search email messages across all mailboxes by subject or sender. ' +
|
|
257
|
+
'Returns matches with message IDs usable in mail_read_message. ' +
|
|
258
|
+
'For threaded conversations all message IDs in the thread are returned.',
|
|
259
|
+
inputSchema: z.object({
|
|
260
|
+
query: z.string().describe('Search query (matches subject and sender name)'),
|
|
261
|
+
limit: z.number().min(1).max(20).optional().describe('Max results (default: 10)'),
|
|
262
|
+
}),
|
|
263
|
+
handler: async (args) => {
|
|
264
|
+
try {
|
|
265
|
+
const response = await fetchOCS('/ocs/v2.php/search/providers/mail/search', {
|
|
266
|
+
queryParams: {
|
|
267
|
+
term: args.query,
|
|
268
|
+
limit: String(args.limit ?? 10),
|
|
269
|
+
format: 'json',
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const results = response.ocs.data;
|
|
273
|
+
if (!results.entries?.length) {
|
|
274
|
+
return {
|
|
275
|
+
content: [{ type: 'text', text: `No messages found for "${args.query}".` }],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const lines = await Promise.all(results.entries.map(async (entry) => {
|
|
279
|
+
const match = entry.resourceUrl?.match(/\/box\/(\d+)\/thread\/(\d+)/);
|
|
280
|
+
if (!match)
|
|
281
|
+
return `• ${entry.title}\n ${entry.subline}`;
|
|
282
|
+
const msgId = parseInt(match[2], 10);
|
|
283
|
+
try {
|
|
284
|
+
const threadResp = await fetchMailAPI(`/messages/${msgId}/thread`);
|
|
285
|
+
if (!threadResp.ok)
|
|
286
|
+
throw new Error('thread fetch failed');
|
|
287
|
+
const threadData = (await threadResp.json());
|
|
288
|
+
const msgs = (Array.isArray(threadData)
|
|
289
|
+
? threadData
|
|
290
|
+
: (threadData.messages ?? []));
|
|
291
|
+
const sorted = msgs.sort((a, b) => (a.dateInt ?? 0) - (b.dateInt ?? 0));
|
|
292
|
+
if (sorted.length <= 1) {
|
|
293
|
+
return `• ${entry.title}\n ${entry.subline} | ID: ${msgId}`;
|
|
294
|
+
}
|
|
295
|
+
const idList = sorted.map((m) => (m.databaseId ?? m.id)).join(', ');
|
|
296
|
+
return `• ${entry.title}\n ${entry.subline} | Thread (${sorted.length} messages) IDs: ${idList}`;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return `• ${entry.title}\n ${entry.subline} | ID: ${msgId}`;
|
|
300
|
+
}
|
|
301
|
+
}));
|
|
302
|
+
let text = `Search results for "${args.query}" (${results.entries.length}):\n${lines.join('\n')}`;
|
|
303
|
+
if (results.isPaginated && results.cursor) {
|
|
304
|
+
text += `\n\nMore results available.`;
|
|
305
|
+
}
|
|
306
|
+
return { content: [{ type: 'text', text }] };
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
return {
|
|
310
|
+
content: [
|
|
311
|
+
{
|
|
312
|
+
type: 'text',
|
|
313
|
+
text: `Error searching messages: ${error instanceof Error ? error.message : String(error)}`,
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
isError: true,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
};
|
|
230
321
|
const sendMessageTool = {
|
|
231
322
|
name: 'mail_send_message',
|
|
232
323
|
description: 'Send an email message through Nextcloud Mail. Requires an account ID and recipient addresses.',
|
|
@@ -411,6 +502,7 @@ export const mailTools = [
|
|
|
411
502
|
listMailboxesTool,
|
|
412
503
|
listMessagesTool,
|
|
413
504
|
readMessageTool,
|
|
505
|
+
searchMessagesTool,
|
|
414
506
|
sendMessageTool,
|
|
415
507
|
deleteMessageTool,
|
|
416
508
|
moveMessageTool,
|
|
@@ -62,8 +62,15 @@ export const unifiedSearchTool = {
|
|
|
62
62
|
content: [{ type: 'text', text: 'No search providers available.' }],
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
//
|
|
66
|
-
|
|
65
|
+
// Always include the most useful providers; fill remaining slots by order.
|
|
66
|
+
// Without this, mail (order 20) and calendar (order 30) fall outside the
|
|
67
|
+
// top 5 on a typical install and are silently never searched.
|
|
68
|
+
const PRIORITY_IDS = ['files', 'mail', 'calendar', 'notes'];
|
|
69
|
+
const prioritized = PRIORITY_IDS.map((id) => providers.find((p) => p.id === id)).filter((p) => p !== undefined);
|
|
70
|
+
const rest = providers
|
|
71
|
+
.filter((p) => !PRIORITY_IDS.includes(p.id))
|
|
72
|
+
.sort((a, b) => a.order - b.order);
|
|
73
|
+
const topProviders = [...prioritized, ...rest].slice(0, 6);
|
|
67
74
|
const searches = await Promise.allSettled(topProviders.map((p) => searchProvider(p.id, args.query, args.limit)));
|
|
68
75
|
const output = [];
|
|
69
76
|
for (const result of searches) {
|