Initial commit
This commit is contained in:
439
skills/play-tight/SKILL.md
Normal file
439
skills/play-tight/SKILL.md
Normal 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.
|
||||
136
skills/play-tight/agents/browser-investigator-subagent.md
Normal file
136
skills/play-tight/agents/browser-investigator-subagent.md
Normal 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
|
||||
81
skills/play-tight/references/patterns.md
Normal file
81
skills/play-tight/references/patterns.md
Normal 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
|
||||
77
skills/play-tight/scripts/check-element.js
Normal file
77
skills/play-tight/scripts/check-element.js
Normal 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 };
|
||||
105
skills/play-tight/scripts/get-text.js
Normal file
105
skills/play-tight/scripts/get-text.js
Normal 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 };
|
||||
112
skills/play-tight/scripts/navigate-and-extract.js
Normal file
112
skills/play-tight/scripts/navigate-and-extract.js
Normal 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 };
|
||||
11
skills/play-tight/scripts/package.json
Normal file
11
skills/play-tight/scripts/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
70
skills/play-tight/scripts/take-screenshot.js
Normal file
70
skills/play-tight/scripts/take-screenshot.js
Normal 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 };
|
||||
Reference in New Issue
Block a user