mcpbrowser 0.3.34 → 0.3.35

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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * read-email.js — Open and read a specific email thread.
3
+ *
4
+ * Tier usage:
5
+ * T1: gmailNavigate to thread by ID hash
6
+ * T2: keyboard shortcut 'o' to open selected row
7
+ * T3: span[email] for sender extraction
8
+ * T4: MESSAGE_CONTAINER, MSG_BODY, MSG_DATE, THREAD_SUBJECT, ATTACHMENT_* selectors
9
+ */
10
+
11
+ import { ErrorResponse } from '../../../core/responses.js';
12
+ import {
13
+ checkPrecondition,
14
+ gmailNavigate,
15
+ checkKeyboardShortcuts,
16
+ selectEmailRow,
17
+ waitForGmail,
18
+ GmailActionResponse
19
+ } from '../helpers.js';
20
+ import {
21
+ THREAD_SUBJECT,
22
+ MESSAGE_CONTAINER,
23
+ MSG_BODY,
24
+ MSG_DATE,
25
+ ATTACHMENT_AREA,
26
+ ATTACHMENT_NAME,
27
+ ATTACHMENT_SIZE
28
+ } from '../selectors.js';
29
+
30
+ /**
31
+ * Open and read a specific email by ID or list index.
32
+ * @param {object} opts
33
+ * @param {import('puppeteer-core').Page} opts.page
34
+ * @param {object} opts.params
35
+ * @param {string} [opts.params.id] - Gmail thread/message ID
36
+ * @param {number} [opts.params.index] - Positional index in current list view
37
+ * @returns {Promise<GmailActionResponse|ErrorResponse>}
38
+ */
39
+ export async function readEmail({ page, params }) {
40
+ if (params.id == null && params.index == null) {
41
+ return new ErrorResponse(
42
+ 'Either id or index is required to read an email.',
43
+ [
44
+ 'Use list_emails to see available emails and their indices',
45
+ 'Use search_emails to find a specific email by keyword'
46
+ ]
47
+ );
48
+ }
49
+
50
+ // Precondition: must be on Gmail
51
+ const pre = await checkPrecondition(page, 'on_gmail');
52
+ if (!pre.met) {
53
+ return new ErrorResponse(pre.error, [
54
+ pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
55
+ ]);
56
+ }
57
+
58
+ if (params.id) {
59
+ // T1: Navigate directly to thread by ID
60
+ await gmailNavigate(page, '#inbox/' + params.id);
61
+ } else {
62
+ // T2: Select row by index, then open with keyboard shortcut
63
+ const kb = await checkKeyboardShortcuts(page);
64
+ if (!kb.enabled) {
65
+ return new ErrorResponse(
66
+ kb.error || 'Keyboard shortcuts are not enabled in Gmail.',
67
+ ['Enable keyboard shortcuts in Gmail Settings → General → Keyboard shortcuts → ON, then reload Gmail.']
68
+ );
69
+ }
70
+
71
+ const sel = await selectEmailRow(page, { index: params.index });
72
+ if (!sel.selected) {
73
+ return new ErrorResponse(
74
+ sel.error || `Could not select email at index ${params.index}.`,
75
+ ['Use list_emails to check available email indices']
76
+ );
77
+ }
78
+
79
+ // T2: Press 'o' to open the selected email
80
+ await page.keyboard.press('o');
81
+ }
82
+
83
+ // Wait for thread content to load
84
+ await waitForGmail(page, THREAD_SUBJECT);
85
+
86
+ // T3+T4: Extract thread data from DOM
87
+ const thread = await page.evaluate((selectors) => {
88
+ const subjectEl = document.querySelector(selectors.threadSubject);
89
+ const subject = subjectEl?.textContent?.trim() || '';
90
+
91
+ const containers = document.querySelectorAll(selectors.messageContainer);
92
+ const messages = [];
93
+
94
+ for (const container of containers) {
95
+ // T3: span[email] for sender info
96
+ const senderEl = container.querySelector('span[email]');
97
+ const sender = senderEl?.getAttribute('name') || senderEl?.textContent?.trim() || '';
98
+ const senderEmail = senderEl?.getAttribute('email') || '';
99
+
100
+ // T4: CSS selectors for body, date, attachments
101
+ const bodyEl = container.querySelector(selectors.msgBody);
102
+ const body = bodyEl?.textContent?.trim() || '';
103
+
104
+ const dateEl = container.querySelector(selectors.msgDate);
105
+ const date = dateEl?.getAttribute('title') || dateEl?.textContent?.trim() || '';
106
+
107
+ const attachments = [];
108
+ const attachArea = container.querySelector(selectors.attachmentArea);
109
+ if (attachArea) {
110
+ const nameEls = attachArea.querySelectorAll(selectors.attachmentName);
111
+ const sizeEls = attachArea.querySelectorAll(selectors.attachmentSize);
112
+ for (let i = 0; i < nameEls.length; i++) {
113
+ attachments.push({
114
+ name: nameEls[i]?.textContent?.trim() || '',
115
+ size: sizeEls[i]?.textContent?.trim() || ''
116
+ });
117
+ }
118
+ }
119
+
120
+ messages.push({ sender, senderEmail, date, body, attachments });
121
+ }
122
+
123
+ // Attempt to extract thread ID from URL hash
124
+ const hash = window.location.hash || '';
125
+ const idMatch = hash.match(/\/([A-Za-z0-9]+)$/);
126
+ const id = idMatch ? idMatch[1] : undefined;
127
+
128
+ return { id, subject, messageCount: messages.length, messages };
129
+ }, {
130
+ threadSubject: THREAD_SUBJECT,
131
+ messageContainer: MESSAGE_CONTAINER,
132
+ msgBody: MSG_BODY,
133
+ msgDate: MSG_DATE,
134
+ attachmentArea: ATTACHMENT_AREA,
135
+ attachmentName: ATTACHMENT_NAME,
136
+ attachmentSize: ATTACHMENT_SIZE
137
+ });
138
+
139
+ return new GmailActionResponse(
140
+ { thread },
141
+ `Email: "${thread.subject}" — ${thread.messageCount} message(s).`,
142
+ [
143
+ 'Use reply_email to respond',
144
+ 'Use forward_email to forward',
145
+ 'Use archive_email to archive',
146
+ 'Use list_emails to return to inbox'
147
+ ]
148
+ );
149
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * reply-email.js — Reply (or reply-all) to the currently open email thread.
3
+ *
4
+ * Tier usage:
5
+ * T2: 'r' for reply, 'a' for reply-all, Ctrl+Enter to send
6
+ * T3: div[aria-label="Message Body"] for reply editor
7
+ */
8
+
9
+ import { ErrorResponse } from '../../../core/responses.js';
10
+ import logger from '../../../core/logger.js';
11
+ import {
12
+ checkPrecondition,
13
+ checkKeyboardShortcuts,
14
+ waitForGmail,
15
+ GmailActionResponse
16
+ } from '../helpers.js';
17
+
18
+ /**
19
+ * Reply to the currently open thread.
20
+ * @param {object} opts
21
+ * @param {import('puppeteer-core').Page} opts.page
22
+ * @param {object} opts.params
23
+ * @param {string} [opts.params.body] - Reply body text
24
+ * @param {boolean} [opts.params.replyAll] - If true, reply-all
25
+ * @param {boolean} [opts.params.send] - If true, send immediately
26
+ * @returns {Promise<GmailActionResponse|ErrorResponse>}
27
+ */
28
+ export async function replyEmail({ page, params }) {
29
+ // Precondition: must be on Gmail
30
+ const pre = await checkPrecondition(page, 'on_gmail');
31
+ if (!pre.met) {
32
+ return new ErrorResponse(pre.error, [
33
+ pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
34
+ ]);
35
+ }
36
+
37
+ // Precondition: must have a thread open
38
+ const threadPre = await checkPrecondition(page, 'thread_open');
39
+ if (!threadPre.met) {
40
+ return new ErrorResponse(threadPre.error, [
41
+ threadPre.suggestion || "Use read_email to open an email thread first."
42
+ ]);
43
+ }
44
+
45
+ // Verify keyboard shortcuts are enabled
46
+ const kb = await checkKeyboardShortcuts(page);
47
+ if (!kb.enabled) {
48
+ return new ErrorResponse(kb.error, [
49
+ 'Enable keyboard shortcuts in Gmail Settings → General → Keyboard shortcuts → ON, then reload Gmail.'
50
+ ]);
51
+ }
52
+
53
+ // T2: Press 'a' for reply-all or 'r' for reply
54
+ const key = params.replyAll ? 'a' : 'r';
55
+ await page.keyboard.press(key);
56
+ logger.debug(`replyEmail: pressed '${key}' for ${params.replyAll ? 'reply-all' : 'reply'}`);
57
+
58
+ // Wait for reply editor
59
+ await waitForGmail(page, 'div[aria-label="Message Body"]');
60
+
61
+ // Fill reply body
62
+ if (params.body) {
63
+ await page.type('div[aria-label="Message Body"]', params.body);
64
+ }
65
+
66
+ // Send if requested
67
+ if (params.send) {
68
+ await page.keyboard.down('Control');
69
+ await page.keyboard.press('Enter');
70
+ await page.keyboard.up('Control');
71
+ logger.debug('replyEmail: sent via Ctrl+Enter');
72
+ }
73
+
74
+ const status = params.send ? 'sent' : 'draft';
75
+ const action = params.replyAll ? 'Reply-all' : 'Reply';
76
+ const summary = params.send
77
+ ? `${action} sent.`
78
+ : `${action} draft created. Review and send manually in Gmail.`;
79
+
80
+ return new GmailActionResponse(
81
+ { status, replyAll: !!params.replyAll },
82
+ summary,
83
+ params.send
84
+ ? ['Use list_emails to return to inbox']
85
+ : ['Review and send the reply draft manually in Gmail']
86
+ );
87
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * search-emails.js — Search emails using Gmail's search functionality.
3
+ *
4
+ * Tier usage:
5
+ * T1: gmailNavigate to #search/<query>
6
+ * T4: EMAIL_ROW + NO_RESULTS selectors for result detection
7
+ */
8
+
9
+ import { ErrorResponse } from '../../../core/responses.js';
10
+ import {
11
+ checkPrecondition,
12
+ gmailNavigate,
13
+ waitForGmail,
14
+ extractEmailRows,
15
+ GmailActionResponse
16
+ } from '../helpers.js';
17
+ import { EMAIL_ROW, NO_RESULTS } from '../selectors.js';
18
+
19
+ /**
20
+ * Search emails by query string.
21
+ * @param {object} opts
22
+ * @param {import('puppeteer-core').Page} opts.page
23
+ * @param {object} opts.params
24
+ * @param {string} opts.params.query - Gmail search query
25
+ * @param {number} [opts.params.limit=25] - Maximum results to return
26
+ * @returns {Promise<GmailActionResponse|ErrorResponse>}
27
+ */
28
+ export async function searchEmails({ page, params }) {
29
+ // Validate query
30
+ if (!params.query || !params.query.trim()) {
31
+ return new ErrorResponse(
32
+ 'Search query is required.',
33
+ [
34
+ 'Provide a query string, e.g. search_emails({ query: "from:boss subject:urgent" })',
35
+ 'Use list_emails to browse without a search query'
36
+ ]
37
+ );
38
+ }
39
+
40
+ // Precondition: must be on Gmail
41
+ const pre = await checkPrecondition(page, 'on_gmail');
42
+ if (!pre.met) {
43
+ return new ErrorResponse(pre.error, [
44
+ pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
45
+ ]);
46
+ }
47
+
48
+ const query = params.query.trim();
49
+ const limit = params.limit || 25;
50
+
51
+ // T1: Navigate to search results
52
+ await gmailNavigate(page, '#search/' + encodeURIComponent(query));
53
+
54
+ // Wait for either email rows or no-results indicator
55
+ let hasResults = true;
56
+ try {
57
+ await waitForGmail(page, EMAIL_ROW);
58
+ } catch {
59
+ // Check if no-results indicator is present instead
60
+ const noResults = await page.$(NO_RESULTS);
61
+ if (noResults) {
62
+ hasResults = false;
63
+ } else {
64
+ // Neither rows nor no-results — re-throw the timeout
65
+ throw new Error(
66
+ `Gmail search did not return results within the timeout. ` +
67
+ `Query: "${query}". The page may still be loading.`
68
+ );
69
+ }
70
+ }
71
+
72
+ if (!hasResults) {
73
+ return new GmailActionResponse(
74
+ { emails: [], query, resultCount: 0 },
75
+ `No results found for "${query}".`,
76
+ [
77
+ 'Try a different or broader search query',
78
+ 'Use list_emails to browse the inbox instead'
79
+ ]
80
+ );
81
+ }
82
+
83
+ // T3+T4: Extract search results
84
+ const emails = await extractEmailRows(page, limit);
85
+
86
+ return new GmailActionResponse(
87
+ { emails, query, resultCount: emails.length },
88
+ `Found ${emails.length} result(s) for "${query}".`,
89
+ [
90
+ 'Use read_email to open a specific result',
91
+ 'Refine your search with Gmail search operators (from:, to:, subject:, has:attachment)',
92
+ 'Use list_emails to return to the inbox'
93
+ ]
94
+ );
95
+ }