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,116 @@
1
+ /**
2
+ * compose-email.js — Compose and optionally send a new email in Gmail.
3
+ *
4
+ * Tier usage:
5
+ * T2: 'c' keyboard shortcut to open compose, Ctrl+Enter to send
6
+ * T3: ARIA dialog, textarea[name], input[name], div[aria-label]
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
+ * Compose a new email, optionally sending it immediately.
20
+ * @param {object} opts
21
+ * @param {import('puppeteer-core').Page} opts.page
22
+ * @param {object} opts.params
23
+ * @param {string} opts.params.to - Recipient email address (required)
24
+ * @param {string} [opts.params.cc] - CC recipients
25
+ * @param {string} [opts.params.subject] - Email subject
26
+ * @param {string} [opts.params.body] - Email body text
27
+ * @param {boolean} [opts.params.send] - If true, send immediately
28
+ * @returns {Promise<GmailActionResponse|ErrorResponse>}
29
+ */
30
+ export async function composeEmail({ page, params }) {
31
+ // Validate required param
32
+ if (!params.to) {
33
+ return new ErrorResponse(
34
+ 'The "to" parameter is required to compose an email.',
35
+ ['Provide a recipient: compose_email({ to: "user@example.com", subject: "Hello" })']
36
+ );
37
+ }
38
+
39
+ // Precondition: must be on Gmail
40
+ const pre = await checkPrecondition(page, 'on_gmail');
41
+ if (!pre.met) {
42
+ return new ErrorResponse(pre.error, [
43
+ pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
44
+ ]);
45
+ }
46
+
47
+ // Verify keyboard shortcuts are enabled
48
+ const kb = await checkKeyboardShortcuts(page);
49
+ if (!kb.enabled) {
50
+ return new ErrorResponse(kb.error, [
51
+ 'Enable keyboard shortcuts in Gmail Settings → General → Keyboard shortcuts → ON, then reload Gmail.'
52
+ ]);
53
+ }
54
+
55
+ // Close any existing compose dialog
56
+ const existingDialog = await page.$('div[role="dialog"]');
57
+ if (existingDialog) {
58
+ logger.debug('composeEmail: closing existing compose dialog');
59
+ await page.keyboard.press('Escape');
60
+ await new Promise(r => setTimeout(r, 300));
61
+ }
62
+
63
+ // T2: Press 'c' to open compose
64
+ await page.keyboard.press('c');
65
+
66
+ // Wait for compose dialog to appear
67
+ await waitForGmail(page, 'div[role="dialog"]');
68
+
69
+ // Fill To field
70
+ await page.type('textarea[name="to"]', params.to);
71
+ await page.keyboard.press('Tab');
72
+
73
+ // Fill CC if provided
74
+ if (params.cc) {
75
+ const ccLink = await page.$('span[data-tooltip="Add Cc"]') ||
76
+ await page.$('span.aB.gQ.pE');
77
+ if (ccLink) {
78
+ await ccLink.click();
79
+ }
80
+ await page.type('textarea[name="cc"]', params.cc);
81
+ await page.keyboard.press('Tab');
82
+ }
83
+
84
+ // Fill Subject
85
+ await page.type('input[name="subjectbox"]', params.subject || '');
86
+
87
+ // Fill Body
88
+ const bodyDiv = await page.$('div[aria-label="Message Body"]');
89
+ if (bodyDiv) {
90
+ await bodyDiv.click();
91
+ if (params.body) {
92
+ await page.type('div[aria-label="Message Body"]', params.body);
93
+ }
94
+ }
95
+
96
+ // Send if requested
97
+ if (params.send) {
98
+ await page.keyboard.down('Control');
99
+ await page.keyboard.press('Enter');
100
+ await page.keyboard.up('Control');
101
+ logger.debug('composeEmail: sent via Ctrl+Enter');
102
+ }
103
+
104
+ const status = params.send ? 'sent' : 'draft';
105
+ const summary = params.send
106
+ ? `Email sent to ${params.to}.`
107
+ : `Compose draft created for ${params.to}.`;
108
+
109
+ return new GmailActionResponse(
110
+ { status, to: params.to, subject: params.subject || '' },
111
+ summary,
112
+ params.send
113
+ ? ['Use list_emails to return to inbox']
114
+ : ['Review and send the draft manually in Gmail']
115
+ );
116
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * delete-email.js — Delete an email by moving it to trash.
3
+ *
4
+ * Tier usage:
5
+ * T2: '#' keyboard shortcut to move to trash
6
+ * T3: selectEmailRow for list view targeting
7
+ */
8
+
9
+ import { ErrorResponse } from '../../../core/responses.js';
10
+ import logger from '../../../core/logger.js';
11
+ import {
12
+ checkPrecondition,
13
+ detectView,
14
+ selectEmailRow,
15
+ VIEW,
16
+ GmailActionResponse
17
+ } from '../helpers.js';
18
+
19
+ /**
20
+ * Delete an email (move to trash) from thread view or list view.
21
+ * @param {object} opts
22
+ * @param {import('puppeteer-core').Page} opts.page
23
+ * @param {object} opts.params
24
+ * @param {number} [opts.params.index] - Email index in list view
25
+ * @param {string} [opts.params.id] - Email ID in list view
26
+ * @returns {Promise<GmailActionResponse|ErrorResponse>}
27
+ */
28
+ export async function deleteEmail({ 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
+ const view = await detectView(page);
38
+
39
+ if (view === VIEW.THREAD) {
40
+ // In thread view, delete directly
41
+ await page.keyboard.type('#');
42
+ logger.debug('deleteEmail: typed "#" in thread view');
43
+ } else if (view === VIEW.EMAIL_LIST || view === VIEW.SEARCH_RESULTS) {
44
+ // In list view, select the row first
45
+ const sel = await selectEmailRow(page, { index: params.index, id: params.id });
46
+ if (!sel.selected) {
47
+ return new ErrorResponse(sel.error || 'Could not select email to delete.', [
48
+ 'Provide an index or id parameter to target a specific email.'
49
+ ]);
50
+ }
51
+ await page.keyboard.type('#');
52
+ logger.debug('deleteEmail: selected row and typed "#"');
53
+ } else {
54
+ return new ErrorResponse(
55
+ 'Cannot delete from the current view. Navigate to inbox or open an email first.',
56
+ ["Use list_emails to view the inbox, or read_email to open a thread."]
57
+ );
58
+ }
59
+
60
+ return new GmailActionResponse(
61
+ { deleted: true },
62
+ 'Email moved to trash.',
63
+ ['Use list_emails to return to inbox']
64
+ );
65
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * forward-email.js — Forward the currently open email to another recipient.
3
+ *
4
+ * Tier usage:
5
+ * T2: 'f' keyboard shortcut to open forward, Ctrl+Enter to send
6
+ * T3: ARIA dialog, textarea[name="to"], div[aria-label="Message Body"]
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
+ * Forward the currently open email thread.
20
+ * @param {object} opts
21
+ * @param {import('puppeteer-core').Page} opts.page
22
+ * @param {object} opts.params
23
+ * @param {string} opts.params.to - Recipient to forward to (required)
24
+ * @param {string} [opts.params.body] - Additional body text
25
+ * @param {boolean} [opts.params.send] - If true, send immediately
26
+ * @returns {Promise<GmailActionResponse|ErrorResponse>}
27
+ */
28
+ export async function forwardEmail({ 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 'f' to open forward
54
+ await page.keyboard.press('f');
55
+ logger.debug('forwardEmail: pressed "f" to open forward dialog');
56
+
57
+ // Wait for forward dialog
58
+ await waitForGmail(page, 'div[role="dialog"]');
59
+
60
+ // Fill To field
61
+ if (params.to) {
62
+ await page.type('textarea[name="to"]', params.to);
63
+ await page.keyboard.press('Tab');
64
+ }
65
+
66
+ // Fill additional body text if provided
67
+ if (params.body) {
68
+ const bodyDiv = await page.$('div[aria-label="Message Body"]');
69
+ if (bodyDiv) {
70
+ await bodyDiv.click();
71
+ await page.type('div[aria-label="Message Body"]', params.body);
72
+ }
73
+ }
74
+
75
+ // Send if requested
76
+ if (params.send) {
77
+ await page.keyboard.down('Control');
78
+ await page.keyboard.press('Enter');
79
+ await page.keyboard.up('Control');
80
+ logger.debug('forwardEmail: sent via Ctrl+Enter');
81
+ }
82
+
83
+ const status = params.send ? 'sent' : 'draft';
84
+ const summary = params.send
85
+ ? `Email forwarded to ${params.to || '(no recipient)'}.`
86
+ : `Forward draft created. Fill in recipients and send manually.`;
87
+
88
+ return new GmailActionResponse(
89
+ { status, to: params.to || '' },
90
+ summary,
91
+ params.send
92
+ ? ['Use list_emails to return to inbox']
93
+ : ['Review and send the forward draft manually in Gmail']
94
+ );
95
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * label-email.js — Apply a label to an email using the label picker.
3
+ *
4
+ * Tier usage:
5
+ * T2: 'l' keyboard shortcut to open label picker
6
+ * T3: selectEmailRow for list view targeting
7
+ * T4: LABEL_ITEM selector for matching labels
8
+ */
9
+
10
+ import { ErrorResponse } from '../../../core/responses.js';
11
+ import logger from '../../../core/logger.js';
12
+ import {
13
+ checkPrecondition,
14
+ checkKeyboardShortcuts,
15
+ detectView,
16
+ selectEmailRow,
17
+ waitForGmail,
18
+ VIEW,
19
+ GmailActionResponse
20
+ } from '../helpers.js';
21
+ import { LABEL_ITEM } from '../selectors.js';
22
+
23
+ /**
24
+ * Apply a label to an email.
25
+ * @param {object} opts
26
+ * @param {import('puppeteer-core').Page} opts.page
27
+ * @param {object} opts.params
28
+ * @param {string} opts.params.label - Label name to apply (required)
29
+ * @param {number} [opts.params.index] - Email index in list view
30
+ * @param {string} [opts.params.id] - Email ID in list view
31
+ * @returns {Promise<GmailActionResponse|ErrorResponse>}
32
+ */
33
+ export async function labelEmail({ page, params }) {
34
+ // Validate required param
35
+ if (!params.label) {
36
+ return new ErrorResponse(
37
+ 'The "label" parameter is required.',
38
+ ['Provide a label name: label_email({ label: "Important" })']
39
+ );
40
+ }
41
+
42
+ // Precondition: must be on Gmail
43
+ const pre = await checkPrecondition(page, 'on_gmail');
44
+ if (!pre.met) {
45
+ return new ErrorResponse(pre.error, [
46
+ pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
47
+ ]);
48
+ }
49
+
50
+ const view = await detectView(page);
51
+
52
+ // Select email row if in list view
53
+ if (view === VIEW.EMAIL_LIST || view === VIEW.SEARCH_RESULTS) {
54
+ const sel = await selectEmailRow(page, { index: params.index, id: params.id });
55
+ if (!sel.selected) {
56
+ return new ErrorResponse(sel.error || 'Could not select email to label.', [
57
+ 'Provide an index or id parameter to target a specific email.'
58
+ ]);
59
+ }
60
+ }
61
+
62
+ // Verify keyboard shortcuts are enabled
63
+ const kb = await checkKeyboardShortcuts(page);
64
+ if (!kb.enabled) {
65
+ return new ErrorResponse(kb.error, [
66
+ 'Enable keyboard shortcuts in Gmail Settings → General → Keyboard shortcuts → ON, then reload Gmail.'
67
+ ]);
68
+ }
69
+
70
+ // T2: Press 'l' to open label picker
71
+ await page.keyboard.press('l');
72
+ logger.debug('labelEmail: pressed "l" to open label picker');
73
+
74
+ // Wait for label picker overlay
75
+ await waitForGmail(page, LABEL_ITEM);
76
+
77
+ // Find matching label in the picker
78
+ const result = await page.evaluate((selector, targetLabel) => {
79
+ const items = document.querySelectorAll(selector);
80
+ const labels = [];
81
+ for (const item of items) {
82
+ const text = item.textContent?.trim() || '';
83
+ labels.push(text);
84
+ if (text.toLowerCase() === targetLabel.toLowerCase()) {
85
+ item.click();
86
+ return { found: true, label: text };
87
+ }
88
+ }
89
+ return { found: false, visibleLabels: labels };
90
+ }, LABEL_ITEM, params.label);
91
+
92
+ if (!result.found) {
93
+ return new ErrorResponse(
94
+ `Label "${params.label}" not found in the label picker.`,
95
+ [
96
+ `Available labels: ${(result.visibleLabels || []).join(', ') || '(none visible)'}`,
97
+ 'Check the exact label name and try again.'
98
+ ]
99
+ );
100
+ }
101
+
102
+ return new GmailActionResponse(
103
+ { labeled: true, label: result.label },
104
+ `Label "${result.label}" applied to email.`,
105
+ ['Use list_emails to return to inbox']
106
+ );
107
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * list-emails.js — List emails from Gmail inbox or specified folder/label.
3
+ *
4
+ * Tier usage:
5
+ * T1: gmailNavigate to folder hash
6
+ * T4: EMAIL_ROW selector for waitForGmail + extractEmailRows
7
+ */
8
+
9
+ import { ErrorResponse } from '../../../core/responses.js';
10
+ import {
11
+ checkPrecondition,
12
+ gmailNavigate,
13
+ folderToHash,
14
+ waitForGmail,
15
+ extractEmailRows,
16
+ GmailActionResponse
17
+ } from '../helpers.js';
18
+ import { EMAIL_ROW } from '../selectors.js';
19
+
20
+ /**
21
+ * List emails from the current Gmail view or a specific folder.
22
+ * @param {object} opts
23
+ * @param {import('puppeteer-core').Page} opts.page
24
+ * @param {object} opts.params
25
+ * @param {string} [opts.params.folder] - Folder name (inbox, sent, drafts, label name, etc.)
26
+ * @param {number} [opts.params.limit=25] - Maximum emails to return
27
+ * @returns {Promise<GmailActionResponse|ErrorResponse>}
28
+ */
29
+ export async function listEmails({ page, params }) {
30
+ // Precondition: must be on Gmail
31
+ const pre = await checkPrecondition(page, 'on_gmail');
32
+ if (!pre.met) {
33
+ return new ErrorResponse(pre.error, [
34
+ pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
35
+ ]);
36
+ }
37
+
38
+ const folder = params.folder || 'inbox';
39
+ const limit = params.limit || 25;
40
+
41
+ // T1: Navigate to the requested folder
42
+ if (params.folder) {
43
+ await gmailNavigate(page, folderToHash(params.folder));
44
+ }
45
+
46
+ // T4: Wait for email rows to appear
47
+ await waitForGmail(page, EMAIL_ROW);
48
+
49
+ // T3+T4: Extract visible email data
50
+ const emails = await extractEmailRows(page, limit);
51
+
52
+ return new GmailActionResponse(
53
+ { emails, folder, totalVisible: emails.length },
54
+ `Found ${emails.length} email(s) in ${folder}.`,
55
+ [
56
+ 'Use read_email to open a specific email',
57
+ 'Use search_emails to find specific messages',
58
+ 'Use compose_email to write a new email'
59
+ ]
60
+ );
61
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * mark-read.js — Mark an email as read in list view.
3
+ *
4
+ * Tier usage:
5
+ * T2: Shift+i keyboard shortcut to mark as read
6
+ * T3: selectEmailRow for targeting
7
+ */
8
+
9
+ import { ErrorResponse } from '../../../core/responses.js';
10
+ import logger from '../../../core/logger.js';
11
+ import {
12
+ checkPrecondition,
13
+ checkKeyboardShortcuts,
14
+ selectEmailRow,
15
+ GmailActionResponse
16
+ } from '../helpers.js';
17
+
18
+ /**
19
+ * Mark an email as read in list view.
20
+ * @param {object} opts
21
+ * @param {import('puppeteer-core').Page} opts.page
22
+ * @param {object} opts.params
23
+ * @param {number} [opts.params.index] - Email index
24
+ * @param {string} [opts.params.id] - Email ID
25
+ * @returns {Promise<GmailActionResponse|ErrorResponse>}
26
+ */
27
+ export async function markRead({ page, params }) {
28
+ // Precondition: must be on Gmail
29
+ const pre = await checkPrecondition(page, 'on_gmail');
30
+ if (!pre.met) {
31
+ return new ErrorResponse(pre.error, [
32
+ pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
33
+ ]);
34
+ }
35
+
36
+ // Precondition: must be in list view
37
+ const listPre = await checkPrecondition(page, 'list_view');
38
+ if (!listPre.met) {
39
+ return new ErrorResponse(listPre.error, [
40
+ listPre.suggestion || "Use list_emails to navigate to an email list first."
41
+ ]);
42
+ }
43
+
44
+ // Verify keyboard shortcuts are enabled
45
+ const kb = await checkKeyboardShortcuts(page);
46
+ if (!kb.enabled) {
47
+ return new ErrorResponse(kb.error, [
48
+ 'Enable keyboard shortcuts in Gmail Settings → General → Keyboard shortcuts → ON, then reload Gmail.'
49
+ ]);
50
+ }
51
+
52
+ // Select the email row
53
+ const sel = await selectEmailRow(page, { index: params.index, id: params.id });
54
+ if (!sel.selected) {
55
+ return new ErrorResponse(sel.error || 'Could not select email.', [
56
+ 'Provide an index or id parameter to target a specific email.'
57
+ ]);
58
+ }
59
+
60
+ // T2: Shift+i to mark as read
61
+ await page.keyboard.down('Shift');
62
+ await page.keyboard.press('KeyI');
63
+ await page.keyboard.up('Shift');
64
+ logger.debug('markRead: pressed Shift+i');
65
+
66
+ return new GmailActionResponse(
67
+ { markedRead: true },
68
+ 'Email marked as read.',
69
+ ['Use list_emails to refresh the email list']
70
+ );
71
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * mark-unread.js — Mark an email as unread in list view.
3
+ *
4
+ * Tier usage:
5
+ * T2: Shift+u keyboard shortcut to mark as unread
6
+ * T3: selectEmailRow for targeting
7
+ */
8
+
9
+ import { ErrorResponse } from '../../../core/responses.js';
10
+ import logger from '../../../core/logger.js';
11
+ import {
12
+ checkPrecondition,
13
+ checkKeyboardShortcuts,
14
+ selectEmailRow,
15
+ GmailActionResponse
16
+ } from '../helpers.js';
17
+
18
+ /**
19
+ * Mark an email as unread in list view.
20
+ * @param {object} opts
21
+ * @param {import('puppeteer-core').Page} opts.page
22
+ * @param {object} opts.params
23
+ * @param {number} [opts.params.index] - Email index
24
+ * @param {string} [opts.params.id] - Email ID
25
+ * @returns {Promise<GmailActionResponse|ErrorResponse>}
26
+ */
27
+ export async function markUnread({ page, params }) {
28
+ // Precondition: must be on Gmail
29
+ const pre = await checkPrecondition(page, 'on_gmail');
30
+ if (!pre.met) {
31
+ return new ErrorResponse(pre.error, [
32
+ pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
33
+ ]);
34
+ }
35
+
36
+ // Precondition: must be in list view
37
+ const listPre = await checkPrecondition(page, 'list_view');
38
+ if (!listPre.met) {
39
+ return new ErrorResponse(listPre.error, [
40
+ listPre.suggestion || "Use list_emails to navigate to an email list first."
41
+ ]);
42
+ }
43
+
44
+ // Verify keyboard shortcuts are enabled
45
+ const kb = await checkKeyboardShortcuts(page);
46
+ if (!kb.enabled) {
47
+ return new ErrorResponse(kb.error, [
48
+ 'Enable keyboard shortcuts in Gmail Settings → General → Keyboard shortcuts → ON, then reload Gmail.'
49
+ ]);
50
+ }
51
+
52
+ // Select the email row
53
+ const sel = await selectEmailRow(page, { index: params.index, id: params.id });
54
+ if (!sel.selected) {
55
+ return new ErrorResponse(sel.error || 'Could not select email.', [
56
+ 'Provide an index or id parameter to target a specific email.'
57
+ ]);
58
+ }
59
+
60
+ // T2: Shift+u to mark as unread
61
+ await page.keyboard.down('Shift');
62
+ await page.keyboard.press('KeyU');
63
+ await page.keyboard.up('Shift');
64
+ logger.debug('markUnread: pressed Shift+u');
65
+
66
+ return new GmailActionResponse(
67
+ { markedUnread: true },
68
+ 'Email marked as unread.',
69
+ ['Use list_emails to refresh the email list']
70
+ );
71
+ }