mcpbrowser 0.3.35 → 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 +9 -6
- package/src/actions/execute-javascript.js +9 -6
- package/src/actions/fetch-page.js +10 -6
- package/src/actions/get-current-html.js +10 -6
- package/src/core/plugin-loader.js +26 -5
- package/src/plugins/_example/index.js +1 -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/index.js +1 -0
- package/src/plugins.json +2 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create-event.js — Create a new calendar event with title, time, location,
|
|
3
|
+
* description, and attendees.
|
|
4
|
+
*
|
|
5
|
+
* Tier usage:
|
|
6
|
+
* T2: 'c' keyboard shortcut to open creation form
|
|
7
|
+
* T3: ARIA dialog / input targeting for form filling
|
|
8
|
+
* T4: SAVE_BUTTON selector for conditional save
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ErrorResponse } from '../../../core/responses.js';
|
|
12
|
+
import logger from '../../../core/logger.js';
|
|
13
|
+
import {
|
|
14
|
+
checkPrecondition,
|
|
15
|
+
checkKeyboardShortcuts,
|
|
16
|
+
waitForCalendar,
|
|
17
|
+
GCalActionResponse
|
|
18
|
+
} from '../helpers.js';
|
|
19
|
+
import { SAVE_BUTTON } from '../selectors.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a new calendar event, optionally saving it immediately.
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* @param {import('puppeteer-core').Page} opts.page
|
|
25
|
+
* @param {object} opts.params
|
|
26
|
+
* @param {string} opts.params.title - Event title (required)
|
|
27
|
+
* @param {string} [opts.params.date] - Event date in ISO format
|
|
28
|
+
* @param {string} [opts.params.startTime] - Start time in HH:MM format
|
|
29
|
+
* @param {string} [opts.params.endTime] - End time in HH:MM format
|
|
30
|
+
* @param {boolean} [opts.params.allDay=false] - Create an all-day event
|
|
31
|
+
* @param {string} [opts.params.location] - Event location
|
|
32
|
+
* @param {string} [opts.params.description] - Event description/notes
|
|
33
|
+
* @param {string[]} [opts.params.attendees] - Array of attendee email addresses
|
|
34
|
+
* @param {boolean} [opts.params.save=false] - If true, save the event (FR-015)
|
|
35
|
+
* @returns {Promise<GCalActionResponse|ErrorResponse>}
|
|
36
|
+
*/
|
|
37
|
+
export async function createEvent({ page, params }) {
|
|
38
|
+
// Validate required param
|
|
39
|
+
if (!params.title) {
|
|
40
|
+
return new ErrorResponse(
|
|
41
|
+
'The "title" parameter is required to create an event.',
|
|
42
|
+
['Provide a title: create_event({ title: "Team Meeting", date: "2026-04-10", startTime: "10:00" })']
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Precondition: must be on Google Calendar
|
|
47
|
+
const pre = await checkPrecondition(page, 'on_calendar');
|
|
48
|
+
if (!pre.met) {
|
|
49
|
+
return new ErrorResponse(pre.error, [
|
|
50
|
+
pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// T2: Verify keyboard shortcuts are enabled
|
|
55
|
+
const kb = await checkKeyboardShortcuts(page);
|
|
56
|
+
if (!kb.enabled) {
|
|
57
|
+
return new ErrorResponse(kb.error, [
|
|
58
|
+
'Enable keyboard shortcuts in Google Calendar Settings → Keyboard shortcuts → Enable keyboard shortcuts, then reload Calendar.'
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// FR-023: Close any existing dialog before opening a new one
|
|
63
|
+
const existingDialog = await page.$('div[role="dialog"]');
|
|
64
|
+
if (existingDialog) {
|
|
65
|
+
logger.debug('createEvent: closing existing dialog');
|
|
66
|
+
await page.keyboard.press('Escape');
|
|
67
|
+
await new Promise(r => setTimeout(r, 300));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Ensure nothing is focused that would swallow the keystroke
|
|
71
|
+
await page.evaluate(() => {
|
|
72
|
+
if (document.activeElement && document.activeElement.tagName !== 'BODY') {
|
|
73
|
+
document.activeElement.blur();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// T2: Press 'c' to open create event form
|
|
78
|
+
await page.keyboard.press('c');
|
|
79
|
+
|
|
80
|
+
// Wait for the quick-add dialog or full event form
|
|
81
|
+
try {
|
|
82
|
+
await waitForCalendar(page, 'div[role="dialog"]');
|
|
83
|
+
} catch {
|
|
84
|
+
return new ErrorResponse(
|
|
85
|
+
'Event creation form did not appear. Keyboard shortcut "c" may not have worked.',
|
|
86
|
+
[
|
|
87
|
+
'Ensure keyboard shortcuts are enabled in Calendar Settings',
|
|
88
|
+
'Try clicking the "+" button on the calendar manually'
|
|
89
|
+
]
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// T3: Fill in the title field — target the title input inside the dialog
|
|
94
|
+
const titleInput = await page.$('input[aria-label="Add title"]') ||
|
|
95
|
+
await page.$('input[data-initial-value]') ||
|
|
96
|
+
await page.$('div[role="dialog"] input[type="text"]');
|
|
97
|
+
if (titleInput) {
|
|
98
|
+
await titleInput.click({ clickCount: 3 }); // select all existing text
|
|
99
|
+
await titleInput.type(params.title);
|
|
100
|
+
logger.debug(`createEvent: typed title "${params.title}"`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if we need to expand to the full form for additional fields
|
|
104
|
+
const hasExtraFields = params.date || params.startTime || params.endTime ||
|
|
105
|
+
params.location || params.description || params.attendees || params.allDay;
|
|
106
|
+
|
|
107
|
+
if (hasExtraFields) {
|
|
108
|
+
// Click "More options" to open full form if in quick-add mode
|
|
109
|
+
const moreOptions = await page.$('button[aria-label="More options"]') ||
|
|
110
|
+
await page.$('[data-moreactions]');
|
|
111
|
+
if (moreOptions) {
|
|
112
|
+
await moreOptions.click();
|
|
113
|
+
logger.debug('createEvent: expanded to full form');
|
|
114
|
+
await new Promise(r => setTimeout(r, 500));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// T3: Fill date field if provided
|
|
119
|
+
if (params.date) {
|
|
120
|
+
const dateInput = await page.$('input[aria-label*="date" i]') ||
|
|
121
|
+
await page.$('input[data-date]');
|
|
122
|
+
if (dateInput) {
|
|
123
|
+
await dateInput.click({ clickCount: 3 });
|
|
124
|
+
await dateInput.type(params.date);
|
|
125
|
+
await page.keyboard.press('Tab');
|
|
126
|
+
logger.debug(`createEvent: set date "${params.date}"`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// T3: Toggle all-day if requested
|
|
131
|
+
if (params.allDay) {
|
|
132
|
+
const allDayCheckbox = await page.$('input[aria-label*="all day" i]') ||
|
|
133
|
+
await page.$('[data-allday] input[type="checkbox"]');
|
|
134
|
+
if (allDayCheckbox) {
|
|
135
|
+
await allDayCheckbox.click();
|
|
136
|
+
logger.debug('createEvent: toggled all-day');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// T3: Fill start time if provided (and not all-day)
|
|
141
|
+
if (params.startTime && !params.allDay) {
|
|
142
|
+
const startInput = await page.$('input[aria-label*="start time" i]') ||
|
|
143
|
+
await page.$('input[aria-label*="Start time" i]');
|
|
144
|
+
if (startInput) {
|
|
145
|
+
await startInput.click({ clickCount: 3 });
|
|
146
|
+
await startInput.type(params.startTime);
|
|
147
|
+
await page.keyboard.press('Tab');
|
|
148
|
+
logger.debug(`createEvent: set startTime "${params.startTime}"`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// T3: Fill end time if provided (and not all-day)
|
|
153
|
+
if (params.endTime && !params.allDay) {
|
|
154
|
+
const endInput = await page.$('input[aria-label*="end time" i]') ||
|
|
155
|
+
await page.$('input[aria-label*="End time" i]');
|
|
156
|
+
if (endInput) {
|
|
157
|
+
await endInput.click({ clickCount: 3 });
|
|
158
|
+
await endInput.type(params.endTime);
|
|
159
|
+
await page.keyboard.press('Tab');
|
|
160
|
+
logger.debug(`createEvent: set endTime "${params.endTime}"`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// T3: Fill location if provided
|
|
165
|
+
if (params.location) {
|
|
166
|
+
const locationInput = await page.$('input[aria-label*="location" i]') ||
|
|
167
|
+
await page.$('[data-locationactionpanel] input');
|
|
168
|
+
if (locationInput) {
|
|
169
|
+
await locationInput.click();
|
|
170
|
+
await locationInput.type(params.location);
|
|
171
|
+
await page.keyboard.press('Tab');
|
|
172
|
+
logger.debug(`createEvent: set location "${params.location}"`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// T3: Fill description if provided
|
|
177
|
+
if (params.description) {
|
|
178
|
+
const descInput = await page.$('div[aria-label*="description" i][contenteditable]') ||
|
|
179
|
+
await page.$('textarea[aria-label*="description" i]') ||
|
|
180
|
+
await page.$('[data-description] [contenteditable]');
|
|
181
|
+
if (descInput) {
|
|
182
|
+
await descInput.click();
|
|
183
|
+
await descInput.type(params.description);
|
|
184
|
+
logger.debug('createEvent: set description');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// T3: Add attendees if provided
|
|
189
|
+
if (params.attendees && params.attendees.length > 0) {
|
|
190
|
+
const guestInput = await page.$('input[aria-label*="guest" i]') ||
|
|
191
|
+
await page.$('input[aria-label*="Add guests" i]');
|
|
192
|
+
if (guestInput) {
|
|
193
|
+
for (const email of params.attendees) {
|
|
194
|
+
await guestInput.click();
|
|
195
|
+
await guestInput.type(email);
|
|
196
|
+
await page.keyboard.press('Enter');
|
|
197
|
+
await new Promise(r => setTimeout(r, 200));
|
|
198
|
+
}
|
|
199
|
+
logger.debug(`createEvent: added ${params.attendees.length} attendee(s)`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// FR-015: Conditionally save — default is false (leave for review)
|
|
204
|
+
const save = params.save === true;
|
|
205
|
+
if (save) {
|
|
206
|
+
const saveBtn = await page.$(SAVE_BUTTON) ||
|
|
207
|
+
await page.$('button[aria-label="Save"]') ||
|
|
208
|
+
await page.$('div[role="dialog"] button:has-text("Save")');
|
|
209
|
+
if (saveBtn) {
|
|
210
|
+
await saveBtn.click();
|
|
211
|
+
logger.debug('createEvent: clicked Save');
|
|
212
|
+
await new Promise(r => setTimeout(r, 500));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const status = save ? 'saved' : 'draft';
|
|
217
|
+
const summary = save
|
|
218
|
+
? `Event "${params.title}" created and saved.`
|
|
219
|
+
: `Event "${params.title}" form filled. Review and save in Calendar.`;
|
|
220
|
+
|
|
221
|
+
return new GCalActionResponse(
|
|
222
|
+
{
|
|
223
|
+
status,
|
|
224
|
+
title: params.title,
|
|
225
|
+
date: params.date || 'today',
|
|
226
|
+
startTime: params.startTime || null,
|
|
227
|
+
endTime: params.endTime || null,
|
|
228
|
+
allDay: params.allDay || false,
|
|
229
|
+
location: params.location || null,
|
|
230
|
+
attendees: params.attendees || [],
|
|
231
|
+
save
|
|
232
|
+
},
|
|
233
|
+
summary,
|
|
234
|
+
save
|
|
235
|
+
? ['Use list_events to see the updated calendar']
|
|
236
|
+
: ['Review the event form in Calendar and click Save manually', 'Use list_events to return to calendar view']
|
|
237
|
+
);
|
|
238
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delete-event.js — Remove an event from Google Calendar.
|
|
3
|
+
*
|
|
4
|
+
* Tier usage:
|
|
5
|
+
* T2: Delete/Backspace keyboard shortcut to delete event
|
|
6
|
+
* T3: selectEvent for clicking event chip, ARIA dialog for confirmation
|
|
7
|
+
*
|
|
8
|
+
* FR-022: Precondition check before destructive keyboard action.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ErrorResponse } from '../../../core/responses.js';
|
|
12
|
+
import logger from '../../../core/logger.js';
|
|
13
|
+
import {
|
|
14
|
+
checkPrecondition,
|
|
15
|
+
selectEvent,
|
|
16
|
+
waitForCalendar,
|
|
17
|
+
detectView,
|
|
18
|
+
VIEW,
|
|
19
|
+
GCalActionResponse
|
|
20
|
+
} from '../helpers.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Delete an event by index or event ID.
|
|
24
|
+
* @param {object} opts
|
|
25
|
+
* @param {import('puppeteer-core').Page} opts.page
|
|
26
|
+
* @param {object} opts.params
|
|
27
|
+
* @param {number} [opts.params.index] - 0-based position in current event list
|
|
28
|
+
* @param {string} [opts.params.id] - Google Calendar event ID
|
|
29
|
+
* @returns {Promise<GCalActionResponse|ErrorResponse>}
|
|
30
|
+
*/
|
|
31
|
+
export async function deleteEvent({ page, params }) {
|
|
32
|
+
// Validate: at least one identifier required
|
|
33
|
+
if (params.index == null && params.id == null) {
|
|
34
|
+
return new ErrorResponse(
|
|
35
|
+
'Either index or id is required to delete an event.',
|
|
36
|
+
[
|
|
37
|
+
'Use list_events to see available events and their indices',
|
|
38
|
+
'Use search_events to find a specific event by keyword'
|
|
39
|
+
]
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Precondition: must be on Google Calendar
|
|
44
|
+
const pre = await checkPrecondition(page, 'on_calendar');
|
|
45
|
+
if (!pre.met) {
|
|
46
|
+
return new ErrorResponse(pre.error, [
|
|
47
|
+
pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// T3: Select (click) the target event to open detail popup
|
|
52
|
+
const sel = await selectEvent(page, { index: params.index, id: params.id });
|
|
53
|
+
if (!sel.selected) {
|
|
54
|
+
return new ErrorResponse(
|
|
55
|
+
sel.error || 'Could not select the event to delete.',
|
|
56
|
+
['Use list_events to refresh the event list and check indices']
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wait for detail popup
|
|
61
|
+
try {
|
|
62
|
+
await waitForCalendar(page, 'div[role="dialog"]');
|
|
63
|
+
} catch {
|
|
64
|
+
return new ErrorResponse(
|
|
65
|
+
'Event detail popup did not appear after clicking the event.',
|
|
66
|
+
['Try list_events to refresh, then delete_event with a valid index']
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Detect recurring event dialog — if it appears, select "This event"
|
|
71
|
+
const recurringDialog = await page.evaluate(() => {
|
|
72
|
+
const buttons = Array.from(document.querySelectorAll('button, span[role="radio"]'));
|
|
73
|
+
return buttons.some(b => b.textContent?.includes('This event'));
|
|
74
|
+
});
|
|
75
|
+
let recurringNote = null;
|
|
76
|
+
if (recurringDialog) {
|
|
77
|
+
const thisEventBtn = await page.evaluateHandle(() => {
|
|
78
|
+
const elements = Array.from(document.querySelectorAll('button, span[role="radio"], label'));
|
|
79
|
+
return elements.find(el => el.textContent?.includes('This event'));
|
|
80
|
+
});
|
|
81
|
+
if (thisEventBtn && thisEventBtn.asElement()) {
|
|
82
|
+
await thisEventBtn.asElement().click();
|
|
83
|
+
await new Promise(r => setTimeout(r, 300));
|
|
84
|
+
recurringNote = 'Deleted this single occurrence of a recurring event.';
|
|
85
|
+
logger.debug('deleteEvent: selected "This event" for recurring event');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// FR-022: Precondition check — verify we're in the right context
|
|
90
|
+
// before sending destructive keyboard shortcut
|
|
91
|
+
const view = await detectView(page);
|
|
92
|
+
const hasDialog = await page.$('div[role="dialog"]');
|
|
93
|
+
if (!hasDialog && view === VIEW.NOT_CALENDAR) {
|
|
94
|
+
return new ErrorResponse(
|
|
95
|
+
'Cannot delete: lost context. The event dialog is no longer visible.',
|
|
96
|
+
['Use list_events to refresh, then try delete_event again']
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// T2: Click the "Delete event" button or press Delete/Backspace
|
|
101
|
+
const deleteBtn = await page.$('button[aria-label="Delete event"]') ||
|
|
102
|
+
await page.$('button[aria-label*="delete" i]') ||
|
|
103
|
+
await page.$('[data-deletebtn]');
|
|
104
|
+
if (deleteBtn) {
|
|
105
|
+
await deleteBtn.click();
|
|
106
|
+
logger.debug('deleteEvent: clicked Delete button');
|
|
107
|
+
} else {
|
|
108
|
+
// Fallback: use keyboard shortcut
|
|
109
|
+
await page.keyboard.press('Delete');
|
|
110
|
+
logger.debug('deleteEvent: pressed Delete key');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Wait for confirmation or undo toast
|
|
114
|
+
await new Promise(r => setTimeout(r, 500));
|
|
115
|
+
|
|
116
|
+
// Check for confirmation dialog (some events require explicit confirmation)
|
|
117
|
+
const confirmBtn = await page.evaluateHandle(() => {
|
|
118
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
119
|
+
return buttons.find(b =>
|
|
120
|
+
b.textContent?.includes('Delete') ||
|
|
121
|
+
b.textContent?.includes('Remove')
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
if (confirmBtn && confirmBtn.asElement()) {
|
|
125
|
+
await confirmBtn.asElement().click();
|
|
126
|
+
logger.debug('deleteEvent: confirmed deletion');
|
|
127
|
+
await new Promise(r => setTimeout(r, 300));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return new GCalActionResponse(
|
|
131
|
+
{
|
|
132
|
+
deleted: true,
|
|
133
|
+
recurringNote
|
|
134
|
+
},
|
|
135
|
+
recurringNote || 'Event deleted successfully.',
|
|
136
|
+
['Use list_events to see the updated calendar']
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edit-event.js — Modify an existing event's fields.
|
|
3
|
+
*
|
|
4
|
+
* Tier usage:
|
|
5
|
+
* T3: selectEvent for clicking event chip, ARIA dialog for edit form,
|
|
6
|
+
* input targeting for field updates
|
|
7
|
+
* T4: SAVE_BUTTON selector for conditional save
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ErrorResponse } from '../../../core/responses.js';
|
|
11
|
+
import logger from '../../../core/logger.js';
|
|
12
|
+
import {
|
|
13
|
+
checkPrecondition,
|
|
14
|
+
selectEvent,
|
|
15
|
+
waitForCalendar,
|
|
16
|
+
GCalActionResponse
|
|
17
|
+
} from '../helpers.js';
|
|
18
|
+
import { SAVE_BUTTON } from '../selectors.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Edit an existing event by index or event ID.
|
|
22
|
+
* @param {object} opts
|
|
23
|
+
* @param {import('puppeteer-core').Page} opts.page
|
|
24
|
+
* @param {object} opts.params
|
|
25
|
+
* @param {number} [opts.params.index] - 0-based position in current event list
|
|
26
|
+
* @param {string} [opts.params.id] - Google Calendar event ID
|
|
27
|
+
* @param {string} [opts.params.title] - New title
|
|
28
|
+
* @param {string} [opts.params.date] - New date (ISO format)
|
|
29
|
+
* @param {string} [opts.params.startTime] - New start time (HH:MM)
|
|
30
|
+
* @param {string} [opts.params.endTime] - New end time (HH:MM)
|
|
31
|
+
* @param {string} [opts.params.location] - New location
|
|
32
|
+
* @param {string} [opts.params.description] - New description
|
|
33
|
+
* @param {string[]} [opts.params.attendees] - New attendee list
|
|
34
|
+
* @param {boolean} [opts.params.allDay] - Toggle all-day
|
|
35
|
+
* @param {boolean} [opts.params.save=false] - If true, save changes (FR-015)
|
|
36
|
+
* @returns {Promise<GCalActionResponse|ErrorResponse>}
|
|
37
|
+
*/
|
|
38
|
+
export async function editEvent({ page, params }) {
|
|
39
|
+
// Validate: at least one identifier required
|
|
40
|
+
if (params.index == null && params.id == null) {
|
|
41
|
+
return new ErrorResponse(
|
|
42
|
+
'Either index or id is required to edit an event.',
|
|
43
|
+
[
|
|
44
|
+
'Use list_events to see available events and their indices',
|
|
45
|
+
'Use search_events to find a specific event by keyword'
|
|
46
|
+
]
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Precondition: must be on Google Calendar
|
|
51
|
+
const pre = await checkPrecondition(page, 'on_calendar');
|
|
52
|
+
if (!pre.met) {
|
|
53
|
+
return new ErrorResponse(pre.error, [
|
|
54
|
+
pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// T3: Select (click) the target event to open detail popup
|
|
59
|
+
const sel = await selectEvent(page, { index: params.index, id: params.id });
|
|
60
|
+
if (!sel.selected) {
|
|
61
|
+
return new ErrorResponse(
|
|
62
|
+
sel.error || 'Could not select the event to edit.',
|
|
63
|
+
['Use list_events to refresh the event list and check indices']
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Wait for detail popup
|
|
68
|
+
try {
|
|
69
|
+
await waitForCalendar(page, 'div[role="dialog"]');
|
|
70
|
+
} catch {
|
|
71
|
+
return new ErrorResponse(
|
|
72
|
+
'Event detail popup did not appear after clicking the event.',
|
|
73
|
+
['Try list_events to refresh, then edit_event with a valid index']
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Detect recurring event dialog — if it appears, select "This event"
|
|
78
|
+
const recurringDialog = await page.evaluate(() => {
|
|
79
|
+
const buttons = Array.from(document.querySelectorAll('button, span[role="radio"]'));
|
|
80
|
+
return buttons.some(b => b.textContent?.includes('This event'));
|
|
81
|
+
});
|
|
82
|
+
let recurringNote = null;
|
|
83
|
+
if (recurringDialog) {
|
|
84
|
+
const thisEventBtn = await page.evaluateHandle(() => {
|
|
85
|
+
const elements = Array.from(document.querySelectorAll('button, span[role="radio"], label'));
|
|
86
|
+
return elements.find(el => el.textContent?.includes('This event'));
|
|
87
|
+
});
|
|
88
|
+
if (thisEventBtn) {
|
|
89
|
+
await thisEventBtn.asElement()?.click();
|
|
90
|
+
await new Promise(r => setTimeout(r, 300));
|
|
91
|
+
recurringNote = 'Editing this single occurrence of a recurring event.';
|
|
92
|
+
logger.debug('editEvent: selected "This event" for recurring event');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Click \"Edit event\" pencil icon to open the edit form
|
|
97
|
+
const editBtn = await page.$('button[aria-label="Edit event"]') ||
|
|
98
|
+
await page.$('button[aria-label*="edit" i]') ||
|
|
99
|
+
await page.$('[data-editbtn]');
|
|
100
|
+
if (editBtn) {
|
|
101
|
+
await editBtn.click();
|
|
102
|
+
logger.debug('editEvent: clicked Edit button');
|
|
103
|
+
await new Promise(r => setTimeout(r, 500));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Wait for the edit form to load
|
|
107
|
+
try {
|
|
108
|
+
await waitForCalendar(page, 'input[aria-label="Add title"], input[data-initial-value]');
|
|
109
|
+
} catch {
|
|
110
|
+
return new ErrorResponse(
|
|
111
|
+
'Edit form did not appear. The event may not be editable.',
|
|
112
|
+
['You may not have edit permissions for this event', 'Try reading the event first with read_event']
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// T3: Update only the provided fields
|
|
117
|
+
const fieldsUpdated = [];
|
|
118
|
+
|
|
119
|
+
if (params.title) {
|
|
120
|
+
const titleInput = await page.$('input[aria-label="Add title"]') ||
|
|
121
|
+
await page.$('input[data-initial-value]');
|
|
122
|
+
if (titleInput) {
|
|
123
|
+
await titleInput.click({ clickCount: 3 });
|
|
124
|
+
await titleInput.type(params.title);
|
|
125
|
+
fieldsUpdated.push('title');
|
|
126
|
+
logger.debug(`editEvent: updated title to "${params.title}"`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (params.date) {
|
|
131
|
+
const dateInput = await page.$('input[aria-label*="date" i]') ||
|
|
132
|
+
await page.$('input[data-date]');
|
|
133
|
+
if (dateInput) {
|
|
134
|
+
await dateInput.click({ clickCount: 3 });
|
|
135
|
+
await dateInput.type(params.date);
|
|
136
|
+
await page.keyboard.press('Tab');
|
|
137
|
+
fieldsUpdated.push('date');
|
|
138
|
+
logger.debug(`editEvent: updated date to "${params.date}"`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (params.allDay !== undefined) {
|
|
143
|
+
const allDayCheckbox = await page.$('input[aria-label*="all day" i]') ||
|
|
144
|
+
await page.$('[data-allday] input[type="checkbox"]');
|
|
145
|
+
if (allDayCheckbox) {
|
|
146
|
+
await allDayCheckbox.click();
|
|
147
|
+
fieldsUpdated.push('allDay');
|
|
148
|
+
logger.debug('editEvent: toggled all-day');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (params.startTime) {
|
|
153
|
+
const startInput = await page.$('input[aria-label*="start time" i]') ||
|
|
154
|
+
await page.$('input[aria-label*="Start time" i]');
|
|
155
|
+
if (startInput) {
|
|
156
|
+
await startInput.click({ clickCount: 3 });
|
|
157
|
+
await startInput.type(params.startTime);
|
|
158
|
+
await page.keyboard.press('Tab');
|
|
159
|
+
fieldsUpdated.push('startTime');
|
|
160
|
+
logger.debug(`editEvent: updated startTime to "${params.startTime}"`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (params.endTime) {
|
|
165
|
+
const endInput = await page.$('input[aria-label*="end time" i]') ||
|
|
166
|
+
await page.$('input[aria-label*="End time" i]');
|
|
167
|
+
if (endInput) {
|
|
168
|
+
await endInput.click({ clickCount: 3 });
|
|
169
|
+
await endInput.type(params.endTime);
|
|
170
|
+
await page.keyboard.press('Tab');
|
|
171
|
+
fieldsUpdated.push('endTime');
|
|
172
|
+
logger.debug(`editEvent: updated endTime to "${params.endTime}"`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (params.location) {
|
|
177
|
+
const locationInput = await page.$('input[aria-label*="location" i]') ||
|
|
178
|
+
await page.$('[data-locationactionpanel] input');
|
|
179
|
+
if (locationInput) {
|
|
180
|
+
await locationInput.click({ clickCount: 3 });
|
|
181
|
+
await locationInput.type(params.location);
|
|
182
|
+
await page.keyboard.press('Tab');
|
|
183
|
+
fieldsUpdated.push('location');
|
|
184
|
+
logger.debug(`editEvent: updated location to "${params.location}"`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (params.description) {
|
|
189
|
+
const descInput = await page.$('div[aria-label*="description" i][contenteditable]') ||
|
|
190
|
+
await page.$('textarea[aria-label*="description" i]') ||
|
|
191
|
+
await page.$('[data-description] [contenteditable]');
|
|
192
|
+
if (descInput) {
|
|
193
|
+
await descInput.click();
|
|
194
|
+
// Select all existing text and replace
|
|
195
|
+
await page.keyboard.down('Control');
|
|
196
|
+
await page.keyboard.press('a');
|
|
197
|
+
await page.keyboard.up('Control');
|
|
198
|
+
await descInput.type(params.description);
|
|
199
|
+
fieldsUpdated.push('description');
|
|
200
|
+
logger.debug('editEvent: updated description');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (params.attendees && params.attendees.length > 0) {
|
|
205
|
+
const guestInput = await page.$('input[aria-label*="guest" i]') ||
|
|
206
|
+
await page.$('input[aria-label*="Add guests" i]');
|
|
207
|
+
if (guestInput) {
|
|
208
|
+
for (const email of params.attendees) {
|
|
209
|
+
await guestInput.click();
|
|
210
|
+
await guestInput.type(email);
|
|
211
|
+
await page.keyboard.press('Enter');
|
|
212
|
+
await new Promise(r => setTimeout(r, 200));
|
|
213
|
+
}
|
|
214
|
+
fieldsUpdated.push('attendees');
|
|
215
|
+
logger.debug(`editEvent: updated attendees (${params.attendees.length} added)`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// FR-015: Conditionally save — default is false
|
|
220
|
+
const save = params.save === true;
|
|
221
|
+
if (save) {
|
|
222
|
+
const saveBtn = await page.$(SAVE_BUTTON) ||
|
|
223
|
+
await page.$('button[aria-label="Save"]');
|
|
224
|
+
if (saveBtn) {
|
|
225
|
+
await saveBtn.click();
|
|
226
|
+
logger.debug('editEvent: clicked Save');
|
|
227
|
+
await new Promise(r => setTimeout(r, 500));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return new GCalActionResponse(
|
|
232
|
+
{
|
|
233
|
+
fieldsUpdated,
|
|
234
|
+
saved: save,
|
|
235
|
+
recurringNote
|
|
236
|
+
},
|
|
237
|
+
save
|
|
238
|
+
? `Event updated and saved. Fields changed: ${fieldsUpdated.join(', ') || 'none'}.`
|
|
239
|
+
: `Event form updated (${fieldsUpdated.join(', ') || 'none'}). Review and save in Calendar.`,
|
|
240
|
+
save
|
|
241
|
+
? ['Use list_events to see the updated calendar']
|
|
242
|
+
: ['Review changes and click Save manually in Calendar', 'Use list_events to return to calendar view']
|
|
243
|
+
);
|
|
244
|
+
}
|