Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:05:10 +08:00
commit 1c87823db1
17 changed files with 1920 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "browser-devtools",
"description": "Provides debugging web applications, monitoring network traffic, capturing screenshots, DOM inspection, performance analysis, and testing UI interactions during development.",
"version": "1.0.0",
"author": {
"name": "Truong Vu",
"email": "vukhanhtruong@gmail.com"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# browser-devtools
Provides debugging web applications, monitoring network traffic, capturing screenshots, DOM inspection, performance analysis, and testing UI interactions during development.

117
plugin.lock.json Normal file
View File

@@ -0,0 +1,117 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:vukhanhtruong/claude-rock:plugins/browser-devtools",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "e51c3cedbeb21afc81ced385efb0aa6976fbe6c3",
"treeHash": "d6f5e26dd43ebcca96337e1538ddd9cc8f44110c8507e8a1c92364008d41a098",
"generatedAt": "2025-11-28T10:28:57.788668Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "browser-devtools",
"description": "Provides debugging web applications, monitoring network traffic, capturing screenshots, DOM inspection, performance analysis, and testing UI interactions during development.",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "38dfdbd3c10af36ebfabf1203c02270d2045aa34a7eee604137bec119b2f8028"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "76a52444895477ed9620f3df67409a932e116337aa2060661b3e3355fe241931"
},
{
"path": "skills/README.md",
"sha256": "e91b6a4167fa0f016c96601eb90fa165ea01a8cf13a24a0e276ca44abbd95d42"
},
{
"path": "skills/package.json",
"sha256": "83e99bfc5e0b167b06cfab769d9532a60c51dd10016211f82191d20e31e390db"
},
{
"path": "skills/SKILL.md",
"sha256": "28416849905ae07e830944ef6bdd831ab79d43068307497b79e52f9273ab63c6"
},
{
"path": "skills/tsconfig.json",
"sha256": "25f0d1e202386f6e181165e1e5f1ca90a019d79a944d7f4f9931b4c6932a5a8c"
},
{
"path": "skills/scripts/snapshot.ts",
"sha256": "9cccadf3bb954a526b0e670934c7532f1c53cdb49cffcbdcd424af9de300a7b7"
},
{
"path": "skills/scripts/performance.ts",
"sha256": "dc9bb6a83df02602055e68ed1eea643c7d65ea676b8df023781f8ea1ff81e3d9"
},
{
"path": "skills/scripts/screenshot.ts",
"sha256": "b9538022a209daf9d4dce77d4b94fa9a353316640a94f73d5c53eef66e25847d"
},
{
"path": "skills/scripts/evaluate.ts",
"sha256": "35fa37032f7710ae4ba027d3cbaa33a19149339dcaa94924971657ee0906fb01"
},
{
"path": "skills/scripts/network.ts",
"sha256": "3b4c6d4895b05f25b8c7b24c0d913e8dc0a56e09ab992fef2ad26e8a84399c79"
},
{
"path": "skills/scripts/install.sh",
"sha256": "d7e1a4602369fcf7e5bf1c567b5be584a6a75fcaf419049c0146ad68543082a0"
},
{
"path": "skills/scripts/navigate.ts",
"sha256": "100da6c50337b7f78b59d61236d2d4f07a4700cf3076eb6f613b351a911e9601"
},
{
"path": "skills/scripts/fill.ts",
"sha256": "c941da84266b958d02a10017ea9b38cc3591e839400320d75623bfcefcb4dd3d"
},
{
"path": "skills/scripts/console.ts",
"sha256": "7d7c259a15158278f39e70e6d68105ab659665523e063a2b4b4b1ec5963866da"
},
{
"path": "skills/scripts/click.ts",
"sha256": "f61e65228e6b2e09ee9b108d774260b9f97b890839bfe4eb698fcf29b29a65ee"
},
{
"path": "skills/lib/browser.ts",
"sha256": "c67574013642161b989bd92c697c30328b4ea15ec83430fc55926ee674fbaaef"
},
{
"path": "skills/lib/browser.d.ts.map",
"sha256": "4ce628498e2ae037e302afd194f5bc03c74da42534e8603b76af80d643d89de4"
},
{
"path": "skills/lib/browser.js.map",
"sha256": "2bb10742fba305c7e02db956a45375fb0388b61fd1c5e38a29955f33b043eadc"
},
{
"path": "skills/lib/browser.js",
"sha256": "b8a28a98bd8ddf437dd82a9cc1ef1e4cc8abc0946e7753233fc7db7a618e8914"
},
{
"path": "skills/lib/browser.d.ts",
"sha256": "f6aa94f88755cb3ff4391e32d6830888cd1e668a9ca071b322bee19cdc964a26"
}
],
"dirSha256": "d6f5e26dd43ebcca96337e1538ddd9cc8f44110c8507e8a1c92364008d41a098"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

151
skills/README.md Normal file
View File

@@ -0,0 +1,151 @@
# Browser Devtools
Use natural language prompts to debug and automate web browsers with Playwright.
## How to Use
### Basic Navigation
- "Navigate to https://example.com and tell me the page title"
- "Open my local app at http://localhost:3000"
- "Check if my website is accessible"
- "Show me what https://github.com/vukhanhtruong/claude-rock looks like"
### Screenshots
- "Take a screenshot of the entire page at https://example.com"
- "Capture just the header section of my local app"
- "Screenshot the login form and save it as login-page.png"
- "Take a full page screenshot of my dashboard"
### Finding Elements
- "Show me all the buttons on this page"
- "What interactive elements are on https://example.com?"
- "Find all input fields and forms on the page"
- "List all links and their text content"
- "Show me the CSS selectors for all buttons"
### Console Debugging
- "Check for JavaScript errors on my page"
- "Monitor the console for warnings and errors for 10 seconds"
- "Show me all console messages while I interact with the site"
- "Are there any 404 errors or missing resources?"
- "Watch the console for network failures"
### Network Monitoring
- "Show me all API calls being made by this page"
- "Monitor network requests for the next 5 seconds"
- "Are there any failed requests or slow loading resources?"
- "Track all XHR and fetch requests"
- "Show me the largest resources loading on the page"
### Performance Analysis
- "Check the Core Web Vitals for my site"
- "What's the LCP (Largest Contentful Paint) for this page?"
- "Is my site performing well? Give me the performance metrics"
- "Run a performance audit and show me the results"
- "Compare performance between my staging and production sites"
### Form Testing
- "Fill out the login form with test credentials"
- "Enter 'test@example.com' in the email field and 'password123' in the password field"
- "Submit the contact form and see what happens"
- "Test the search functionality by entering 'query' and clicking search"
- "Fill out all required fields in the registration form"
### Element Interaction
- "Click the submit button"
- "Click on the menu toggle"
- "Click the 'Add to Cart' button and wait for the result"
- "Interact with the dropdown menu"
- "Click on the first link in the navigation"
### JavaScript Execution
- "Run `document.title` to get the page title"
- "Execute `document.querySelectorAll('.error').length` to count errors"
- "Check if there are any console errors with JavaScript"
- "Run `localStorage.clear()` to clear storage"
- "Execute `window.scrollTo(0, document.body.scrollHeight)` to scroll to bottom"
### Debugging Workflows
- "I'm getting a JavaScript error on my site. Can you find it?"
- "My form isn't submitting properly. Can you test it?"
- "The page loads slowly. Can you analyze the performance?"
- "Some API calls are failing. Monitor the network for me"
- "I need to test my responsive design. Take screenshots at different sizes"
### Development Testing
- "Test my local development server at localhost:3000"
- "Check if my staging environment is working"
- "Monitor my app for errors while I use it"
- "Take before and after screenshots of my changes"
- "Verify that my new feature is working correctly"
## Example Conversations
### Debugging Console Errors
```
User: "My React app is showing errors at http://localhost:3000. Can you check the console?"
Claude: [Analyzes console output] "I found 3 JavaScript errors related to missing props..."
```
### Form Testing
```
User: "Test my login form here https://www.saucedemo.com/, use standard_user and secret_sauce"
Claude: "I'll navigate to your login page. What's the URL and what credentials should I use?"
User: ""
Claude: [Navigates, fills form, submits] "The form submitted successfully and redirected to dashboard"
```
### Performance Issues
```
User: "run performance test this URL https://myapp.com"
Claude: [Analyzes performance] "Your LCP is 4.2 seconds which is slow. The main bottleneck is..."
```
## Tips for Best Results
- **Be specific about URLs** - Provide the full URL including http:// or https://
- **Describe what you're testing** - Explain what you expect to happen
- **Mention local vs production** - Specify if you're testing localhost or a live site
- **Provide credentials when needed** - For form testing, give test usernames/passwords
- **Describe the problem** - Explain what issue you're trying to diagnose
- **Ask for specific metrics** - Request particular performance data or error types
## Common Scenarios
### "My site is broken" workflow:
1. "Check if https://mysite.com loads"
2. "Look for JavaScript errors in the console"
3. "Monitor network requests for failures"
4. "Take a screenshot to see what's visible"
### "Test my new feature" workflow:
1. "Navigate to http://localhost:3000/new-feature"
2. "Click the [button/element] to activate it"
3. "Watch the console for any errors"
4. "Verify the expected result appears"
### "Performance investigation" workflow:
1. "Analyze performance metrics for https://mysite.com"
2. "Show me the largest resources loading"
3. "Check Core Web Vitals"
4. "Identify the main bottlenecks"
Just describe what you want to do in plain language, and I'll handle the browser automation for you!

193
skills/SKILL.md Normal file
View File

@@ -0,0 +1,193 @@
---
name: browser-devtools
description: Browser debugging and automation using Playwright. Use for web debugging, console monitoring, network analysis, screenshot capture, DOM inspection, and UI testing during development.
license: Apache-2.0
---
# Browser Devtools
Browser automation and debugging using Playwright for web development testing and analysis.
## Decision tree
Here the decison tree when user want to inspect the console log, apply the same pattern for other prompt.
```
User task → Is URL provided?
├─ Yes → Detect if it reachable.
│ ├─ Success → Implement Reconnaissance-then-action (RTA)
| | 1. Navigate to provided URL.
| | 2. Inspect console log
| | 3. Identify the errors
| | 4. Execute actions with discovered issues
│ └─ Fails → End
└─ No → Is the development server already running?
├─ No → End
└─ Yes → Ask user if they want to test development server?
├─ Yes → Reconnaissance-then-action (RTA)
├─ No → End
```
## Quick Start
Install dependencies:
```bash
cd plugins/browser-devtools/skills
./scripts/install.sh
```
Test installation:
```bash
node dist/scripts/navigate.js --url https://example.com
```
## Scripts
All scripts located in `scripts/` (compiled to `dist/scripts/`):
**Core Commands:**
- `navigate.js` - Navigate to URLs
- `screenshot.js` - Capture screenshots (full page or elements)
- `snapshot.js` - DOM inspection and element discovery
- `evaluate.js` - Execute JavaScript in browser context
- `click.js` - Element interaction
- `fill.js` - Form input testing
**Monitoring:**
- `console.js` - Console log monitoring and error tracking
- `network.js` - Network request analysis and performance debugging
- `performance.js` - Core Web Vitals and performance metrics
## Best Practices
- **Compliance Decision tree**: The decision tree must be followed strictly.
- **Wait for elements**: Use appropriate wait strategies for dynamic content
- **Clean up sessions**: Close sessions when done to free resources
## Usage
### Single Commands
```bash
# Screenshot
node dist/scripts/screenshot.js --url https://example.com --output page.png
# Performance analysis
node dist/scripts/performance.js --url https://example.com | jq '.vitals.LCP'
# Console monitoring (10 seconds)
node dist/scripts/console.js --url https://example.com --types error,warn --duration 10000
```
### Chained Commands (reuse session)
```bash
# Start session (keep browser open)
node dist/scripts/navigate.js --url https://example.com/login --close false
# Continue with same browser
node dist/scripts/fill.js --selector "#email" --value "user@example.com" --close false
node dist/scripts/click.js --selector "button[type=submit]"
```
### Common Debugging Patterns
```bash
# Find elements
node dist/scripts/snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}'
# Check for errors
node dist/scripts/console.js --url https://example.com --types error
# Monitor API calls
node dist/scripts/network.js --url https://example.com --types xhr,fetch --duration 5000
# Test forms
node dist/scripts/fill.js --url https://example.com --selector "#search" --value "query" --close false
node dist/scripts/click.js --selector "button[type=submit]"
```
## Options
All scripts support:
- `--headless false` - Show browser window (default: true)
- `--close false` - Keep browser open for chaining (default: true)
- `--timeout 30000` - Timeout in milliseconds
- `--browser chromium|firefox|webkit` - Browser engine (default: chromium)
## Output Format
**Success:**
```json
{
"success": true,
"url": "https://example.com",
"title": "Example Domain",
"sessionId": "session_1234567890_abc123",
"timestamp": "2024-01-01T12:00:00.000Z",
"...": "script-specific data"
}
```
**Error:**
```json
{
"success": false,
"error": "Element not found: .missing-element",
"stack": "Error: Element not found..."
}
```
## Selectors
```bash
# CSS selectors
node dist/scripts/click.js --selector "#main .button.primary"
# XPath
node dist/scripts/click.js --selector "xpath=//button[contains(text(), 'Submit')]"
# Text selectors
node dist/scripts/click.js --selector "text=Click me"
```
## Troubleshooting
**Browser not installed**: Run `npm run install-browsers`
**Element not found**: Use `snapshot.js` first to find correct selectors
**Script timeout**: Increase with `--timeout 60000`
**Permission denied**: Run `chmod +x scripts/install.sh`
**Debug mode**: Use `--headless false` to see browser actions
**Stale sessions**: Start new session with `--close true`
## Integration
**Pre-commit testing:**
```bash
node dist/scripts/navigate.js --url http://localhost:3000 && \
node dist/scripts/performance.js --url http://localhost:3000 | jq '.vitals.LCP'
```
**Performance regression:**
```bash
node dist/scripts/performance.js --url $APP_URL | jq '.vitals.LCP' | \
awk '{print $1 < 2500 ? "PASS" : "FAIL"}'
```
## References
- [Playwright Documentation](https://playwright.dev/)
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
- [Core Web Vitals](https://web.dev/vitals/)

37
skills/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "browser-devtools",
"version": "1.0.0",
"description": "Browser debugging and automation skill using Playwright for developers",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"install-browsers": "playwright install",
"test": "node dist/scripts/test.js"
},
"keywords": [
"browser",
"debugging",
"automation",
"playwright",
"development"
],
"author": "Claude Skills",
"license": "Apache-2.0",
"dependencies": {
"@playwright/test": "^1.40.0",
"playwright": "^1.40.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/node": "^20.8.0",
"@types/yargs": "^17.0.32",
"typescript": "^5.2.2"
},
"engines": {
"node": ">=18.0.0"
}
}

91
skills/scripts/click.ts Normal file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env node
/**
* Element interaction for testing UI behavior
* Usage: node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]
*/
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, waitForElement, getElementInfo } from '../lib/browser.js';
interface ClickArgs {
selector?: string;
url?: string;
'wait-for'?: string;
'wait-timeout'?: string;
headless?: string | boolean;
close?: string | boolean;
browser?: string;
}
async function click() {
const args = parseArgs(process.argv.slice(2)) as ClickArgs;
if (!args.selector) {
return outputError(new Error('--selector is required'));
return;
}
try {
const session = await getBrowserSession({
browser: args.browser as any,
headless: args.headless !== 'false',
});
// Navigate to URL if provided
if (args.url) {
await session.page.goto(args.url, { waitUntil: 'networkidle' });
}
// Wait for element to be available
const waitTimeout = args['wait-timeout'] ? parseInt(args['wait-timeout']) : 30000;
await waitForElement(session.page, args.selector, waitTimeout);
// Get element info before click
const elementInfo = await getElementInfo(session.page, args.selector);
// Check if element is visible and clickable
if (!elementInfo.visible) {
return outputError(new Error(`Element is not visible: ${args.selector}`));
return;
}
// Click the element
await session.page.click(args.selector);
// Wait for navigation or new content if specified
if (args['wait-for']) {
try {
await waitForElement(session.page, args['wait-for'], waitTimeout);
} catch (error) {
// Don't fail if wait-for element doesn't appear, just log it
console.warn(`Warning: Wait-for element not found: ${args['wait-for']}`);
}
}
const result = {
success: true,
url: session.page.url(),
title: await session.page.title(),
action: {
type: 'click',
selector: args.selector,
elementInfo: elementInfo,
waitFor: args['wait-for'] || null,
},
sessionId: session.sessionId,
timestamp: new Date().toISOString(),
};
outputJSON(result);
if (args.close !== 'false') {
await closeBrowserSession();
} else {
// Explicitly exit the process when keeping session open
process.exit(0);
}
} catch (error) {
return outputError(error instanceof Error ? error : new Error(String(error)));
}
}
click();

146
skills/scripts/console.ts Normal file
View File

@@ -0,0 +1,146 @@
#!/usr/bin/env node
/**
* Console log monitoring and error tracking
* Usage: node console.js [--url URL] [--types error,warn] [--duration 5000] [--no-navigation]
*
* Options:
* --url URL to navigate to (required if no existing browser session)
* --types Console message types to capture (comma-separated, default: all)
* --duration How long to monitor console (ms, default: 5000)
* --no-navigation Don't navigate, use existing browser session
*
* Examples:
* node console.js --url https://your-app.com --types error,warn
* node console.js --no-navigation true # Uses existing session
* node navigate.js --url https://your-app.com --close false
* node console.js --no-navigation true --types error # Monitor existing page
*/
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, getExistingSession } from '../lib/browser.js';
interface ConsoleArgs {
url?: string;
types?: string;
duration?: string;
timeout?: string;
headless?: string | boolean;
close?: string | boolean;
browser?: string;
'no-navigation'?: string | boolean;
}
interface ConsoleMessage {
type: string;
text: string;
location?: {
url: string;
lineNumber: number;
columnNumber: number;
};
timestamp: number;
}
async function monitorConsole() {
const args = parseArgs(process.argv.slice(2)) as ConsoleArgs;
// Smart URL handling
let shouldNavigate = args['no-navigation'] !== 'true';
let targetUrl = args.url;
// If no URL provided, try to use existing session
if (!targetUrl) {
const existingSession = await getExistingSession();
if (existingSession && existingSession.browser.isConnected()) {
// Use existing session, don't navigate
shouldNavigate = false;
targetUrl = existingSession.page.url();
} else {
// No existing session and no URL provided - require explicit URL from user
return outputError(new Error('No browser session found. Please provide a URL with --url or navigate first using navigate.js. For example: --url https://your-site.com'));
}
}
try {
const session = await getBrowserSession({
browser: args.browser as any,
headless: args.headless !== 'false',
timeout: args.timeout ? parseInt(args.timeout) : undefined,
});
const messages: ConsoleMessage[] = [];
const filterTypes = args.types ? args.types.split(',').map(t => t.trim().toLowerCase()) : null;
const duration = args.duration ? parseInt(args.duration) : 5000;
// Set up console message listener
session.page.on('console', (msg) => {
const messageType = msg.type().toLowerCase();
// Filter by message types if specified
if (filterTypes && !filterTypes.includes(messageType)) {
return;
}
const consoleMessage: ConsoleMessage = {
type: messageType,
text: msg.text(),
timestamp: Date.now(),
};
// Add location info if available
const location = msg.location();
if (location) {
consoleMessage.location = {
url: location.url,
lineNumber: location.lineNumber,
columnNumber: location.columnNumber,
};
}
messages.push(consoleMessage);
});
// Navigate to the URL if needed
if (shouldNavigate && targetUrl) {
await session.page.goto(targetUrl, { waitUntil: 'networkidle' });
}
// Wait for the specified duration
await new Promise(resolve => setTimeout(resolve, duration));
// Count messages by type
const messageCount = messages.reduce((acc, msg) => {
acc[msg.type] = (acc[msg.type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const result = {
success: true,
url: session.page.url(),
title: await session.page.title(),
monitoring: {
duration: duration,
types: filterTypes || 'all',
messageCount: messages.length,
messageCountByType: messageCount,
usedExistingSession: !args.url && !shouldNavigate,
autoNavigated: shouldNavigate && args.url,
},
messages: messages,
sessionId: session.sessionId,
timestamp: new Date().toISOString(),
};
outputJSON(result);
if (args.close !== 'false') {
await closeBrowserSession();
} else {
// Explicitly exit the process when keeping session open
process.exit(0);
}
} catch (error) {
return outputError(error instanceof Error ? error : new Error(String(error)));
}
}
monitorConsole();

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env node
/**
* Execute JavaScript for debugging and analysis
* Usage: node evaluate.js --url https://example.com --script "document.title"
*/
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, executeScript } from '../lib/browser.js';
interface EvaluateArgs {
url?: string;
script?: string;
timeout?: string;
'wait-until'?: string;
headless?: string | boolean;
close?: string | boolean;
browser?: string;
}
async function evaluate() {
const args = parseArgs(process.argv.slice(2)) as EvaluateArgs;
if (!args.script) {
return outputError(new Error('--script is required'));
return;
}
try {
const session = await getBrowserSession({
browser: args.browser as any,
headless: args.headless !== 'false',
timeout: args.timeout ? parseInt(args.timeout) : undefined,
});
// Navigate to URL if provided
if (args.url) {
await session.page.goto(args.url, {
waitUntil: args['wait-until'] as any || 'load',
timeout: args.timeout ? parseInt(args.timeout) : 30000,
});
}
// Execute the script
const result = await executeScript(session.page, args.script);
const responseData = {
success: true,
url: session.page.url(),
title: await session.page.title(),
script: args.script,
result: result,
sessionId: session.sessionId,
timestamp: new Date().toISOString(),
};
outputJSON(responseData);
if (args.close !== 'false') {
await closeBrowserSession();
} else {
// Explicitly exit the process when keeping session open
process.exit(0);
}
} catch (error) {
return outputError(error instanceof Error ? error : new Error(String(error)));
}
}
evaluate();

104
skills/scripts/fill.ts Normal file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env node
/**
* Form testing and input validation
* Usage: node fill.js --selector "#input" --value "text" [--url https://example.com] [--clear true]
*/
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, waitForElement, getElementInfo } from '../lib/browser.js';
interface FillArgs {
selector?: string;
value?: string;
url?: string;
clear?: string | boolean;
'wait-timeout'?: string;
headless?: string | boolean;
close?: string | boolean;
browser?: string;
}
async function fill() {
const args = parseArgs(process.argv.slice(2)) as FillArgs;
if (!args.selector) {
return outputError(new Error('--selector is required'));
return;
}
if (args.value === undefined) {
return outputError(new Error('--value is required'));
return;
}
try {
const session = await getBrowserSession({
browser: args.browser as any,
headless: args.headless !== 'false',
});
// Navigate to URL if provided
if (args.url) {
await session.page.goto(args.url, { waitUntil: 'networkidle' });
}
// Wait for element to be available
const waitTimeout = args['wait-timeout'] ? parseInt(args['wait-timeout']) : 30000;
await waitForElement(session.page, args.selector, waitTimeout);
// Get element info before filling
const elementInfo = await getElementInfo(session.page, args.selector);
// Check if element is visible and fillable
if (!elementInfo.visible) {
return outputError(new Error(`Element is not visible: ${args.selector}`));
return;
}
// Check if element is an input-like element
const fillableTags = ['input', 'textarea', 'select'];
if (!fillableTags.includes(elementInfo.tagName)) {
return outputError(new Error(`Element is not fillable: ${elementInfo.tagName} (${args.selector})`));
return;
}
// Clear the field if requested
if (args.clear === 'true') {
await session.page.fill(args.selector, '');
}
// Fill the element with the value
await session.page.fill(args.selector, args.value);
// Verify the value was set
const actualValue = await session.page.inputValue(args.selector);
const result = {
success: true,
url: session.page.url(),
title: await session.page.title(),
action: {
type: 'fill',
selector: args.selector,
elementInfo: elementInfo,
value: args.value,
actualValue: actualValue,
cleared: args.clear === 'true',
},
sessionId: session.sessionId,
timestamp: new Date().toISOString(),
};
outputJSON(result);
if (args.close !== 'false') {
await closeBrowserSession();
} else {
// Explicitly exit the process when keeping session open
process.exit(0);
}
} catch (error) {
return outputError(error instanceof Error ? error : new Error(String(error)));
}
}
fill();

155
skills/scripts/install.sh Executable file
View File

@@ -0,0 +1,155 @@
#!/bin/bash
# Installation script for browser-devtools skill
set -e
echo "🚀 Installing browser-devtools skill..."
# Check if Node.js is installed
if ! command -v node &>/dev/null; then
echo "❌ Node.js is not installed. Please install Node.js 18+ first."
exit 1
fi
# Check Node.js version
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
echo "❌ Node.js 18+ is required. Current version: $(node -v)"
exit 1
fi
echo "✅ Node.js $(node -v) detected"
# Check if npm dependencies are already installed
if [ ! -d "node_modules" ] || ! npm list playwright &>/dev/null; then
echo "📦 Installing npm dependencies..."
npm install
else
echo "✅ npm dependencies already installed"
fi
# Check if build is needed
BUILD_NEEDED=false
if [ ! -d "dist" ]; then
BUILD_NEEDED=true
else
# Check if any source files are newer than dist files
for ts_file in scripts/*.ts lib/*.ts; do
if [ -f "$ts_file" ]; then
js_file="dist/${ts_file%.*}.js"
if [ ! -f "$js_file" ] || [ "$ts_file" -nt "$js_file" ]; then
BUILD_NEEDED=true
break
fi
fi
done
fi
if [ "$BUILD_NEEDED" = true ]; then
echo "📦 Building TypeScript files..."
npm run build
else
echo "✅ Build files are up to date"
fi
# Check if Playwright browsers are installed
BROWSER_CHECK=$(node -e "try { require('playwright').chromium.executablePath(); console.log('OK'); } catch(e) { console.log('MISSING'); }" 2>/dev/null || echo "MISSING")
if [ "$BROWSER_CHECK" != "OK" ]; then
echo "🌐 Installing Playwright browsers..."
npx playwright install
else
echo "✅ Playwright browsers already installed"
fi
# Install system dependencies on Linux
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
echo "🔧 Installing system dependencies for Linux..."
# Detect distribution
if [ -f /etc/debian_version ]; then
# Debian/Ubuntu
echo "Detected Debian/Ubuntu-based system"
sudo apt-get update
sudo apt-get install -y \
libnss3 \
libnspr4 \
libasound2t64 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcups2 \
libdrm2 \
libxkbcommon0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1
elif [ -f /etc/redhat-release ]; then
# RHEL/Fedora/CentOS
echo "Detected RedHat/Fedora-based system"
if command -v dnf &>/dev/null; then
sudo dnf install -y \
nss \
nspr \
alsa-lib \
atk \
at-spi2-atk \
cups-libs \
libdrm \
libxkbcommon \
libXcomposite \
libXdamage \
libXfixes \
libXrandr \
mesa-libgbm
else
sudo yum install -y \
nss \
nspr \
alsa-lib \
atk \
at-spi2-atk \
cups-libs \
libdrm \
libxkbcommon \
libXcomposite \
libXdamage \
libXfixes \
libXrandr \
mesa-libgbm
fi
elif [ -f /etc/arch-release ]; then
# Arch Linux
echo "Detected Arch-based system"
sudo pacman -S --needed \
nss \
nspr \
alsa-lib \
atk \
at-spi2-atk \
cups \
libdrm \
libxkbcommon \
libxcomposite \
libxdamage \
libxfixes \
libxrandr \
mesa
else
echo "⚠️ Unknown Linux distribution. Please install browser dependencies manually."
echo "See: https://playwright.dev/docs/linux/"
fi
fi
# Create necessary directories
mkdir -p docs/screenshots
mkdir -p docs/traces
mkdir -p docs/reports
echo "✅ Installation completed successfully!"
echo ""
echo "🧪 Test the installation:"
echo " node dist/scripts/navigate.js --url https://example.com"
echo ""
echo "📚 For usage examples, see SKILL.md"

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* Navigate to a URL for debugging
* Usage: node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
*/
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError } from '../lib/browser.js';
interface NavigateArgs {
url?: string;
'wait-until'?: string;
timeout?: string;
headless?: string | boolean;
close?: string | boolean;
browser?: string;
}
async function navigate() {
const args = parseArgs(process.argv.slice(2)) as NavigateArgs;
if (!args.url) {
return outputError(new Error('--url is required'));
return;
}
try {
const session = await getBrowserSession({
browser: args.browser as any,
headless: args.headless !== 'false',
timeout: args.timeout ? parseInt(args.timeout) : undefined,
});
const waitUntil = args['wait-until'] as any || 'networkidle';
await session.page.goto(args.url, {
waitUntil,
timeout: args.timeout ? parseInt(args.timeout) : 30000,
});
const title = await session.page.title();
const finalUrl = session.page.url();
const result = {
success: true,
url: finalUrl,
title: title,
sessionId: session.sessionId,
timestamp: new Date().toISOString(),
};
outputJSON(result);
if (args.close !== 'false') {
await closeBrowserSession();
} else {
// Explicitly exit the process when keeping session open
process.exit(0);
}
} catch (error) {
return outputError(error instanceof Error ? error : new Error(String(error)));
}
}
navigate();

254
skills/scripts/network.ts Normal file
View File

@@ -0,0 +1,254 @@
#!/usr/bin/env node
/**
* Network request debugging and performance analysis
* Usage: node network.js --url https://example.com [--types xhr,fetch] [--duration 5000] [--output requests.json]
*/
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError } from '../lib/browser.js';
import { writeFile } from 'fs/promises';
interface NetworkArgs {
url?: string;
types?: string;
duration?: string;
output?: string;
timeout?: string;
headless?: string | boolean;
close?: string | boolean;
browser?: string;
}
interface NetworkRequest {
url: string;
method: string;
status: number;
statusText: string;
type: string;
resourceType: string;
size: {
request: number;
response: number;
total: number;
};
timing: {
startTime: number;
endTime: number;
duration: number;
};
headers: {
request: Record<string, string>;
response: Record<string, string>;
};
failed: boolean;
errorText?: string;
}
async function monitorNetwork() {
const args = parseArgs(process.argv.slice(2)) as NetworkArgs;
if (!args.url) {
return outputError(new Error('--url is required'));
return;
}
try {
const session = await getBrowserSession({
browser: args.browser as any,
headless: args.headless !== 'false',
timeout: args.timeout ? parseInt(args.timeout) : undefined,
});
const requests: NetworkRequest[] = [];
const filterTypes = args.types ? args.types.split(',').map(t => t.trim().toLowerCase()) : null;
const duration = args.duration ? parseInt(args.duration) : 5000;
// Set up request and response listeners
session.page.on('request', (request) => {
const resourceType = request.resourceType().toLowerCase();
// Filter by resource types if specified
if (filterTypes && !filterTypes.includes(resourceType)) {
return;
}
const requestData: any = {
url: request.url(),
method: request.method(),
resourceType: resourceType,
type: 'request',
headers: request.headers(),
size: {
request: 0, // Playwright doesn't expose header size directly
response: 0,
total: 0,
},
timing: {
startTime: Date.now(),
endTime: 0,
duration: 0,
},
status: 0,
statusText: '',
failed: false,
};
// Store temporary request data
(request as any)._requestData = requestData;
});
session.page.on('response', async (response) => {
const request = response.request() as any;
const requestData = request._requestData;
if (!requestData) {
return; // Skip if not tracked
}
const resourceType = response.request().resourceType().toLowerCase();
// Filter by resource types if specified
if (filterTypes && !filterTypes.includes(resourceType)) {
return;
}
const endTime = Date.now();
const responseBody = await response.text();
const responseSize = responseBody.length;
const networkRequest: NetworkRequest = {
url: response.url(),
method: request.method(),
status: response.status(),
statusText: response.statusText(),
type: 'response',
resourceType: resourceType,
size: {
request: requestData.size.request,
response: responseSize,
total: requestData.size.request + responseSize,
},
timing: {
startTime: requestData.timing.startTime,
endTime: endTime,
duration: endTime - requestData.timing.startTime,
},
headers: {
request: requestData.headers,
response: response.headers(),
},
failed: !response.ok(),
errorText: !response.ok() ? response.statusText() : undefined,
};
requests.push(networkRequest);
});
session.page.on('requestfailed', (request) => {
const requestData = (request as any)._requestData;
if (!requestData) {
return; // Skip if not tracked
}
const resourceType = request.resourceType().toLowerCase();
// Filter by resource types if specified
if (filterTypes && !filterTypes.includes(resourceType)) {
return;
}
const endTime = Date.now();
const networkRequest: NetworkRequest = {
url: request.url(),
method: request.method(),
status: 0,
statusText: 'Failed',
type: 'failed',
resourceType: resourceType,
size: requestData.size,
timing: {
startTime: requestData.timing.startTime,
endTime: endTime,
duration: endTime - requestData.timing.startTime,
},
headers: {
request: requestData.headers,
response: {},
},
failed: true,
errorText: (request as any).failure()?.errorText || 'Request failed',
};
requests.push(networkRequest);
});
// Navigate to the URL
await session.page.goto(args.url, { waitUntil: 'networkidle' });
// Wait for the specified duration
await new Promise(resolve => setTimeout(resolve, duration));
// Calculate summary statistics
const summary = {
totalRequests: requests.length,
successfulRequests: requests.filter(r => !r.failed).length,
failedRequests: requests.filter(r => r.failed).length,
totalSize: requests.reduce((sum, r) => sum + r.size.total, 0),
averageResponseTime: requests.length > 0
? requests.reduce((sum, r) => sum + r.timing.duration, 0) / requests.length
: 0,
requestsByType: requests.reduce((acc, r) => {
acc[r.resourceType] = (acc[r.resourceType] || 0) + 1;
return acc;
}, {} as Record<string, number>),
requestsByStatus: requests.reduce((acc, r) => {
if (r.failed) {
acc['failed'] = (acc['failed'] || 0) + 1;
} else {
const statusGroup = Math.floor(r.status / 100) * 100;
acc[statusGroup] = (acc[statusGroup] || 0) + 1;
}
return acc;
}, {} as Record<string, number>),
};
const result = {
success: true,
url: session.page.url(),
title: await session.page.title(),
monitoring: {
duration: duration,
types: filterTypes || 'all',
...summary,
},
requests: requests,
sessionId: session.sessionId,
timestamp: new Date().toISOString(),
};
// Save to file if output path provided
if (args.output) {
await writeFile(args.output, JSON.stringify(result, null, 2));
outputJSON({
success: true,
output: args.output,
...summary,
url: session.page.url()
});
} else {
outputJSON(result);
}
if (args.close !== 'false') {
await closeBrowserSession();
} else {
// Explicitly exit the process when keeping session open
process.exit(0);
}
} catch (error) {
return outputError(error instanceof Error ? error : new Error(String(error)));
}
}
monitorNetwork();

View File

@@ -0,0 +1,231 @@
#!/usr/bin/env node
/**
* Performance metrics and Core Web Vitals
* Usage: node performance.js --url https://example.com [--trace trace.json] [--resources true]
*/
import {
getBrowserSession,
closeBrowserSession,
parseArgs,
outputJSON,
outputError,
} from "../lib/browser.js";
import { writeFile } from "fs/promises";
interface PerformanceArgs {
url?: string;
trace?: string;
resources?: string | boolean;
timeout?: string;
headless?: string | boolean;
close?: string | boolean;
browser?: string;
}
interface CoreWebVitals {
LCP: number | null; // Largest Contentful Paint
FID: number | null; // First Input Delay
CLS: number; // Cumulative Layout Shift
FCP: number | null; // First Contentful Paint
TTFB: number | null; // Time to First Byte
}
interface PerformanceResource {
name: string;
type: string;
duration: number;
size: number;
startTime: number;
responseEnd: number;
}
async function measurePerformance() {
const args = parseArgs(process.argv.slice(2)) as PerformanceArgs;
if (!args.url) {
return outputError(new Error("--url is required"));
}
try {
const session = await getBrowserSession({
browser: args.browser as any,
headless: args.headless !== "false",
timeout: args.timeout ? parseInt(args.timeout) : undefined,
});
// Start tracing if requested
if (args.trace) {
await session.page.context().tracing.start({
screenshots: true,
snapshots: true,
sources: true,
});
}
// Navigate to the URL
await session.page.goto(args.url, { waitUntil: "networkidle" });
// Stop tracing and save if requested
if (args.trace) {
await session.page.context().tracing.stop({ path: args.trace });
}
// Get performance metrics from Chrome DevTools Protocol
const client = await session.page.context().newCDPSession(session.page);
const metrics = await client.send("Performance.getMetrics");
// Get Core Web Vitals
const vitals = await session.page.evaluate(() => {
return new Promise((resolve) => {
const vitals: CoreWebVitals = {
LCP: null,
FID: null,
CLS: 0,
FCP: null,
TTFB: null,
};
// LCP - Largest Contentful Paint
try {
new PerformanceObserver((list) => {
const entries = list.getEntries();
if (entries.length > 0) {
const lastEntry = entries[entries.length - 1] as any;
vitals.LCP = lastEntry.renderTime || lastEntry.loadTime;
}
}).observe({
entryTypes: ["largest-contentful-paint"],
buffered: true,
});
} catch (e) {
// LCP not supported
}
// CLS - Cumulative Layout Shift
try {
new PerformanceObserver((list) => {
list.getEntries().forEach((entry: any) => {
if (!entry.hadRecentInput) {
vitals.CLS += entry.value;
}
});
}).observe({ entryTypes: ["layout-shift"], buffered: true });
} catch (e) {
// CLS not supported
}
// FCP - First Contentful Paint
try {
const paintEntries = performance.getEntriesByType("paint");
const fcpEntry = paintEntries.find(
(e: any) => e.name === "first-contentful-paint",
);
if (fcpEntry) {
vitals.FCP = fcpEntry.startTime;
}
} catch (e) {
// FCP not supported
}
// TTFB - Time to First Byte
try {
const [navigationEntry] = performance.getEntriesByType(
"navigation",
) as any[];
if (navigationEntry) {
vitals.TTFB =
navigationEntry.responseStart - navigationEntry.requestStart;
}
} catch (e) {
// Navigation timing not supported
}
// Wait a bit for metrics to stabilize
setTimeout(() => resolve(vitals), 1000);
});
});
// Get resource timing information
let resources: PerformanceResource[] = [];
if (args.resources === "true") {
resources = await session.page.evaluate(() => {
return performance.getEntriesByType("resource").map((r: any) => ({
name: r.name,
type: r.initiatorType,
duration: r.duration,
size: r.transferSize || 0,
startTime: r.startTime,
responseEnd: r.responseEnd,
}));
});
}
const resourceSummary = {
count: resources.length,
totalDuration: resources.reduce((sum, r) => sum + r.duration, 0),
totalSize: resources.reduce((sum, r) => sum + r.size, 0),
averageDuration:
resources.length > 0
? resources.reduce((sum, r) => sum + r.duration, 0) / resources.length
: 0,
resourcesByType: resources.reduce(
(acc, r) => {
acc[r.type] = (acc[r.type] || 0) + 1;
return acc;
},
{} as Record<string, number>,
),
};
// Process metrics into a more usable format
const processedMetrics: Record<string, any> = {};
metrics.metrics?.forEach((metric: any) => {
processedMetrics[metric.name] = metric.value;
});
const result = {
success: true,
url: session.page.url(),
title: await session.page.title(),
metrics: {
...processedMetrics,
JSHeapUsedSizeMB: (
(processedMetrics.JSHeapUsedSize || 0) /
1024 /
1024
).toFixed(2),
JSHeapTotalSizeMB: (
(processedMetrics.JSHeapTotalSize || 0) /
1024 /
1024
).toFixed(2),
},
vitals: vitals,
resources:
args.resources === "true"
? {
...resourceSummary,
items: resources,
}
: resourceSummary,
trace: args.trace || null,
sessionId: session.sessionId,
timestamp: new Date().toISOString(),
};
outputJSON(result);
if (args.close !== "false") {
await closeBrowserSession();
} else {
// Explicitly exit the process when keeping session open
process.exit(0);
}
} catch (error) {
return outputError(error instanceof Error ? error : new Error(String(error)));
}
}
measurePerformance();

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env node
/**
* Capture screenshots for visual debugging
* Usage: node screenshot.js --url https://example.com --output screenshot.png [--full-page true] [--selector .element]
*/
import {
getBrowserSession,
closeBrowserSession,
parseArgs,
outputJSON,
outputError,
createScreenshot,
getOutputDirectory,
} from "../lib/browser.js";
import { writeFile } from "fs/promises";
import { join } from "path";
interface ScreenshotArgs {
url?: string;
output?: string;
"full-page"?: string | boolean;
selector?: string;
format?: string;
quality?: string;
"wait-until"?: string;
headless?: string | boolean;
close?: string | boolean;
browser?: string;
}
async function takeScreenshot() {
const args = parseArgs(process.argv.slice(2)) as ScreenshotArgs;
if (!args.url && !args.selector) {
return outputError(new Error("Either --url or --selector is required"));
}
// --output is optional - will auto-generate if not provided
try {
const session = await getBrowserSession({
browser: args.browser as any,
headless: args.headless !== "false",
});
// Navigate to URL if provided
if (args.url) {
await session.page.goto(args.url, {
waitUntil: (args["wait-until"] as any) || "load",
});
}
const outputPath =
args.output || join(getOutputDirectory(), `screenshot_${Date.now()}.png`);
const fullPage = args["full-page"] === "true";
// Create screenshot
const result = await createScreenshot(session.page, outputPath, {
fullPage,
selector: args.selector,
});
const screenshotData = {
success: true,
url: session.page.url(),
title: await session.page.title(),
outputPath: typeof result === "string" ? result : outputPath,
fullPage,
selector: args.selector || null,
size: typeof result === "string" ? null : result.length,
sessionId: session.sessionId,
timestamp: new Date().toISOString(),
};
outputJSON(screenshotData);
if (args.close !== "false") {
await closeBrowserSession();
} else {
// Explicitly exit the process when keeping session open
process.exit(0);
}
} catch (error) {
return outputError(
error instanceof Error ? error : new Error(String(error)),
);
}
}
takeScreenshot();

172
skills/scripts/snapshot.ts Normal file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env node
/**
* DOM inspection and element discovery
* Usage: node snapshot.js --url https://example.com [--output snapshot.json]
*/
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, getOutputDirectory } from '../lib/browser.js';
import { writeFile } from 'fs/promises';
import { join } from 'path';
interface SnapshotArgs {
url?: string;
output?: string;
timeout?: string;
headless?: string | boolean;
close?: string | boolean;
browser?: string;
}
interface ElementInfo {
index: number;
tagName: string;
id: string | null;
className: string | null;
name: string | null;
value: string | null;
type: string | null;
text: string | null;
href: string | null;
selector: string;
xpath: string;
visible: boolean;
position: {
x: number;
y: number;
width: number;
height: number;
};
}
async function takeSnapshot() {
const args = parseArgs(process.argv.slice(2)) as SnapshotArgs;
try {
const session = await getBrowserSession({
browser: args.browser as any,
headless: args.headless !== 'false',
timeout: args.timeout ? parseInt(args.timeout) : undefined,
});
// Navigate if URL provided
if (args.url) {
await session.page.goto(args.url, {
waitUntil: 'networkidle',
timeout: args.timeout ? parseInt(args.timeout) : 30000,
});
}
// Get interactive elements with metadata
const elements = await session.page.evaluate(() => {
const interactiveSelectors = [
'a[href]',
'button',
'input',
'textarea',
'select',
'[onclick]',
'[role="button"]',
'[role="link"]',
'[contenteditable]',
'[tabindex]'
];
const elements: ElementInfo[] = [];
const selector = interactiveSelectors.join(', ');
const nodes = document.querySelectorAll(selector);
nodes.forEach((el, index) => {
const rect = el.getBoundingClientRect();
const element = el as HTMLElement;
// Generate unique selector
let uniqueSelector = '';
if (element.id) {
uniqueSelector = `#${element.id}`;
} else if (element.className) {
const classes = Array.from(element.classList).join('.');
uniqueSelector = `${element.tagName.toLowerCase()}.${classes}`;
} else {
uniqueSelector = element.tagName.toLowerCase();
}
elements.push({
index: index,
tagName: element.tagName.toLowerCase(),
id: element.id || null,
className: element.className || null,
name: (element as any).name || null,
value: (element as any).value || null,
type: (element as any).type || null,
text: element.textContent?.trim().substring(0, 100) || null,
href: (element as HTMLAnchorElement).href || null,
selector: uniqueSelector,
xpath: getXPath(element),
visible: rect.width > 0 && rect.height > 0,
position: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
}
});
});
function getXPath(element: Element): string {
if (element.id) {
return `//*[@id="${element.id}"]`;
}
if (element === document.body) {
return '/html/body';
}
let ix = 0;
const parent = element.parentNode as Element;
const siblings = parent?.children || [];
for (let i = 0; i < siblings.length; i++) {
const sibling = siblings[i];
if (sibling === element) {
return getXPath(parent) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
}
if (sibling.tagName === element.tagName) {
ix++;
}
}
return '';
}
return elements;
});
const result = {
success: true,
url: session.page.url(),
title: await session.page.title(),
elementCount: elements.length,
elements: elements,
sessionId: session.sessionId,
timestamp: new Date().toISOString(),
};
// Save to file if output path provided, otherwise save to project directory
const outputPath = args.output || join(getOutputDirectory(), `snapshot_${Date.now()}.json`);
await writeFile(outputPath, JSON.stringify(result, null, 2));
outputJSON({
success: true,
output: outputPath,
elementCount: elements.length,
url: session.page.url()
});
if (args.close !== 'false') {
await closeBrowserSession();
} else {
// Explicitly exit the process when keeping session open
process.exit(0);
}
} catch (error) {
return outputError(error instanceof Error ? error : new Error(String(error)));
}
}
takeSnapshot();

31
skills/tsconfig.json Normal file
View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"resolveJsonModule": true,
"lib": ["ES2018", "DOM", "DOM.Iterable"],
"types": ["node"]
},
"include": [
"scripts/**/*",
"lib/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts"
]
}