mcpbrowser 0.3.34 → 0.3.36
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/package.json +1 -1
- package/src/actions/click-element.js +8 -3
- package/src/actions/execute-javascript.js +8 -3
- package/src/actions/fetch-page.js +9 -3
- package/src/actions/get-current-html.js +9 -3
- package/src/actions/plugin-action.js +180 -0
- package/src/actions/plugin-info.js +170 -0
- package/src/core/logger.js +3 -7
- package/src/core/plugin-loader.js +344 -0
- package/src/mcp-browser.js +34 -2
- package/src/plugins/_example/index.js +140 -0
- package/src/plugins/gcal/actions/check-availability.js +185 -0
- package/src/plugins/gcal/actions/create-event.js +238 -0
- package/src/plugins/gcal/actions/delete-event.js +138 -0
- package/src/plugins/gcal/actions/edit-event.js +244 -0
- package/src/plugins/gcal/actions/list-events.js +96 -0
- package/src/plugins/gcal/actions/read-event.js +174 -0
- package/src/plugins/gcal/actions/rsvp-event.js +149 -0
- package/src/plugins/gcal/actions/search-events.js +121 -0
- package/src/plugins/gcal/helpers.js +415 -0
- package/src/plugins/gcal/index.js +148 -0
- package/src/plugins/gcal/selectors.js +54 -0
- package/src/plugins/gmail/actions/archive-email.js +65 -0
- package/src/plugins/gmail/actions/compose-email.js +116 -0
- package/src/plugins/gmail/actions/delete-email.js +65 -0
- package/src/plugins/gmail/actions/forward-email.js +95 -0
- package/src/plugins/gmail/actions/label-email.js +107 -0
- package/src/plugins/gmail/actions/list-emails.js +61 -0
- package/src/plugins/gmail/actions/mark-read.js +71 -0
- package/src/plugins/gmail/actions/mark-unread.js +71 -0
- package/src/plugins/gmail/actions/read-email.js +149 -0
- package/src/plugins/gmail/actions/reply-email.js +87 -0
- package/src/plugins/gmail/actions/search-emails.js +95 -0
- package/src/plugins/gmail/helpers.js +419 -0
- package/src/plugins/gmail/index.js +195 -0
- package/src/plugins/gmail/selectors.js +82 -0
- package/src/plugins.json +4 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selectors.js — Tier 4 CSS class selectors for the Google Calendar plugin.
|
|
3
|
+
*
|
|
4
|
+
* TIER 4 ONLY — These are Closure Compiler-generated class names that may
|
|
5
|
+
* change when Google deploys Calendar updates. All other interaction tiers
|
|
6
|
+
* (T1: URL path navigation, T2: keyboard shortcuts, T3: ARIA / data attributes)
|
|
7
|
+
* are defined inline in helpers.js since they use stable identifiers.
|
|
8
|
+
*
|
|
9
|
+
* Per FR-021: All CSS class selectors are centralized here so a Calendar UI
|
|
10
|
+
* update can be resolved by updating values in ONE place.
|
|
11
|
+
*
|
|
12
|
+
* Per SC-008: No action file should import CSS class names directly.
|
|
13
|
+
* All CSS access goes through this module.
|
|
14
|
+
*
|
|
15
|
+
* Tier coverage (SC-007): ~75% of interactions use T1/T2 (URL + keyboard).
|
|
16
|
+
* These T4 selectors cover only data extraction from event chips and detail
|
|
17
|
+
* popups where no ARIA/data-attribute alternative exists.
|
|
18
|
+
*
|
|
19
|
+
* @version 2026-04-06 — Initial capture against Google Calendar web UI
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// CALENDAR VIEW — Event chips (used by extractVisibleEvents)
|
|
23
|
+
// No ARIA alternative: event chip container uses generated classes
|
|
24
|
+
export const EVENT_CHIP = '[data-eventchip]';
|
|
25
|
+
// No ARIA alternative: title text within chip is plain span
|
|
26
|
+
export const EVENT_TITLE_IN_CHIP = 'span';
|
|
27
|
+
// No ARIA alternative: time text within chip lacks data attributes
|
|
28
|
+
export const EVENT_TIME_IN_CHIP = 'span';
|
|
29
|
+
|
|
30
|
+
// EVENT DETAIL POPUP — Fields in the detail popup (used by read_event)
|
|
31
|
+
// No ARIA alternative: location line lacks role/data attributes
|
|
32
|
+
export const EVENT_LOCATION_IN_DETAIL = '[data-location]';
|
|
33
|
+
// No ARIA alternative: description area uses contenteditable without data-attr
|
|
34
|
+
export const EVENT_DESCRIPTION_IN_DETAIL = '[data-description]';
|
|
35
|
+
|
|
36
|
+
// ATTENDEES — Within event detail popup (used by read_event, rsvp_event)
|
|
37
|
+
// No ARIA alternative: attendee rows lack role="listitem"
|
|
38
|
+
export const ATTENDEE_ROW = '[data-guest-email]';
|
|
39
|
+
// No ARIA alternative: RSVP icons use generated classes
|
|
40
|
+
export const ATTENDEE_RSVP_STATUS = '[data-rsvp]';
|
|
41
|
+
|
|
42
|
+
// RSVP BUTTONS — Within event detail popup (used by rsvp_event)
|
|
43
|
+
// No ARIA alternative: RSVP buttons lack unique aria-label
|
|
44
|
+
export const RSVP_YES_BUTTON = '[data-response="1"]';
|
|
45
|
+
export const RSVP_NO_BUTTON = '[data-response="2"]';
|
|
46
|
+
export const RSVP_MAYBE_BUTTON = '[data-response="3"]';
|
|
47
|
+
|
|
48
|
+
// CALENDAR INDICATOR — Color dot on event chips
|
|
49
|
+
// No ARIA alternative: color indicator is CSS-only
|
|
50
|
+
export const CALENDAR_COLOR_DOT = '[data-calendar-color]';
|
|
51
|
+
|
|
52
|
+
// FORM CONTROLS — Event creation/edit form (used by create_event, edit_event)
|
|
53
|
+
// No ARIA alternative: save button lacks unique accessible name
|
|
54
|
+
export const SAVE_BUTTON = '[data-savebtn]';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* archive-email.js — Archive an email, removing it from the inbox.
|
|
3
|
+
*
|
|
4
|
+
* Tier usage:
|
|
5
|
+
* T2: 'e' keyboard shortcut to archive
|
|
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
|
+
* Archive an email 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 archiveEmail({ 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, archive directly
|
|
41
|
+
await page.keyboard.press('e');
|
|
42
|
+
logger.debug('archiveEmail: pressed "e" 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 archive.', [
|
|
48
|
+
'Provide an index or id parameter to target a specific email.'
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
await page.keyboard.press('e');
|
|
52
|
+
logger.debug('archiveEmail: selected row and pressed "e"');
|
|
53
|
+
} else {
|
|
54
|
+
return new ErrorResponse(
|
|
55
|
+
'Cannot archive 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
|
+
{ archived: true },
|
|
62
|
+
'Email archived successfully.',
|
|
63
|
+
['Use list_emails to return to inbox']
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -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
|
+
}
|