AgentStack
SKILL verified MIT Self-run

Browser Automation

skill-kinhluan-skills-browser-automation · by kinhluan

Browser automation, web scraping, Chrome Extension development, and UI automation with Playwright/CDP. Use when building Chrome extensions, automating web workflows, scraping data, filling forms, or integrating web content with agent skills.

No reviews yet
0 installs
9 views
0.0% view→install

Install

$ agentstack add skill-kinhluan-skills-browser-automation

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Browser Automation? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Browser Automation & Chrome Extension Skill

Build browser automation workflows, Chrome Extensions, and web scraping pipelines that integrate with the kinhluan/skills system.

When to Use

  • Building a Chrome Extension or browser extension
  • Automating web workflows (form filling, clicking, navigation)
  • Scraping data from websites
  • Extracting and summarizing web page content
  • Connecting browser actions to agent skills
  • Using Playwright, Puppeteer, or Chrome DevTools Protocol (CDP)
  • "Tự động hóa trình duyệt", "mở rộng Chrome", "cào dữ liệu web"

Chrome Extension Architecture (Manifest V3)

Core Components

| Component | Runs In | Purpose | |-----------|---------|---------| | Popup | User click | UI panel — skill browser, search, settings | | Content Script | Page context | DOM access — extract data, inject automation | | Service Worker | Background | Event handling — fetch skills, caching, messaging | | Options Page | Tab | Configuration — API keys, preferences |

Minimal Manifest V3

{
  "manifest_version": 3,
  "name": "KinhLuan Skills Browser",
  "version": "1.0.0",
  "permissions": ["activeTab", "storage", "scripting"],
  "host_permissions": ["https://raw.githubusercontent.com/*"],
  "action": {
    "default_popup": "popup.html",
    "default_icon": { "16": "icon16.png", "48": "icon48.png", "128": "icon128.png" }
  },
  "background": { "service_worker": "background.js" },
  "content_scripts": [
    {
      "matches": [""],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ]
}

Permission Minimization Rule

> Start with least privilege. Only add permissions when a feature genuinely needs them.

| Feature | Minimum Permission | |---------|-------------------| | Read current page | activeTab | | Read any page | host_permissions (specific domains) | | Store settings | storage | | Inject scripts | scripting | | Cross-origin fetch | host_permissions (target origins) |


Content Extraction Strategies

1. Readability-Style Article Extraction

// content.ts — Extract main article content
function extractArticle(): { title: string; content: string; url: string } {
  const title = document.title;
  const url = location.href;

  // Heuristic: find the largest text block
  const candidates = Array.from(document.querySelectorAll('article, main, [role="main"], .content, .post, .entry'));
  let best = candidates[0];
  if (!best) {
    // Fallback: score paragraphs by text density
    const paragraphs = Array.from(document.querySelectorAll('p'));
    const scored = paragraphs.map(p => ({
      el: p.parentElement!,
      score: p.innerText.length + (p.querySelectorAll('p').length * 20)
    }));
    scored.sort((a, b) => b.score - a.score);
    best = scored[0]?.el || document.body;
  }

  return { title, content: best.innerText.trim(), url };
}

// Send to background/service worker
chrome.runtime.sendMessage({ type: 'EXTRACTED_CONTENT', payload: extractArticle() });

2. Structured Data Extraction (JSON-LD / Microdata)

function extractStructuredData(): Record[] {
  const jsonLd = Array.from(document.querySelectorAll('script[type="application/ld+json"]'))
    .map(s => {
      try { return JSON.parse(s.textContent || '{}'); } catch { return null; }
    })
    .filter(Boolean);

  const microdata = Array.from(document.querySelectorAll('[itemscope]'))
    .map(el => {
      const props: Record = {};
      el.querySelectorAll('[itemprop]').forEach(p => {
        props[p.getAttribute('itemprop')!] = p.textContent || '';
      });
      return { type: el.getAttribute('itemtype'), ...props };
    });

  return [...jsonLd, ...microdata];
}

3. Table & List Scraping

function extractTables(): Array {
  return Array.from(document.querySelectorAll('table')).map(table => {
    const headers = Array.from(table.querySelectorAll('th')).map(th => th.innerText.trim());
    const rows = Array.from(table.querySelectorAll('tr')).slice(headers.length ? 0 : 1).map(tr =>
      Array.from(tr.querySelectorAll('td')).map(td => td.innerText.trim())
    ).filter(r => r.length > 0);
    return { headers: headers.length ? headers : rows.shift() || [], rows };
  });
}

UI Automation Patterns

Playwright Integration (Local Agent → Browser)

// Agent-side: Control browser via Playwright
import { chromium, Page } from 'playwright';

async function runAutomation(skillPrompt: string, url: string) {
  const browser = await chromium.launch({ headless: false });
  const page = await browser.newPage();
  await page.goto(url);

  // Parse skill instructions into actions
  const actions = parseSkillActions(skillPrompt); // [{ type: 'click', selector: '...' }, ...]

  for (const action of actions) {
    switch (action.type) {
      case 'click':
        await page.click(action.selector);
        break;
      case 'type':
        await page.fill(action.selector, action.value);
        break;
      case 'wait':
        await page.waitForSelector(action.selector, { timeout: action.timeout || 5000 });
        break;
      case 'scroll':
        await page.evaluate(() => window.scrollBy(0, window.innerHeight));
        break;
      case 'extract':
        return await page.evaluate(extractArticle);
    }
  }

  await browser.close();
}

Chrome DevTools Protocol (CDP) — Direct Browser Control

// Connect to existing Chrome instance via CDP
import CDP from 'chrome-remote-interface';

async function cdpExtract(url: string) {
  const client = await CDP({ port: 9222 });
  const { Page, Runtime } = client;

  await Page.enable();
  await Page.navigate({ url });
  await Page.loadEventFired();

  // Execute extraction function in browser context
  const { result } = await Runtime.evaluate({
    expression: `(${extractArticle.toString()})()`,
    returnByValue: true
  });

  await client.close();
  return result.value;
}

Element Selection Strategy

| Strategy | Selector | Best For | |----------|----------|----------| | Test ID | [data-testid="submit-btn"] | Stable, semantic | | ARIA | [aria-label="Search"] | Accessible, reliable | | Role | [role="button"] | Semantic markup | | Text | text="Submit" | User-visible labels | | CSS | .btn-primary | Visual styling | | XPath | //button[contains(.,"Submit")] | Complex matching |

> Priority: data-testid > aria-label > role > text > CSS class > XPath


Form Filling Patterns

Auto-Detect & Fill

interface FieldMapping {
  label: string;
  selector: string;
  type: 'text' | 'email' | 'select' | 'checkbox' | 'radio';
  value: string;
}

function detectFormFields(): FieldMapping[] {
  const fields: FieldMapping[] = [];

  document.querySelectorAll('input, select, textarea').forEach(el => {
    const input = el as HTMLInputElement;
    const label = findLabel(input);
    const type = input.type === 'select-one' ? 'select' : (input.type as FieldMapping['type']);
    fields.push({ label, selector: getSelector(input), type, value: '' });
  });

  return fields;
}

function findLabel(input: HTMLElement): string {
  const id = input.id;
  if (id) {
    const label = document.querySelector(`label[for="${id}"]`);
    if (label) return label.textContent?.trim() || '';
  }
  const parent = input.closest('label');
  if (parent) return parent.textContent?.trim() || '';
  return input.placeholder || input.name || input.getAttribute('aria-label') || '';
}

function getSelector(el: HTMLElement): string {
  if (el.id) return `#${el.id}`;
  if (el.getAttribute('data-testid')) return `[data-testid="${el.getAttribute('data-testid')}"]`;
  const tag = el.tagName.toLowerCase();
  const classes = Array.from(el.classList).join('.');
  return classes ? `${tag}.${classes}` : tag;
}

Batch Fill from Skill Data

async function fillForm(data: Record) {
  const fields = detectFormFields();

  for (const [key, value] of Object.entries(data)) {
    const field = fields.find(f =>
      f.label.toLowerCase().includes(key.toLowerCase()) ||
      f.selector.includes(key.toLowerCase())
    );
    if (!field) continue;

    const el = document.querySelector(field.selector) as HTMLInputElement;
    if (!el) continue;

    if (field.type === 'select') {
      const option = Array.from(el.querySelectorAll('option')).find(o =>
        o.textContent?.toLowerCase().includes(value.toLowerCase())
      );
      if (option) (el as HTMLSelectElement).value = option.value;
    } else if (field.type === 'checkbox') {
      el.checked = value.toLowerCase() === 'true' || value === 'yes';
    } else {
      el.value = value;
      el.dispatchEvent(new Event('input', { bubbles: true }));
      el.dispatchEvent(new Event('change', { bubbles: true }));
    }
  }
}

Skill Routing from Web Content

Intent Matching on Extracted Content

// Lightweight client-side routing (mirrors router skill)
const WEB_ROUTES = [
  { skill: 'sota-survey', keywords: ['survey', 'literature', 'review', 'papers', 'state of the art', 'related work'] },
  { skill: 'research-question', keywords: ['research question', 'hypothesis', 'contribution', 'novelty'] },
  { skill: 'c4-model', keywords: ['architecture', 'system diagram', 'c4 model', 'container', 'component'] },
  { skill: 'ddd-core', keywords: ['domain driven', 'bounded context', 'ubiquitous language', 'ddd'] },
  { skill: 'python-development', keywords: ['python', 'fastapi', 'pydantic', 'django', 'flask'] },
  { skill: 'javascript-typescript', keywords: ['javascript', 'typescript', 'react', 'node.js', 'frontend'] },
  { skill: 'security-analysis', keywords: ['security', 'vulnerability', 'owasp', 'pentest', 'xss', 'sql injection'] },
  { skill: 'docker-containerization', keywords: ['docker', 'container', 'dockerfile', 'docker-compose'] },
  { skill: 'kubernetes-orchestration', keywords: ['kubernetes', 'k8s', 'pod', 'deployment', 'helm'] },
  { skill: 'business-product-leadership', keywords: ['product strategy', 'jtbd', 'mvp', 'product market'] },
];

function routeFromContent(text: string): string[] {
  const lower = text.toLowerCase();
  const matched = new Set();
  for (const route of WEB_ROUTES) {
    if (route.keywords.some(kw => lower.includes(kw))) matched.add(route.skill);
  }
  return Array.from(matched).slice(0, 3);
}

Full Workflow: Page → Skill → Agent

1. User clicks extension on any page
2. Content script extracts article text
3. Extension runs routeFromContent(text)
4. Suggests top 3 matching kinhluan skills
5. User selects a skill
6. Extension fetches SKILL.md from raw GitHub
7. Formats prompt: [SKILL instructions] + [page context]
8. Copies to clipboard OR opens agent chat with pre-filled prompt

Security & Privacy

CSP Compliance

{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' https://raw.githubusercontent.com"
  }
}

CORS Handling

// Fetch skills.json from raw GitHub (CORS-enabled)
const SKILLS_JSON_URL = 'https://raw.githubusercontent.com/kinhluan/skills/main/skills.json';

async function fetchSkills(): Promise {
  const res = await fetch(SKILLS_JSON_URL);
  if (!res.ok) throw new Error(`Failed to fetch skills: ${res.status}`);
  return res.json();
}

// Cache in chrome.storage
async function cacheSkills(skills: SkillsManifest) {
  await chrome.storage.local.set({ skillsCache: skills, skillsCacheTime: Date.now() });
}

Data Sanitization

function sanitizeForPrompt(text: string, maxLength = 8000): string {
  return text
    .replace(/[<>]/g, '')           // Strip HTML-like chars
    .replace(/\s+/g, ' ')           // Collapse whitespace
    .trim()
    .slice(0, maxLength);
}

Permission Checklist

  • [ ] Use activeTab instead of broad host permissions where possible
  • [ ] Never inject scripts from remote URLs
  • [ ] Sanitize all DOM-extracted data before sending to APIs
  • [ ] Store sensitive data in chrome.storage.local (encrypted at rest by OS)
  • [ ] Declare minimum permissions in manifest
  • [ ] Use chrome.scripting.executeScript with function injection (not string eval)

Integration with kinhluan/skills

Fetch Skill from GitHub Raw

const RAW_BASE = 'https://raw.githubusercontent.com/kinhluan/skills/main/.agent-skills';

async function fetchSkill(skillName: string): Promise {
  const [mdRes, toonRes] = await Promise.all([
    fetch(`${RAW_BASE}/${skillName}/SKILL.md`),
    fetch(`${RAW_BASE}/${skillName}/SKILL.toon`).catch(() => null)
  ]);

  const md = await mdRes.text();
  const toon = toonRes ? await toonRes.text() : '';
  return { md, toon };
}

Parse SKILL.md Frontmatter

interface SkillFrontmatter {
  name: string;
  description: string;
  metadata: { tags: string[]; triggers?: string[]; version?: string };
}

function parseFrontmatter(md: string): { frontmatter: SkillFrontmatter; body: string } {
  const match = md.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
  if (!match) throw new Error('Invalid SKILL.md: missing frontmatter');

  // Simple YAML parser (sufficient for skill frontmatter)
  const yaml = match[1];
  const body = match[2].trim();

  const frontmatter: any = {};
  let currentKey = '';
  for (const line of yaml.split('\n')) {
    const keyMatch = line.match(/^(\w+):\s*(.*)$/);
    if (keyMatch) {
      currentKey = keyMatch[1];
      frontmatter[currentKey] = keyMatch[2].replace(/^["']|["']$/g, '');
    } else if (line.trim().startsWith('- ')) {
      const val = line.trim().slice(2).replace(/^["']|["']$/g, '');
      if (!frontmatter[currentKey]) frontmatter[currentKey] = [];
      (frontmatter[currentKey] as string[]).push(val);
    }
  }

  return { frontmatter, body };
}

Quick Reference

Chrome Extension Message Passing

// Content → Background
chrome.runtime.sendMessage({ type: 'ACTION', payload: data });

// Background → Content (specific tab)
chrome.tabs.sendMessage(tabId, { type: 'ACTION', payload: data });

// Background → Popup (via storage or direct messaging)
chrome.storage.local.set({ key: value });

Playwright Quick Start

npm init -y
npm install playwright
npx playwright install chromium
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
const text = await page.innerText('article');
await browser.close();

CDP Quick Start

# Launch Chrome with remote debugging
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug
import CDP from 'chrome-remote-interface';
const client = await CDP({ port: 9222 });

Resources

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.