mcpbrowser 0.3.33 → 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.
@@ -38,13 +38,9 @@ async function notifyAgent(level, data) {
38
38
  // Skip if client requested a higher threshold.
39
39
  if (mcpServer.isMessageIgnored?.(level)) return;
40
40
  await mcpServer.sendLoggingMessage({ level, logger: 'mcpbrowser', data });
41
- } catch (err) {
42
- // Fall back to stderr without recursing through logger.
43
- try {
44
- process.stderr.write(`${PREFIX} logging notification failed: ${err?.message || err}\n`);
45
- } catch (_) {
46
- /* ignore */
47
- }
41
+ } catch {
42
+ // Silently drop this is expected during startup before the MCP
43
+ // transport handshake completes. Stderr already has the message.
48
44
  }
49
45
  }
50
46
 
@@ -0,0 +1,323 @@
1
+ /**
2
+ * plugin-loader.js — Core plugin infrastructure for MCPBrowser.
3
+ * Reads the plugin registry, validates manifests, dynamically loads plugins,
4
+ * and provides detection/accessor functions for dispatch tools.
5
+ */
6
+
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { join, resolve } from 'path';
9
+ import { pathToFileURL } from 'url';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname } from 'path';
12
+ import logger from './logger.js';
13
+
14
+ // ============================================================================
15
+ // CONSTANTS
16
+ // ============================================================================
17
+
18
+ /** Current plugin interface version. Plugins must match this exactly. */
19
+ export const CURRENT_INTERFACE_VERSION = 1;
20
+
21
+ /** @type {Map<string, object>} Loaded plugin instances keyed by name */
22
+ const loadedPlugins = new Map();
23
+
24
+ // ============================================================================
25
+ // REGISTRY
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Resolve the path to plugins.json relative to this module's package root.
30
+ * @returns {string} Absolute path to plugins.json
31
+ */
32
+ function getRegistryPath() {
33
+ const __filename = fileURLToPath(import.meta.url);
34
+ const __dirname = dirname(__filename);
35
+ return join(__dirname, '../plugins.json');
36
+ }
37
+
38
+ /**
39
+ * Resolve the path to the plugins/ directory.
40
+ * @returns {string} Absolute path to plugins/
41
+ */
42
+ function getPluginsDir() {
43
+ const __filename = fileURLToPath(import.meta.url);
44
+ const __dirname = dirname(__filename);
45
+ return join(__dirname, '../plugins');
46
+ }
47
+
48
+ /**
49
+ * Read and parse the plugin registry file.
50
+ * @returns {{ enabled: string[] }} Registry data with enabled plugin names
51
+ */
52
+ export function readRegistry() {
53
+ const registryPath = getRegistryPath();
54
+
55
+ if (!existsSync(registryPath)) {
56
+ logger.debug('plugins.json not found — no plugins to load');
57
+ return { enabled: [] };
58
+ }
59
+
60
+ try {
61
+ const raw = readFileSync(registryPath, 'utf-8');
62
+ const data = JSON.parse(raw);
63
+
64
+ if (!data || !Array.isArray(data.enabled)) {
65
+ logger.warn('plugins.json: "enabled" must be an array — no plugins loaded');
66
+ return { enabled: [] };
67
+ }
68
+
69
+ return { enabled: data.enabled.filter(name => typeof name === 'string' && name.length > 0) };
70
+ } catch (err) {
71
+ logger.warn(`plugins.json: failed to parse — ${err.message}`);
72
+ return { enabled: [] };
73
+ }
74
+ }
75
+
76
+ // ============================================================================
77
+ // VALIDATION
78
+ // ============================================================================
79
+
80
+ /**
81
+ * Validate a plugin manifest against required fields and interface version.
82
+ * @param {object} manifest - Plugin manifest object
83
+ * @param {string} folderName - Expected folder name for the plugin
84
+ * @returns {{ valid: boolean, reason?: string }}
85
+ */
86
+ export function validateManifest(manifest, folderName) {
87
+ if (!manifest || typeof manifest !== 'object') {
88
+ return { valid: false, reason: 'manifest is missing or not an object' };
89
+ }
90
+
91
+ const required = ['name', 'version', 'description', 'interfaceVersion', 'urlPatterns'];
92
+ for (const field of required) {
93
+ if (manifest[field] === undefined || manifest[field] === null || manifest[field] === '') {
94
+ return { valid: false, reason: `missing required field: ${field}` };
95
+ }
96
+ }
97
+
98
+ if (typeof manifest.name !== 'string') {
99
+ return { valid: false, reason: 'name must be a string' };
100
+ }
101
+ if (typeof manifest.version !== 'string') {
102
+ return { valid: false, reason: 'version must be a string' };
103
+ }
104
+ if (typeof manifest.description !== 'string') {
105
+ return { valid: false, reason: 'description must be a string' };
106
+ }
107
+ if (typeof manifest.interfaceVersion !== 'number' || !Number.isInteger(manifest.interfaceVersion)) {
108
+ return { valid: false, reason: 'interfaceVersion must be an integer' };
109
+ }
110
+ if (!Array.isArray(manifest.urlPatterns) || manifest.urlPatterns.length === 0) {
111
+ return { valid: false, reason: 'urlPatterns must be a non-empty array' };
112
+ }
113
+
114
+ if (manifest.name !== folderName) {
115
+ return { valid: false, reason: `manifest name "${manifest.name}" does not match folder name "${folderName}"` };
116
+ }
117
+
118
+ if (manifest.interfaceVersion !== CURRENT_INTERFACE_VERSION) {
119
+ return { valid: false, reason: `interfaceVersion ${manifest.interfaceVersion} is not compatible (expected ${CURRENT_INTERFACE_VERSION})` };
120
+ }
121
+
122
+ return { valid: true };
123
+ }
124
+
125
+ /**
126
+ * Validate that a plugin module has all required exports.
127
+ * @param {object} mod - Imported module namespace
128
+ * @param {string} pluginName - Plugin name for logging
129
+ * @returns {{ valid: boolean, reason?: string }}
130
+ */
131
+ function validateExports(mod, pluginName) {
132
+ if (typeof mod.matchesPage !== 'function') {
133
+ return { valid: false, reason: 'missing required export: matchesPage (function)' };
134
+ }
135
+ if (typeof mod.getActions !== 'function') {
136
+ return { valid: false, reason: 'missing required export: getActions (function)' };
137
+ }
138
+ if (typeof mod.getInfo !== 'function') {
139
+ return { valid: false, reason: 'missing required export: getInfo (function)' };
140
+ }
141
+
142
+ // Validate getActions returns non-empty array with required fields
143
+ try {
144
+ const actions = mod.getActions();
145
+ if (!Array.isArray(actions) || actions.length === 0) {
146
+ return { valid: false, reason: 'getActions() must return a non-empty array' };
147
+ }
148
+
149
+ const actionNames = new Set();
150
+ for (const action of actions) {
151
+ if (!action.name || !action.description || !Array.isArray(action.params) || typeof action.execute !== 'function') {
152
+ return { valid: false, reason: `action "${action.name || '?'}" is missing required fields (name, description, params, execute)` };
153
+ }
154
+ if (actionNames.has(action.name)) {
155
+ return { valid: false, reason: `duplicate action name "${action.name}" — action names must be unique within a plugin` };
156
+ }
157
+ actionNames.add(action.name);
158
+ }
159
+ } catch (err) {
160
+ return { valid: false, reason: `getActions() threw: ${err.message}` };
161
+ }
162
+
163
+ return { valid: true };
164
+ }
165
+
166
+ // ============================================================================
167
+ // LOADING
168
+ // ============================================================================
169
+
170
+ /**
171
+ * Load all enabled plugins from the registry.
172
+ * Reads plugins.json, dynamically imports each plugin, validates manifest
173
+ * and exports, and stores valid plugins in the loaded map.
174
+ * @returns {Promise<number>} Number of successfully loaded plugins
175
+ */
176
+ export async function loadPlugins() {
177
+ loadedPlugins.clear();
178
+
179
+ const registry = readRegistry();
180
+ if (registry.enabled.length === 0) {
181
+ logger.debug('No plugins enabled in registry');
182
+ return 0;
183
+ }
184
+
185
+ const pluginsDir = getPluginsDir();
186
+
187
+ // Check for duplicate names in registry
188
+ const seen = new Set();
189
+ const uniqueEnabled = [];
190
+ for (const name of registry.enabled) {
191
+ if (seen.has(name)) {
192
+ logger.warn(`Plugin "${name}" is listed multiple times in registry — skipping duplicate`);
193
+ continue;
194
+ }
195
+ seen.add(name);
196
+ uniqueEnabled.push(name);
197
+ }
198
+
199
+ for (const pluginName of uniqueEnabled) {
200
+ const pluginDir = join(pluginsDir, pluginName);
201
+ const entryPoint = join(pluginDir, 'index.js');
202
+
203
+ if (!existsSync(entryPoint)) {
204
+ logger.warn(`Plugin "${pluginName}": entry point not found at ${entryPoint} — skipping`);
205
+ continue;
206
+ }
207
+
208
+ try {
209
+ const moduleUrl = pathToFileURL(resolve(entryPoint)).href;
210
+ const mod = await import(moduleUrl);
211
+
212
+ // Validate manifest
213
+ const manifestCheck = validateManifest(mod.manifest, pluginName);
214
+ if (!manifestCheck.valid) {
215
+ logger.warn(`Plugin "${pluginName}": invalid manifest — ${manifestCheck.reason} — skipping`);
216
+ continue;
217
+ }
218
+
219
+ // Validate exports
220
+ const exportsCheck = validateExports(mod, pluginName);
221
+ if (!exportsCheck.valid) {
222
+ logger.warn(`Plugin "${pluginName}": invalid exports — ${exportsCheck.reason} — skipping`);
223
+ continue;
224
+ }
225
+
226
+ // Store the loaded plugin
227
+ loadedPlugins.set(pluginName, {
228
+ manifest: mod.manifest,
229
+ matchesPage: mod.matchesPage,
230
+ getActions: mod.getActions,
231
+ getInfo: mod.getInfo
232
+ });
233
+
234
+ logger.info(`Plugin "${pluginName}" v${mod.manifest.version} loaded (${mod.getActions().length} actions)`);
235
+ } catch (err) {
236
+ logger.warn(`Plugin "${pluginName}": failed to load — ${err.message} — skipping`);
237
+ }
238
+ }
239
+
240
+ logger.info(`Plugin loader: ${loadedPlugins.size} plugin(s) loaded`);
241
+ return loadedPlugins.size;
242
+ }
243
+
244
+ // ============================================================================
245
+ // DETECTION
246
+ // ============================================================================
247
+
248
+ /**
249
+ * Detect which loaded plugins match the given page URL and HTML.
250
+ * URL patterns are checked first (fast path), then DOM patterns if defined.
251
+ * @param {string} url - Current page URL
252
+ * @param {string} html - Extracted page HTML
253
+ * @returns {Array<{ pluginName: string, confidence: number, nextSteps: string[] }>}
254
+ */
255
+ export function detectPlugins(url, html) {
256
+ if (loadedPlugins.size === 0) return [];
257
+
258
+ const results = [];
259
+
260
+ for (const [name, plugin] of loadedPlugins) {
261
+ try {
262
+ const match = plugin.matchesPage(url, html);
263
+ if (match && match.matched) {
264
+ const confidence = typeof match.confidence === 'number' ? match.confidence : 1.0;
265
+
266
+ // Build nextSteps from plugin info
267
+ const info = plugin.getInfo();
268
+ const topActions = (info.actions || []).slice(0, 3);
269
+ const actionSummary = topActions.map(a => a.name).join(', ');
270
+
271
+ const nextSteps = [
272
+ `Plugin "${name}" detected — use plugin_info({ plugin: '${name}' }) to see all available actions`,
273
+ ...(actionSummary ? [`Top actions: ${actionSummary}. Use plugin_action({ plugin: '${name}', action: '<name>' }) to execute`] : [])
274
+ ];
275
+
276
+ results.push({ pluginName: name, confidence, nextSteps });
277
+ }
278
+ } catch (err) {
279
+ // Detection must not throw — skip this plugin silently
280
+ logger.debug(`Plugin "${name}" detection error: ${err.message}`);
281
+ }
282
+ }
283
+
284
+ // Sort by confidence descending
285
+ results.sort((a, b) => b.confidence - a.confidence);
286
+ return results;
287
+ }
288
+
289
+ /**
290
+ * Convert detection results into a flat nextSteps string array for response augmentation.
291
+ * @param {string} url - Current page URL
292
+ * @param {string} html - Extracted page HTML
293
+ * @returns {string[]} Array of nextSteps strings from matching plugins
294
+ */
295
+ export function getPluginNextSteps(url, html) {
296
+ const detections = detectPlugins(url, html);
297
+ const steps = [];
298
+ for (const d of detections) {
299
+ steps.push(...d.nextSteps);
300
+ }
301
+ return steps;
302
+ }
303
+
304
+ // ============================================================================
305
+ // ACCESSORS
306
+ // ============================================================================
307
+
308
+ /**
309
+ * Get the map of all loaded plugins.
310
+ * @returns {Map<string, object>}
311
+ */
312
+ export function getLoadedPlugins() {
313
+ return loadedPlugins;
314
+ }
315
+
316
+ /**
317
+ * Get a specific loaded plugin by name.
318
+ * @param {string} name - Plugin name
319
+ * @returns {object|undefined} Plugin instance or undefined
320
+ */
321
+ export function getPlugin(name) {
322
+ return loadedPlugins.get(name);
323
+ }
@@ -28,6 +28,11 @@ import { getCurrentHtml, GET_CURRENT_HTML_TOOL } from './actions/get-current-htm
28
28
  import { takeScreenshot, TAKE_SCREENSHOT_TOOL } from './actions/take-screenshot.js';
29
29
  import { scrollPage, SCROLL_PAGE_TOOL } from './actions/scroll-page.js';
30
30
  import { executeJavascript, EXECUTE_JAVASCRIPT_TOOL } from './actions/execute-javascript.js';
31
+ import { navigateHistory, NAVIGATE_HISTORY_TOOL } from './actions/navigate-history.js';
32
+
33
+ // Import plugin dispatch tools
34
+ import { pluginAction, PLUGIN_ACTION_TOOL } from './actions/plugin-action.js';
35
+ import { pluginInfo, PLUGIN_INFO_TOOL } from './actions/plugin-info.js';
31
36
 
32
37
  // Import functions for testing exports
33
38
  import { getBrowser, closeBrowser } from './core/browser.js';
@@ -36,6 +41,9 @@ import { isLikelyAuthUrl, waitForAuth, pollUntilAuthDone, detectLoginPage } from
36
41
  import { cleanHtml, enrichHtml, prepareHtml } from './core/html.js';
37
42
  import { getBaseDomain } from './utils.js';
38
43
 
44
+ // Import plugin system
45
+ import { loadPlugins, getLoadedPlugins, getPlugin, detectPlugins, getPluginNextSteps } from './core/plugin-loader.js';
46
+
39
47
  /**
40
48
  * Main entry point for the MCP server.
41
49
  * Sets up the Model Context Protocol server with all available tools,
@@ -67,7 +75,10 @@ async function main() {
67
75
  CLOSE_TAB_TOOL,
68
76
  GET_CURRENT_HTML_TOOL,
69
77
  TAKE_SCREENSHOT_TOOL,
70
- SCROLL_PAGE_TOOL
78
+ SCROLL_PAGE_TOOL,
79
+ NAVIGATE_HISTORY_TOOL,
80
+ PLUGIN_INFO_TOOL,
81
+ PLUGIN_ACTION_TOOL
71
82
  ];
72
83
 
73
84
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
@@ -121,6 +132,18 @@ async function main() {
121
132
  case "scroll_page":
122
133
  result = await scrollPage(safeArgs);
123
134
  break;
135
+
136
+ case "navigate_history":
137
+ result = await navigateHistory(safeArgs);
138
+ break;
139
+
140
+ case "plugin_info":
141
+ result = pluginInfo(safeArgs);
142
+ break;
143
+
144
+ case "plugin_action":
145
+ result = await pluginAction(safeArgs);
146
+ break;
124
147
 
125
148
  default:
126
149
  throw new Error(`Unknown tool: ${name}`);
@@ -142,6 +165,13 @@ async function main() {
142
165
  });
143
166
 
144
167
  const transport = new StdioServerTransport();
168
+
169
+ // Load plugins before starting the server
170
+ const pluginCount = await loadPlugins();
171
+ if (pluginCount > 0) {
172
+ logger.info(`${pluginCount} plugin(s) loaded and ready`);
173
+ }
174
+
145
175
  await server.connect(transport);
146
176
  logger.info(`MCPBrowser server v${packageJson.version} started`);
147
177
  }
@@ -171,7 +201,16 @@ export {
171
201
  getCurrentHtml,
172
202
  takeScreenshot,
173
203
  scrollPage,
174
- handleAcceptEula
204
+ navigateHistory,
205
+ handleAcceptEula,
206
+ // Plugin system exports
207
+ loadPlugins,
208
+ getLoadedPlugins,
209
+ getPlugin,
210
+ detectPlugins,
211
+ getPluginNextSteps,
212
+ pluginAction,
213
+ pluginInfo
175
214
  };
176
215
 
177
216
  // Run the MCP server only if this is the main module (not imported for testing)
@@ -0,0 +1,139 @@
1
+ /**
2
+ * _example plugin — Stub plugin for testing and documentation.
3
+ * Implements the full plugin interface contract (interfaceVersion 1).
4
+ * Matches pages on "example.test" domain for unit/integration testing.
5
+ */
6
+
7
+ import { MCPResponse } from '../../core/responses.js';
8
+
9
+ // ============================================================================
10
+ // MANIFEST
11
+ // ============================================================================
12
+
13
+ export const manifest = {
14
+ name: "_example",
15
+ version: "1.0.0",
16
+ description: "Example stub plugin for testing the MCPBrowser plugin system",
17
+ interfaceVersion: 1,
18
+ urlPatterns: ["example.test"],
19
+ domPatterns: [".example-plugin-marker"]
20
+ };
21
+
22
+ // ============================================================================
23
+ // DETECTION
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Detect whether this plugin is applicable for the given page.
28
+ * @param {string} url - Current page URL
29
+ * @param {string} html - Extracted page HTML
30
+ * @returns {{ matched: boolean, confidence?: number }}
31
+ */
32
+ export function matchesPage(url, html) {
33
+ try {
34
+ // Fast URL check first
35
+ if (url && url.includes("example.test")) {
36
+ return { matched: true, confidence: 1.0 };
37
+ }
38
+ // DOM fallback check
39
+ if (html && html.includes("example-plugin-marker")) {
40
+ return { matched: true, confidence: 0.8 };
41
+ }
42
+ return { matched: false };
43
+ } catch {
44
+ return { matched: false };
45
+ }
46
+ }
47
+
48
+ // ============================================================================
49
+ // ACTIONS
50
+ // ============================================================================
51
+
52
+ class ExampleActionResponse extends MCPResponse {
53
+ constructor(data, nextSteps) {
54
+ super(nextSteps);
55
+ this.data = data;
56
+ }
57
+ _getAdditionalFields() { return { data: this.data }; }
58
+ getTextSummary() { return `Example action returned ${Array.isArray(this.data) ? this.data.length : 0} items`; }
59
+ }
60
+
61
+ /**
62
+ * Return the complete list of actions this plugin provides.
63
+ * @returns {import('../../specs/002-site-plugins/data-model.md').ActionDescriptor[]}
64
+ */
65
+ export function getActions() {
66
+ return [
67
+ {
68
+ name: "list_items",
69
+ description: "List items from the example page",
70
+ params: [
71
+ { name: "limit", type: "number", description: "Maximum number of items to return", required: false, default: 10 }
72
+ ],
73
+ execute: async ({ page, params }) => {
74
+ const limit = params?.limit ?? 10;
75
+ const items = await page.evaluate((lim) => {
76
+ const rows = document.querySelectorAll('.item-row');
77
+ return Array.from(rows).slice(0, lim).map(row => ({
78
+ title: row.querySelector('.title')?.textContent?.trim() || 'Untitled',
79
+ date: row.querySelector('.date')?.textContent?.trim() || ''
80
+ }));
81
+ }, limit);
82
+
83
+ return new ExampleActionResponse(items, [
84
+ "Call plugin_action with action 'get_item_detail' to read a specific item"
85
+ ]);
86
+ }
87
+ },
88
+ {
89
+ name: "get_item_detail",
90
+ description: "Get details for a specific item by ID",
91
+ params: [
92
+ { name: "itemId", type: "string", description: "Item identifier", required: true }
93
+ ],
94
+ execute: async ({ page, params }) => {
95
+ if (!params?.itemId) {
96
+ throw new Error("itemId parameter is required");
97
+ }
98
+ const detail = await page.evaluate((id) => {
99
+ const el = document.querySelector(`[data-id="${id}"]`);
100
+ if (!el) return null;
101
+ return {
102
+ id,
103
+ title: el.querySelector('.title')?.textContent?.trim() || '',
104
+ body: el.querySelector('.body')?.textContent?.trim() || ''
105
+ };
106
+ }, params.itemId);
107
+
108
+ if (!detail) {
109
+ const { ErrorResponse } = await import('../../src/core/responses.js');
110
+ return new ErrorResponse(
111
+ `Item '${params.itemId}' not found on the page`,
112
+ ["Verify the item ID is correct", "Use list_items to see available items"]
113
+ );
114
+ }
115
+
116
+ return new ExampleActionResponse(detail, [
117
+ "Use plugin_action with action 'list_items' to see all items"
118
+ ]);
119
+ }
120
+ }
121
+ ];
122
+ }
123
+
124
+ // ============================================================================
125
+ // INFO
126
+ // ============================================================================
127
+
128
+ /**
129
+ * Return high-level plugin context for the AI agent.
130
+ * @returns {import('../../specs/002-site-plugins/data-model.md').PluginInfo}
131
+ */
132
+ export function getInfo() {
133
+ return {
134
+ description: "Example stub plugin for testing MCPBrowser's plugin system. Lists items and retrieves item details from example.test pages.",
135
+ targetPages: ["Example test page (example.test)"],
136
+ authFlow: "No authentication required — example.test is a test domain",
137
+ actions: getActions().map(({ name, description, params }) => ({ name, description, params }))
138
+ };
139
+ }
@@ -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
+ }