browser-script · git:20260401.7c1eb70 · 2026-04-01 · sha256 1443ab95e61b1d77

browser-script git:20260401.7c1eb70A

Immutable. This exact content is served forever at /api/v1/blob/1443ab95e61b1d77.

---
name: browser-script
description: "Create a browser automation script that bundles multi-step browser interactions into a single browser_run_code call. Use when: 'automate browser flow', 'create browser script', 'script this browser task', 'bundle browser steps', 'reduce browser tokens'."
user_invocable: true
---

# Browser Script Builder

Create self-contained JS scripts that run inside platform browser agents (linkedin-agent, reddit-agent, twitter-agent) via `browser_run_code`. Replaces 5-10 Claude browser tool calls with 2 calls (set params + run script), cutting token usage 10-20x.

## Usage

```
/browser-script <platform> <action-description>
/browser-script linkedin "edit a comment to append link text"
/browser-script reddit "post a reply to a comment"
/browser-script linkedin "extract post data from search results"
```

## Workflow

### Step 1: Understand the browser flow

Before writing code, manually walk through the flow in the target browser agent to discover:
- What elements exist (use `browser_snapshot`)
- What role/name locators work (use `getByRole`)
- What API calls the page makes (use `browser_network_requests`)
- What timing/scrolling is needed

### Step 2: Write the script

Create `scripts/<action>_<platform>.js` following this skeleton:

```javascript
// action_platform.js — One-line description
// Runs inside {platform}-agent browser via browser_run_code.
//
// Params via window.__params:
//   { field1: "...", field2: "..." }
//
// Returns: { ok: true, ...data } or { ok: false, error: "error_code" }

async (page) => {
  const params = await page.evaluate(() => window.__params);
  if (!params || !params.requiredField) {
    return JSON.stringify({ ok: false, error: 'missing_params' });
  }

  try {
    // Step 1: Navigate
    await page.goto(params.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
    await page.waitForTimeout(3000);

    // Step 2: Scroll to load content
    await page.evaluate(() => window.scrollBy(0, 1500));
    await page.waitForTimeout(2000);

    // Step 3: Find element (with retry after scroll)
    const element = page.getByRole('button', { name: /pattern/i }).first();
    try {
      await element.waitFor({ timeout: 10000 });
    } catch {
      await page.evaluate(() => window.scrollBy(0, 2000));
      await page.waitForTimeout(2000);
      try {
        await element.waitFor({ timeout: 5000 });
      } catch {
        return JSON.stringify({ ok: false, error: 'element_not_found' });
      }
    }

    // Step 4: Interact
    await element.click();
    await page.waitForTimeout(1000);

    // Step 5: Verify
    return JSON.stringify({ ok: true, result: 'done' });

  } catch (e) {
    return JSON.stringify({ ok: false, error: e.message });
  }
}
```

### Step 3: Test interactively

Test in two calls:

```
// Set params
mcp__{platform}-agent__browser_run_code code:
  async (page) => {
    await page.evaluate(() => {
      window.__params = { url: "...", text: "..." };
    });
  }

// Run script
mcp__{platform}-agent__browser_run_code filename:
  ~/social-autoposter/scripts/the_script.js
```

If it fails, take a `browser_snapshot` to debug, fix the script, re-test.

### Step 4: Update the shell script

Replace the Claude prompt instructions for this flow with the 2-call pattern. Example from engage.sh Phase D LinkedIn:

```bash
# Old: 8-line prompt telling Claude to navigate, find comment, click menu, edit, save
# New:
8. For LinkedIn: use the edit script via linkedin-agent browser:
   a. Set params: browser_run_code with code setting window.__params
   b. Run: browser_run_code with filename=$REPO_DIR/scripts/edit_linkedin_comment.js
   c. Parse JSON result: {ok:true} = success, {ok:false, error} = handle error
```

## Design Rules (MUST follow)

1. **Params via `window.__params`** — never hardcode. Script must be reusable across different targets.

2. **Return JSON always** — `{ok: true, ...data}` or `{ok: false, error: "code"}`. Error codes are specific and actionable: `comment_not_found`, `link_already_present`, `not_logged_in`, `save_failed`, `element_not_found`.

3. **Script owns all waits** — `waitForTimeout`, `waitFor`, scroll-retry loops. Caller just runs and reads result.

4. **`keyboard.type()` not `fill()`** — React-based sites (LinkedIn, possibly Twitter) don't detect `fill()` changes. Always `keyboard.type()` for rich text editors. For full text replacement: `click({clickCount: 3})` + `Meta+a` + `keyboard.type(newText)`.

5. **Scroll before find** — social platforms lazy-load. Always scroll into view before locating elements.

6. **Locate by role/name** — `page.getByRole('button', {name: /pattern/i})` not CSS class selectors. Survives redesigns.

7. **One script = one complete action** — "edit a comment", "scan notifications", "post a reply". Not a single click. Not an entire engagement loop.

8. **Safety checks inside the script** — dedup detection (link already present, already replied). Don't rely on the caller.

9. **Generous timeouts** — social sites are slow. Use 3s after navigation, 2s after scroll, 1s after click.

10. **Two-phase element finding** — first try with 10s timeout, then scroll more and retry with 5s. Return specific error if still not found.

## When NOT to Script

- **Content generation** — Claude needs to decide what to write. Keep that in prompts.
- **Project/topic matching** — Claude picks which project fits. Keep in prompts.
- **One-off investigations** — manual browser_snapshot is fine.
- **Flows that vary** — if the steps change based on page state in unpredictable ways.

The split: **Claude decides WHAT** (pick project, write text, choose targets). **Scripts execute HOW** (navigate, click, type, save).

## Platform Notes

### LinkedIn
- Rich text editor uses React — `fill()` won't trigger change detection
- Edit textbox is the LAST `textbox[name="Text editor for creating comment"]` after clicking Edit
- Our comment menu: `button[name=/View more options for Matthew/i]`
- Official API cannot read or edit comments
- Internal Voyager API accessible via in-page `fetch()` for read-only operations (notifications)
- Agent: `mcp__linkedin-agent__browser_run_code`

### Reddit
- old.reddit.com is simpler to automate than new Reddit
- Edit button visible on own comments without menu
- Agent: `mcp__reddit-agent__browser_run_code`

### Twitter/X
- Tweets cannot be edited
- Agent: `mcp__twitter-agent__browser_run_code`

## Existing Scripts

| Script | Platform | Action | Params |
|--------|----------|--------|--------|
| `scan_linkedin_notifications.js` | LinkedIn | Read notifications via internal API | none (uses cookies) |
| `edit_linkedin_comment.js` | LinkedIn | Append text to our comment | `{postUrl, appendText}` |

## Reference

Full design doc: `scripts/BROWSER_SCRIPTS.md`