Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:58:00 +08:00
commit ef9c3b3679
11 changed files with 1118 additions and 0 deletions

439
skills/play-tight/SKILL.md Normal file
View File

@@ -0,0 +1,439 @@
---
name: play-tight
description: Context-efficient browser automation using Playwright scripts and subagent isolation. Use when you need to interact with web pages, extract data from websites, verify page elements, or automate browser tasks while avoiding context window pollution from verbose HTML/accessibility trees. Provides both direct script execution and a specialized subagent pattern for complex investigations that generate large intermediate responses.
---
# Play-Tight
Play-Tight provides context-efficient browser automation using Playwright scripts executed via bash, with an optional subagent pattern for isolating verbose browser responses. The skill replaces the verbose Playwright MCP server with optimized scripts and subagent patterns to minimize context window pollution, saving context for actual engineering processes.
## When to Use This Skill
Use Play-Tight when you need to:
- Verify elements exist on web pages (buttons, forms, links)
- Extract text content or structured data from websites
- Check status of web dashboards (CI/CD, monitoring, PRs)
- Take screenshots of pages or specific elements
- Automate repetitive browser tasks
- Validate web application behavior
## Automatic Browser Detection
**IMPORTANT**: Before using any Playtight script, automatically check if Playwright browsers are installed.
### Detection Process
1. **Check for browser installation** by running:
```bash
node -e "const { chromium } = require('playwright'); chromium.executablePath()" 2>&1
```
2. **If the check fails** (exit code non-zero or error message):
- Inform the user that Playwright browsers need to be installed
- Offer to run the installation: "I can install the required browsers by running `npm run install-browsers` in the scripts directory. Would you like me to do that?"
- If user agrees, run:
```bash
cd ~/.claude/plugins/mad-skills/play-tight/scripts && npm run install-browsers
```
3. **If npm dependencies are missing** (error about 'playwright' module not found):
- First install npm dependencies: `cd ~/.claude/plugins/mad-skills/play-tight/scripts && npm install`
- Then install browsers: `npm run install-browsers`
### Example Detection Flow
```bash
# First attempt to use a script
node scripts/check-element.js https://example.com h1
# If error mentions "browserType.launch: Executable doesn't exist"
# Or "Cannot find module 'playwright'"
# Then detect and offer installation:
"I see that Playwright browsers aren't installed yet. I can install them now by running 'npm run install-browsers'. This will download Chromium (~100MB). Would you like me to proceed?"
# On user approval:
cd ~/.claude/plugins/mad-skills/play-tight/scripts && npm install && npm run install-browsers
```
**When to skip detection**: If you've already successfully run a Play-Tight script in the current session, browsers are installed and you can skip detection.
## Two Approaches
### Approach 1: Direct Script Execution (Simple Tasks)
For straightforward tasks where responses are compact, use scripts directly:
```bash
node scripts/check-element.js <url> <selector>
node scripts/get-text.js <url> [selector]
node scripts/take-screenshot.js <url> <output-path> [selector]
```
**Use when:**
- Single element check
- Quick text extraction
- Simple verification
- Parent context has room
### Approach 2: Subagent Isolation (Complex Tasks)
For tasks requiring multiple iterations or verbose exploration, delegate to the browser-investigator subagent:
```bash
# In parent agent
"Use browser-investigator subagent to find all login form elements on example.com"
```
**Use when:**
- Multiple exploration steps needed
- Large HTML responses expected
- Complex element discovery
- Parent context is precious
## Available Scripts
### check-element.js
Check if an element exists and get its properties.
**Usage:**
```bash
node scripts/check-element.js https://example.com "#login-button"
```
**Returns:**
```json
{
"found": true,
"tagName": "BUTTON",
"text": "Sign In",
"visible": true,
"enabled": true,
"attributes": {
"id": "login-button",
"class": "btn btn-primary",
"type": "submit"
}
}
```
**Key features:**
- Text limited to 100 chars to prevent context flooding
- Only essential attributes extracted (id, class, type, href, value)
- Visibility and enabled state checked
- 30s timeout for page load
### get-text.js
Extract text content from element or entire page.
**Usage:**
```bash
# From specific element
node scripts/get-text.js https://example.com ".main-content"
# From entire page
node scripts/get-text.js https://example.com
```
**Returns:**
```json
{
"found": true,
"text": "extracted text content...",
"length": 1523,
"truncated": false
}
```
**Key features:**
- Auto-truncates to 2000 chars to prevent context flooding
- Extracts only visible text (excludes hidden, script, style tags)
- Selector optional - defaults to entire page body
- Whitespace normalized
### take-screenshot.js
Capture page or element screenshot.
**Usage:**
```bash
# Full page
node scripts/take-screenshot.js https://example.com output.png
# Specific element
node scripts/take-screenshot.js https://example.com output.png "#dashboard"
```
**Returns:**
```json
{
"success": true,
"path": "/absolute/path/to/output.png",
"url": "https://example.com",
"selector": "full-page"
}
```
**Key features:**
- Full page or specific element
- Returns file path for evidence
- Keeps response compact by saving to disk
### navigate-and-extract.js
Extract structured data using configuration.
**Usage:**
```bash
node scripts/navigate-and-extract.js "https://example.com" '{
"waitFor": ".content",
"selectors": {"title": "h1", "description": ".desc"},
"counts": {"items": ".list-item"},
"checks": {"has_error": ".error-message"}
}'
```
**Config format:**
- `waitFor` (optional): Selector to wait for before extracting
- `selectors`: Map of names to selectors for text extraction (200 char limit per field)
- `counts`: Map of names to selectors for element counting
- `checks`: Map of names to selectors for visibility checks
**Returns:**
```json
{
"success": true,
"url": "https://example.com",
"data": {
"title": "Page Title",
"description": "Page description text",
"items": 5,
"has_error": false
}
}
```
**Example:**
```bash
node scripts/navigate-and-extract.js "https://github.com/user/repo/pull/123" '{
"waitFor": ".merge-status-item",
"selectors": {"title": ".js-issue-title"},
"counts": {"total_checks": ".merge-status-item"},
"checks": {"is_approved": ".review-status.approved"}
}'
```
## Browser Investigator Subagent
For complex tasks that generate verbose intermediate responses, use the browser-investigator subagent.
**Location:** `agents/browser-investigator-subagent.md`
**Purpose:** Execute complex multi-step browser investigations while isolating verbose responses from parent agent.
### Setup
1. Copy the subagent definition to your project:
```bash
cp agents/browser-investigator-subagent.md .claude/agents/
```
2. Ensure scripts are accessible from project root:
```bash
# Either copy scripts to project
cp -r scripts/ ./scripts/
# Or create symlink
ln -s /path/to/skill/scripts ./scripts
```
### Usage Pattern
```bash
# Parent agent delegates task
"Use browser-investigator subagent to check the status of PR #123 on github.com/user/repo"
```
The subagent will:
1. Make multiple script calls to explore the page (5-10+ calls typical)
2. Absorb all verbose HTML/accessibility responses in its context
3. Process and filter the information
4. Return concise structured summary (< 500 tokens) to parent
### Return Format
The subagent always returns structured JSON:
**Element location:**
```json
{
"type": "element_location",
"selector": "#login-button",
"element_type": "button",
"verification": "element found and visible",
"attempts": 3
}
```
**Data extraction:**
```json
{
"type": "data_extraction",
"extracted_data": {...},
"data_points": 5,
"confidence": "high"
}
```
**Verification:**
```json
{
"type": "verification",
"status": "success",
"checks_completed": ["element_exists", "text_matches"],
"issues": [],
"screenshot_path": "/path/to/screenshot.png"
}
```
**Status check:**
```json
{
"type": "status_check",
"url": "https://example.com",
"status": {...},
"summary": "brief description"
}
```
See `agents/browser-investigator-subagent.md` for complete subagent definition.
## Setup Requirements
### First-Time Setup
```bash
# In skill's scripts directory
cd scripts/
npm install
npm run install-browsers
```
This installs Playwright and Chromium browser.
### Project Integration
For projects using this skill:
```bash
# Option 1: Copy scripts to project
cp -r /path/to/skill/scripts ./browser-scripts
cd browser-scripts && npm install && npm run install-browsers
# Option 2: Global installation (if using across multiple projects)
cd /path/to/skill/scripts
npm install -g playwright
playwright install chromium
```
## Common Patterns
See `references/patterns.md` for detailed examples including:
- GitHub PR status checks
- Form field discovery
- Content verification
- CI/CD dashboard monitoring
## Context Efficiency
**Problem**: Browser automation typically floods context with verbose HTML, accessibility trees, and JavaScript. The Playwright MCP server returns 50KB+ HTML accessibility trees per interaction.
**Solution**: Playtight solves it two ways:
1. **Scripts return compact JSON**: Only essential data, no raw HTML
2. **Subagent isolation**: Verbose exploration happens in subagent's context, parent receives summary
### Comparison
**Traditional Playwright MCP:**
```
Query: "Find login form"
Response 1: [30KB HTML tree] = 7,200 tokens
Response 2: [25KB narrowing] = 6,000 tokens
Response 3: [20KB more] = 4,800 tokens
Total: 18,000 tokens, context nearly exhausted
```
**Playtight Direct Scripts:**
```
Query: "Find login form"
Script 1: {found: true, ...} = 150 tokens
Script 2: {found: true, ...} = 150 tokens
Script 3: {found: true, ...} = 150 tokens
Total: 450 tokens in parent context
```
**Playtight with Subagent:**
```
Parent query: "Find login form"
Subagent internally:
- 10 script calls = 1,500 tokens (isolated in subagent)
Parent receives:
- {type: "element_location", elements: {...}} = 80 tokens
Total in parent: 80 tokens
```
**Result:** 225x more efficient, enabling 100+ queries vs 2-3 with MCP
## Best Practices
1. **Use subagent for exploration**: When you need multiple iterations to find elements, delegate to browser-investigator
2. **Direct scripts for known targets**: When you know exact selectors, use scripts directly
3. **Batch extractions**: Use navigate-and-extract for multiple data points
4. **Error handling**: Always check `success`, `found`, or `error` fields in responses
5. **Screenshot for evidence**: When reporting issues, take screenshots and save to file
6. **Context preservation**: Keep parent agent context clean for engineering work
## Critical Design Principles
When modifying or extending this skill:
1. **Never Return Raw HTML** - Always return structured JSON with specific fields
2. **Truncate Everything** - Text: 100-2000 chars, errors: 100 chars
3. **Keep Responses Compact** - Target < 500 bytes per script response
4. **Filter at Source** - Extract only essential data in the script
5. **Use Subagent for Verbosity** - Complex exploration happens in isolated context
6. **Headless by Default** - All scripts use headless: true for performance
7. **Timeout Protection** - 30s page load timeout, avoid hangs
## Troubleshooting
### Browser not found / "Executable doesn't exist"
This should be automatically detected (see "Automatic Browser Detection" section above). If you encounter this error:
1. Check if npm dependencies are installed:
```bash
cd ~/.claude/plugins/mad-skills/play-tight/scripts
npm list playwright
```
2. If playwright is missing, install dependencies:
```bash
npm install
```
3. Install browsers:
```bash
npm run install-browsers
```
The automatic detection should offer to do this for you when you first use the skill.
### Script errors
Check that Playwright is installed: `npm list playwright` in the scripts directory
### Element not found
- Verify selector is correct
- Try get-text.js to see page content
- Take screenshot to visually inspect
- Use waitFor in navigate-and-extract config
### Timeout errors
Increase timeout or check network connectivity. Scripts use 30s default timeout for page loads.

View File

@@ -0,0 +1,136 @@
# Browser Investigator
You are a browser investigation specialist that handles verbose web automation tasks while keeping the parent agent's context clean.
## Your Mission
Execute browser automation tasks using Playwright scripts while isolating large HTML/accessibility responses from the parent agent's context.
## Available Tools
You have access to these browser automation scripts:
- `node scripts/check-element.js <url> <selector>` - Check if element exists, returns compact JSON
- `node scripts/get-text.js <url> [selector]` - Extract text content, truncated to 2000 chars
- `node scripts/take-screenshot.js <url> <output-path> [selector]` - Capture screenshots
- `node scripts/navigate-and-extract.js <url> <config-json>` - Structured data extraction
All scripts return compact, structured JSON - never raw HTML or verbose accessibility trees.
## Browser Detection
**BEFORE running any script for the first time**, check if browsers are installed:
```bash
node -e "const { chromium } = require('playwright'); chromium.executablePath()" 2>&1
```
**If this fails**:
1. Install npm dependencies if missing: `cd ~/.claude/plugins/mad-skills/play-tight/scripts && npm install`
2. Install browsers: `npm run install-browsers`
3. Inform parent agent that setup was needed (include in your summary)
**Skip this check** if you've already successfully run a script in this session.
## Process
1. **Receive Task**: Parent agent delegates browser automation task
2. **Iterate Internally**: Use scripts to explore and narrow down (you absorb verbose responses)
3. **Process Results**: Extract only essential information from script outputs
4. **Return Summary**: Send back concise, structured results to parent (< 500 tokens)
## Return Format Guidelines
Always return concise, structured results:
### For Element Location Tasks
```json
{
"type": "element_location",
"selector": "the-final-selector",
"element_type": "button|input|link|etc",
"verification": "confirmed working",
"attempts": 3
}
```
### For Data Extraction Tasks
```json
{
"type": "data_extraction",
"extracted_data": {...},
"data_points": 5,
"confidence": "high"
}
```
### For Verification Tasks
```json
{
"type": "verification",
"status": "pass|fail",
"checks_completed": 8,
"issues": [],
"screenshot_path": "path/if/needed"
}
```
### For Status Checks
```json
{
"type": "status_check",
"url": "checked-url",
"status": {
"key1": "value1",
"key2": true
},
"summary": "brief-description"
}
```
## Critical Rules
1. **NEVER pass raw HTML/accessibility trees back to parent**
2. **Process all verbose responses in YOUR context**
3. **If you need to show evidence, save to file and return path**
4. **Keep parent communication to < 500 tokens**
5. **Use multiple script calls if needed - you absorb the context cost**
6. **Extract only actionable information for parent**
## Example Workflow
```bash
# Bad: Would flood parent's context
# (Don't do this in parent agent)
# Good: Isolate in subagent context
Task: "Find login form elements on example.com"
Step 1: node scripts/check-element.js https://example.com "form"
# (Large response absorbed in subagent context)
Step 2: node scripts/check-element.js https://example.com "#username"
# (Another response absorbed)
Step 3: node scripts/check-element.js https://example.com "#password"
# (More context absorbed)
Return to parent:
{
"type": "element_location",
"elements": {
"username": "#username",
"password": "#password",
"submit": "button[type='submit']"
},
"verification": "all elements found and visible",
"attempts": 3
}
```
## Notes
- You may make 5-10+ script calls to narrow down to the right answer
- All that verbosity stays in YOUR context, not parent's
- Think of yourself as a filter: verbose in, concise out
- Parent trusts your summary - make it count

View File

@@ -0,0 +1,81 @@
# Browser Automation Patterns
## Common Extraction Patterns
### GitHub PR Status Check
```bash
node scripts/navigate-and-extract.js "https://github.com/user/repo/pull/123" '{
"waitFor": ".merge-status-item",
"counts": {
"total_checks": ".merge-status-item",
"passing_checks": ".merge-status-item.text-green"
},
"checks": {
"has_conflicts": "text=conflicts",
"is_approved": ".review-status.approved"
},
"selectors": {
"title": ".js-issue-title"
}
}'
```
### Form Field Discovery
```bash
# Step 1: Find form
node scripts/check-element.js "https://example.com" "form#login"
# Step 2: Find inputs
node scripts/check-element.js "https://example.com" "input[name='username']"
node scripts/check-element.js "https://example.com" "input[name='password']"
node scripts/check-element.js "https://example.com" "button[type='submit']"
```
### Content Verification
```bash
# Check if specific text appears
node scripts/get-text.js "https://example.com/docs" ".main-content"
# Verify element visibility
node scripts/check-element.js "https://example.com" "#success-message"
```
### CI/CD Dashboard Status
```bash
node scripts/navigate-and-extract.js "https://ci.example.com/build/123" '{
"waitFor": ".build-status",
"selectors": {
"status": ".build-status",
"branch": ".branch-name",
"commit": ".commit-hash"
},
"checks": {
"is_passing": ".status-success",
"is_running": ".status-running"
}
}'
```
## Error Handling
All scripts return JSON with error information:
```json
{
"found": false,
"error": "timeout navigating to page"
}
```
Check `success`, `found`, or `error` fields in responses.
## Performance Tips
1. **Use waitFor**: Specify element to wait for in navigate-and-extract
2. **Batch extractions**: Use navigate-and-extract for multiple elements
3. **Headless mode**: All scripts run headless by default (fast)
4. **Truncation**: Scripts automatically limit text length

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env node
/**
* Check if an element exists on a page and return its properties
* Usage: node check-element.js <url> <selector>
* Returns: Compact JSON with element info
*/
const { chromium } = require('playwright');
async function checkElement(url, selector) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
// Wait briefly for dynamic content
await page.waitForTimeout(1000);
const element = await page.$(selector);
if (!element) {
await browser.close();
return { found: false, selector };
}
// Extract only essential information
const info = await element.evaluate((el) => {
const rect = el.getBoundingClientRect();
return {
found: true,
tagName: el.tagName,
text: el.textContent?.trim().substring(0, 100) || '',
visible: rect.width > 0 && rect.height > 0,
enabled: !el.disabled,
attributes: {
id: el.id || null,
class: el.className || null,
type: el.type || null,
href: el.href || null,
value: el.value || null
}
};
});
await browser.close();
return info;
} catch (error) {
await browser.close();
return {
found: false,
error: error.message.substring(0, 100)
};
}
}
// Main execution
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(JSON.stringify({
error: 'Usage: node check-element.js <url> <selector>'
}));
process.exit(1);
}
const [url, selector] = args;
checkElement(url, selector)
.then(result => console.log(JSON.stringify(result, null, 2)))
.catch(error => console.log(JSON.stringify({ error: error.message })));
}
module.exports = { checkElement };

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env node
/**
* Extract text content from a specific element or entire page
* Usage: node get-text.js <url> [selector]
* If selector is omitted, extracts visible text from body
* Returns: Compact JSON with extracted text
*/
const { chromium } = require('playwright');
async function getText(url, selector = null) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1000);
let text;
let found = true;
if (selector) {
const element = await page.$(selector);
if (!element) {
await browser.close();
return { found: false, selector };
}
text = await element.textContent();
} else {
// Extract visible text from body, excluding script/style tags
text = await page.evaluate(() => {
const body = document.body;
const walker = document.createTreeWalker(
body,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node) => {
const parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_REJECT;
const tag = parent.tagName.toLowerCase();
if (['script', 'style', 'noscript'].includes(tag)) {
return NodeFilter.FILTER_REJECT;
}
const style = window.getComputedStyle(parent);
if (style.display === 'none' || style.visibility === 'hidden') {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
}
);
let text = '';
let node;
while (node = walker.nextNode()) {
text += node.textContent + ' ';
}
return text;
});
}
await browser.close();
// Clean and truncate text
const cleaned = text.trim().replace(/\s+/g, ' ');
return {
found,
text: cleaned.substring(0, 2000), // Limit to 2000 chars
length: cleaned.length,
truncated: cleaned.length > 2000
};
} catch (error) {
await browser.close();
return {
found: false,
error: error.message.substring(0, 100)
};
}
}
// Main execution
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log(JSON.stringify({
error: 'Usage: node get-text.js <url> [selector]'
}));
process.exit(1);
}
const [url, selector] = args;
getText(url, selector)
.then(result => console.log(JSON.stringify(result, null, 2)))
.catch(error => console.log(JSON.stringify({ error: error.message })));
}
module.exports = { getText };

View File

@@ -0,0 +1,112 @@
#!/usr/bin/env node
/**
* Navigate to a page and extract structured data
* Usage: node navigate-and-extract.js <url> <extraction-config-json>
* Config format: {"selectors": {"name": "selector", ...}, "waitFor": "selector"}
* Returns: Compact JSON with extracted data
*/
const { chromium } = require('playwright');
async function navigateAndExtract(url, config) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
// Wait for specific element if specified
if (config.waitFor) {
await page.waitForSelector(config.waitFor, { timeout: 10000 }).catch(() => {});
} else {
await page.waitForTimeout(1000);
}
const results = {};
// Extract data for each selector
for (const [name, selector] of Object.entries(config.selectors || {})) {
try {
const element = await page.$(selector);
if (element) {
const text = await element.textContent();
results[name] = text.trim().substring(0, 200);
} else {
results[name] = null;
}
} catch (e) {
results[name] = null;
}
}
// Count elements if requested
if (config.counts) {
for (const [name, selector] of Object.entries(config.counts)) {
try {
const count = await page.$$(selector).then(els => els.length);
results[name] = count;
} catch (e) {
results[name] = 0;
}
}
}
// Check for element visibility if requested
if (config.checks) {
for (const [name, selector] of Object.entries(config.checks)) {
try {
const element = await page.$(selector);
results[name] = element !== null && await element.isVisible();
} catch (e) {
results[name] = false;
}
}
}
await browser.close();
return {
success: true,
url,
data: results
};
} catch (error) {
await browser.close();
return {
success: false,
error: error.message.substring(0, 100)
};
}
}
// Main execution
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(JSON.stringify({
error: 'Usage: node navigate-and-extract.js <url> <config-json>'
}));
process.exit(1);
}
const [url, configJson] = args;
let config;
try {
config = JSON.parse(configJson);
} catch (e) {
console.log(JSON.stringify({
error: 'Invalid JSON config: ' + e.message
}));
process.exit(1);
}
navigateAndExtract(url, config)
.then(result => console.log(JSON.stringify(result, null, 2)))
.catch(error => console.log(JSON.stringify({ error: error.message })));
}
module.exports = { navigateAndExtract };

View File

@@ -0,0 +1,11 @@
{
"name": "browser-automation-scripts",
"version": "1.0.0",
"description": "Browser automation scripts using Playwright",
"scripts": {
"install-browsers": "playwright install chromium"
},
"dependencies": {
"playwright": "^1.40.0"
}
}

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env node
/**
* Take a screenshot of a page or specific element
* Usage: node take-screenshot.js <url> <output-path> [selector]
* If selector is provided, captures only that element
* Returns: Compact JSON with screenshot info
*/
const { chromium } = require('playwright');
const path = require('path');
async function takeScreenshot(url, outputPath, selector = null) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1000);
const absolutePath = path.resolve(outputPath);
if (selector) {
const element = await page.$(selector);
if (!element) {
await browser.close();
return { success: false, error: `Element not found: ${selector}` };
}
await element.screenshot({ path: absolutePath });
} else {
await page.screenshot({ path: absolutePath, fullPage: true });
}
await browser.close();
return {
success: true,
path: absolutePath,
url,
selector: selector || 'full-page'
};
} catch (error) {
await browser.close();
return {
success: false,
error: error.message.substring(0, 100)
};
}
}
// Main execution
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(JSON.stringify({
error: 'Usage: node take-screenshot.js <url> <output-path> [selector]'
}));
process.exit(1);
}
const [url, outputPath, selector] = args;
takeScreenshot(url, outputPath, selector)
.then(result => console.log(JSON.stringify(result, null, 2)))
.catch(error => console.log(JSON.stringify({ error: error.message })));
}
module.exports = { takeScreenshot };