Commit Graph

613 Commits

Author SHA1 Message Date
Tommy D. Rossi a17af39616 release: playwriter@0.4.0
Cloud browser sessions, headless browser mode, API key auth,
multi-profile relay routing fix, cloud billing race fix, cloud login fix.

Fixes #80
2026-06-25 14:04:03 +02:00
Tommy D. Rossi 0671191a2d add headless browser mode and Chrome install command
Add `playwriter browser install` to download Chrome for Testing from
Google into ~/.playwriter/browsers/. Supports mac-arm64, mac-x64,
linux64, win64. Skips if already installed. Retries downloads 3x
with backoff and 5-minute timeout.

Add `playwriter session new --browser headless` to launch a headless
Chrome via chromium.launch() and create a direct CDP session. No
extension needed, no user browser touched. Multiple headless sessions
share the same Chrome process (each gets its own context/page).

Key implementation details:

- browser-config.ts checks ~/.playwriter/browsers/ first when
  resolving browser binaries (highest priority after explicit path
  and PLAYWRITER_BROWSER_PATH env var)

- Shared headless browser uses a static field on PlaywrightExecutor
  with a launch promise to deduplicate concurrent first-session races.
  Single disconnect handler registered at launch time clears both
  statics so next session relaunches after a crash.

- Session creation forces immediate connection so missing Chrome
  errors surface at `session new` time, not on first execute call.

- Reset only closes the session context in headless mode, not the
  shared browser (other sessions depend on it).

- Session delete closes the headless context to prevent leaks.
  Relay shutdown closes the shared browser.

- `browser list` shows headless as an option when a Chrome binary
  is available, alongside extension and cloud browsers.

- Zip extraction uses execFileSync with args array (not shell
  interpolation) for safety. Uses unzip on macOS/Linux and
  PowerShell Expand-Archive on Windows.

Session: ses_101fbc895ffeysfyjR2JE1su3L
2026-06-25 12:34:56 +02:00
Tommy D. Rossi 252e06d317 add cloud live CLI command, dashboard sessions table, replace Browser Use live URLs
**CLI: `playwriter cloud live [key]`**
- prints and opens playwriter.dev/live?wss= URL for an active cloud session
- accepts cloud-N key, cloudSessionId, or browserUseSessionId
- auto-selects when only one session is active

**Dashboard: browser slots table**
- new CloudSessionsPanel shows all subscription slots (not just active)
- active sessions: green dot, duration, expiry countdown, "Watch live" link
- empty slots: gray "Available" status with dashes
- header shows active/total counter (e.g. "1/4 active")
- auto-refreshes every 10s, clears transient errors on success

**Replace Browser Use live URLs**
- all liveUrl fields now point to our /live?wss= route instead of live.browser-use.com
- uses exact CDP endpoint URL (not reconstructed from UUID) to handle BU sharding
- cloud-api.ts, cli.ts, cloud-client.ts all updated
- new buildLiveUrl() helper in cloud-client.ts (CLI) and cloud-api.ts (website)

Session: ses_10bc2a749ffeInm3HXNY1r5v6D
2026-06-24 18:24:53 +02:00
Tommy D. Rossi 59500c77f8 fix cloud login: use better-auth client SDK, custom device/token endpoint
**CLI (`playwriter/src/cli.ts`):**
- Use `createAuthClient` with `deviceAuthorizationClient` plugin instead of
  hardcoded endpoint URLs. Hardcoded URLs broke when better-auth changed paths
  between versions. Now calls `client.device.code()` and `client.device.token()`.
- Added `better-auth` as a dependency of the playwriter package.

**Website server (`website/src/server.tsx`):**
- Added custom `/api/auth/device/token` handler in the auth middleware, intercepted
  before better-auth processes it. The drizzle adapter does not implement `consumeOne`
  which the device authorization plugin requires for the token exchange. Our handler
  does: find approved device code → delete it → create session → return access_token.
- Device verify page queries the deviceCode table directly via Drizzle instead of
  `auth.api.deviceVerify` (which depends on `incrementOne`, also missing from adapter).
- Session check moved before device code lookup so we redirect to login first.

**Website actions (`website/src/actions.tsx`):**
- Approve/deny actions update the DB directly via Drizzle instead of
  `auth.api.deviceApprove`/`deviceDeny` (which depend on adapter methods not
  available in better-auth-drizzle-adapter: `consumeOne`, `incrementOne`).

**Other:**
- Hidden Cloud Browsers tab in docs.json.
- Cloud browsers docs copy improvements (unrelated).
- Changeset for the playwriter package.

Session: ses_10aba81e2ffeUWR93aNz4avLK2
2026-06-23 20:24:19 +02:00
Tommy D. Rossi eeb59cc435 add API key authentication for cloud browsers
Add `@better-auth/api-key` plugin with `enableSessionForAPIKeys: true` so
API keys resolve to sessions transparently via the `x-api-key` header.
All existing `requireSession`/`requireOrgSession` calls work without changes.

**Server (website):**
- `apikey` table added to D1 schema with migration
- `apiKey()` plugin in `getAuth()` config, rate limiting disabled because
  `better-auth-drizzle-adapter` lacks `incrementOne` support
- `getSession()` guard updated to check `x-api-key` header
- Server actions: `createApiKey` and `revokeApiKey` in actions.tsx
- `listUserApiKeys` helper in db.ts
- `ApiKeyPanel` client component on dashboard (create, copy, revoke)
- `apiKeyClient()` plugin added to auth-client.ts

**CLI (playwriter):**
- `PLAYWRITER_API_KEY` env var is now highest-priority auth method
- API keys sent via `x-api-key` header; session tokens via `Authorization: Bearer`
- Auth priority: PLAYWRITER_API_KEY > PLAYWRITER_CLOUD_TOKEN > ~/.playwriter/auth.json
- Error messages updated to mention API key as alternative to `cloud login`

**Docs:**
- skill.md cloud section updated with API key auth option
- cloud-browsers.mdx getting started section adds Option B for API keys

Session: ses_10bef183cffeBNdpywc27HbcLi
2026-06-23 12:53:13 +02:00
Tommy D. Rossi 7b184d37e5 fix oracle findings: soften cloud tip wording, use pre-execute tracking snapshot
- removed "to stop the cloud browser" from tip since shared VMs
  are only stopped when all attached sessions are deleted
- use the cloudTracking variable captured before execute() instead
  of a fresh map lookup, so long-running executes that outlive idle
  cleanup still report isCloud correctly

Session: ses_10c89c8b8ffe5EaurFv0P4Q2jW
2026-06-23 11:56:19 +02:00
Tommy D. Rossi 86e02d1934 show session delete tip after every cloud browser execute
relay now includes isCloud in the execute response so the CLI knows
whether the session is backed by a cloud VM. after each execute call
on a cloud session, prints a dimmed reminder to delete the session
when the task is finished.

Session: ses_10c89c8b8ffe5EaurFv0P4Q2jW
2026-06-23 11:51:20 +02:00
Tommy D. Rossi 569f5fb731 fix oracle review findings for cloud browser UX
fixes 3 of 4 issues found by oracle review:

- **Network.enable before setBlockedURLs**: added Network.enable call
  before Network.setBlockedURLs to ensure reliable blocking across
  Chromium versions

- **cloud key typo shows available list**: handleCloudBrowserNotFound
  now takes hasCloudOptions param. if cloud options exist (user has
  cloud sessions), a typo like cloud-99 falls through to the generic
  "Browser not found" message showing available keys

- **expired token vs no subscription**: handleCloudBrowserNotFound
  now verifies the token with a quick API call when auth file exists.
  shows "authentication expired, please log in again" instead of
  "you may need a subscription" when the token is invalid

skipped fix 1 (persist blockProxyResources for reattach) because it
requires API changes to include proxy state in cloud session status.
minor edge case: user must re-pass --proxy when reattaching.

Session: ses_10c89c8b8ffe5EaurFv0P4Q2jW
2026-06-23 11:11:56 +02:00
Tommy D. Rossi c0d855b931 show helpful cloud login/subscribe instructions when --browser cloud fails
when user passes --browser cloud (or cloud-N) and no cloud browsers are
available, detect the cloud key prefix and show step-by-step instructions
instead of a generic "Browser not found" error.

two cases:
- not logged in: shows login + subscribe + retry steps
- logged in but no sessions: shows subscribe link + retry

checked at all 4 code paths where browser key lookup can fail:
direct-only mode, cloud-only mode, multi-browser mode, and the
no-extensions fallback path.

Session: ses_10c89c8b8ffe5EaurFv0P4Q2jW
2026-06-23 11:03:10 +02:00
Tommy D. Rossi 22bc3e1c26 disable proxy by default for cloud browsers, block raster images when proxy enabled, add cloud browser tips
**proxy disabled by default**: proxyCountryCode now defaults to null instead of us.
users must pass --proxy <region> explicitly to enable residential proxies.
saves cost for users who do not need anti-detection or geo-targeting.

**proxy bandwidth acceleration**: when proxy is enabled, raster images
(png, jpg, jpeg, gif, webp, ico, bmp, avif) are automatically blocked
via Network.setBlockedURLs CDP command. this is a single fire-and-forget
command with zero per-request overhead. SVGs, fonts, and videos are allowed.
opt out with --disable-proxy-bandwidth-acceleration on session new.

**cloud browser tips**: added printCloudTip() shown after every local
session creation and in browser list when no cloud sessions exist.
updated EXTENSION_NOT_CONNECTED_ERROR to mention cloud as option 3.

**skill.md cloud section expanded**: full feature list covering CAPTCHA
bypass, anti-detection, residential proxies, VPS usage, parallel execution
with subagents, and multiple identity control.

Session: ses_10c89c8b8ffe5EaurFv0P4Q2jW
2026-06-23 11:00:56 +02:00
Tommy D. Rossi bbf1241bf7 add cloud browser CLI commands and session new cloud integration
New CLI commands:
- cloud login: device flow auth with playwriter.dev, stores token in
  ~/.playwriter/auth.json. Uses goke openInBrowser for cross-platform URL opening.
- cloud status: shows active cloud VMs with expiry times
- cloud subscribe: opens dashboard in browser for subscription management

session new enhancements:
- Discovers cloud browsers alongside local extension and direct CDP instances
- --browser cloud creates a fresh VM, --browser cloud-N reattaches to running VM
- --proxy <region> enables residential proxy (us, de, jp, etc.)
- --custom-proxy user:pass@host:port for custom proxy servers
- --timeout <minutes> sets VM timeout (1-240, default 60)
- --disable-proxy-bandwidth-acceleration allows images/video/fonts through proxy
- Cloud-only users (no extension) see cloud options without error
- discoverCloudBrowsers() fetches active VMs + always adds a "cloud" new option

browser list also discovers and displays cloud browsers.

skill.md: document cloud browser CLI usage (login, status, session new with
proxy/region options, auto-stop behavior).

Session: ses_10d035e28ffe5cVA8SzrKoNgAP
2026-06-23 10:52:10 +02:00
Tommy D. Rossi d6d1eaeffc add cloud session tracking, idle timeout, and crash recovery to CDP relay
Cloud session lifecycle management in the relay server:

**Activity tracking**: CloudSessionTracking map tracks lastActivityAt and
activeExecutions count per relay session. Execute endpoint bumps activity
timestamps before and after each code execution. Sessions with active
executions are never idle-timed-out.

**Idle timeout**: 60s interval checks all cloud-backed sessions. Sessions
idle >10 minutes (or past BU hard timeout) are auto-disconnected via the
website /api/cloud/disconnect endpoint. Reference counting prevents stopping
a VM that is still used by another relay session (e.g. user attached twice).

**Crash recovery**: cloud session IDs are persisted to
~/.playwriter/cloud-sessions.json on every state change. On relay startup
(after successful listen), orphaned VMs from a previous crash are
disconnected. Atomic write (tmp+rename) prevents corrupt JSON on crash
during write. File is removed when no active sessions remain.

**Session creation**: /cli/session/new accepts optional cloud metadata
(cloudSessionId, cloudBaseUrl, cloudToken, timeoutAt, blockProxyResources)
and registers tracking. Session deletion cleans up tracking and stops the
VM if it is the last reference.

**Other**: global Hono error handler to prevent silent 500s.

Session: ses_10d035e28ffe5cVA8SzrKoNgAP
2026-06-23 10:51:56 +02:00
Tommy D. Rossi 7fdde7b558 add cloud session support to PlaywrightExecutor
Cloud-aware execution with VM expiration tracking and proxy bandwidth optimization:

- CloudSessionInfo interface: tracks timeoutAt and blockProxyResources per session
- Cloud timeout warnings: enqueue warnings when VM expires in <5 minutes,
  deduped by minute bucket so users get one warning per minute not per execute()
- Disconnection detection: isDisconnectionError() walks cause chains to detect
  browser/WebSocket/connection closed errors, maps them to a clear
  CLOUD_SESSION_EXPIRED_ERROR instead of cryptic Playwright internals
- Proxy resource blocking: applyProxyResourceBlocking() uses Network.setBlockedURLs
  CDP command to block raster images when proxy is enabled, saving residential
  proxy bandwidth. Single fire-and-forget command, zero per-request overhead.
  Applied to existing and future pages via context.on("page") listener.
- ensureConnection() wrapped in try/catch to detect cloud VM expiration on connect
- Error hints distinguish cloud disconnections from generic Playwright errors
- Remove captcha-solver import (stashed separately)

Session: ses_10d035e28ffe5cVA8SzrKoNgAP
2026-06-23 10:51:39 +02:00
Tommy D. Rossi e4d8228629 add cloud-client.ts for CLI cloud browser API and https:// CDP URL resolution
cloud-client.ts: HTTP client for CLI to call website /api/cloud/* routes.
- Auth persistence in ~/.playwriter/auth.json (PLAYWRITER_CLOUD_TOKEN env for CI)
- CloudClient class with getStatus, connect, disconnect, getSessionStatus methods
- getCloudClient() factory returns null if not logged in
- ConnectResult includes timeoutAt for VM expiration tracking

chrome-discovery.ts: resolve https:// CDP URLs from cloud browser providers
(Browser Use, Browserbase) to wss:// for WebSocket connections. Also handles
http:// to ws:// conversion.

Session: ses_10d035e28ffe5cVA8SzrKoNgAP
2026-06-23 10:51:27 +02:00
Tommy D. Rossi 9512aa2459 feat: add --patchright flag for stealth mode using @playwriter/patchright-core
Add conditional playwright-core import that switches between
@xmorse/playwright-core (default) and @playwriter/patchright-core
(stealth, bypasses bot detection) based on --patchright CLI flag
or PLAYWRITER_PATCHRIGHT=1 env var.

**playwright-import.ts**: new module with getChromium() async loader
that dynamically imports the correct package. Type imports always
come from @xmorse/playwright-core so TS works without the optional
dep installed.

**executor.ts**: switched from static chromium import to
await getChromium() at both connectOverCDP call sites.

**cli.ts**: added --patchright flag to default command and session new.

**package.json**: added @playwriter/patchright-core as optional dep.

**PLAYWRITER_AGENTS.md**: added patchright-playwriter fork section
with sync instructions.

The @playwriter/patchright-core package is built from
github.com/remorses/patchright-playwriter which applies patchright
stealth patches (no Runtime.enable, no Console.enable, CSP bypass,
removed automation flags) plus our playwriter extensions (targetId,
sessionId, getExistingCDPSession, onMouseAction, etc.) on top of
upstream Playwright.

Session: ses_1110cb2f6ffemR3JAe7i3Ig0bE
2026-06-22 14:11:20 +02:00
Tommy D. Rossi 6f51ad414c Document direct CDP connection and cloud browser provider support in skill.md
Rewrote the direct CDP section in skill.md with expanded documentation:

- Moved from under CLI Usage to also have a copy in the best practices
  section so both the CLI skill (full skill.md) and the MCP prompt
  (prompt.md, which strips CLI sections) include the docs
- Added cloud browser provider use case (e.g. wss://xxx.cdp.browser-use.com)
  as a first-class example alongside local Chrome debugging
- Documented PLAYWRITER_DIRECT env var for MCP client configs with a
  wss:// cloud URL example instead of just "1"
- Listed all accepted PLAYWRITER_DIRECT formats: "1", ws/wss URLs, host:port
- Noted recording limitation in direct CDP mode
- Added cloud provider URL to CLI --direct examples

Session: ses_11110612bffey1Qz5zvSEpOQ7C
2026-06-22 12:52:55 +02:00
Tommy D. Rossi bd6c7eb363 fix playwriter multi-profile browser routing
Route extension relay connections by their per-profile install id before falling back to Google account identity. This keeps two Chrome profiles signed into the same account from replacing each other in the relay.

Keep Playwright-created tabs in the intended Chrome window by remembering the window where the extension was activated and using that window for Target.createTarget and initial tabs.

Handle Chrome debugger cancellation as a global detach event. When Chrome reports canceled_by_user, clear all tracked debugger tabs and emit detach events so Playwright stops sending commands to tabs Chrome already detached from. Also avoid resurrecting stale error tab state if the detach happens while attachTab is still unwinding.

Validation:
- pnpm test --run src/relay-state.test.ts
- pnpm typecheck
- pnpm build in extension/
- Manual two-profile Hacker News navigation and comment extraction
- Mythos review found no blocking issues

Session: ses_120923e0fffe5l4GT77WwyXEGP
2026-06-19 13:31:31 +02:00
Tommy D. Rossi 6847ac41fa add remote access page to website docs, reference from skill.md
- Create website/src/docs/remote-access.mdx with full remote access guide
  (traforo tunnel setup, Docker/devcontainer, LAN, security, env vars)
- Add docs/remote-access to website navigation in docs.json
- Add "Remote access" section to skill.md with quick-start snippet
  linking to https://playwriter.dev/docs/remote-access
- Converted original docs/remote-access.md content to MDX with
  diagram code fences and Aside components

Session: ses_1257fc4baffeU7BhTtBImAFQ1a
2026-06-18 14:11:00 +02:00
Tommy D. Rossi e486f87914 revert relay block, keep only skill.md warning for clearBrowserCookies
The runtime block was too aggressive; the command might be useful in
some cases. Instead, rely on the skill doc rule so agents know not to
use it unless explicitly asked.

Session: ses_12890788fffeo527ik9Os483KR
2026-06-17 23:22:25 +02:00
Tommy D. Rossi db13737808 remove redundant never-use warning from skill.md, relay already blocks it
Session: ses_12890788fffeo527ik9Os483KR
2026-06-17 23:21:45 +02:00
Tommy D. Rossi 7ea40c0e04 block Network.clearBrowserCookies and Network.clearBrowserCache in CDP relay
These CDP commands are profile-wide destructive operations that wipe ALL
cookies/cache across every domain in the user Chrome profile, not just
the current page. A previous incident wiped Gmail, GitHub, and every
authenticated session.

Changes:
- cdp-relay.ts: added blocklist at top of routeCdpCommand() that throws
  a descriptive error if an agent or script tries to send either command.
  The error message explains the safe alternative (Network.getCookies +
  Network.deleteCookies for per-domain cookie clearing).
- skill.md: added prominent warning in the cookies section with the
  correct per-domain cookie clearing pattern using getCookies/deleteCookies.
- MEMORY.md: documented the incident for future sessions.

Session: ses_12890788fffeo527ik9Os483KR
2026-06-17 23:16:44 +02:00
Tommy D. Rossi 95ed4a1f04 release: playwriter@0.3.1 2026-06-13 17:40:55 +02:00
Tommy D. Rossi c1112964e6 Enable Playwriter auto pages by default
Auto page creation is now the default policy instead of per-client plumbing. MCP and CLI both create an initial blank tab when no Playwriter-enabled pages are available, which prevents a connected extension with zero targets from failing before the agent can act.

PLAYWRITER_AUTO_ENABLE=false is the single opt-out. The relay and executor both consult the same shouldAutoEnablePlaywriter helper so Target.setAutoAttach auto-creation and executor fallback page creation stay aligned.

This removes the earlier CLI-only autoEnable request flow from session creation and CDP URLs because auto-enable is no longer a special CLI capability.

Session: ses_143538111ffe2cKnv4TqZ7ZoqQ
2026-06-13 17:32:07 +02:00
Tommy D. Rossi 1088381e9f release: playwriter@0.3.0
Release Playwriter 0.3.0 with the pending user-facing changes since 0.2.0.

Included release notes:

1. Add `sinceLastCall` to `getLatestLogs()` so agents can inspect only new browser logs after each action.
2. Rotate `~/.playwriter/cdp.jsonl` automatically to prevent unbounded disk growth.
3. Include page runtime errors and related frame target logs in `getLatestLogs({ page })`.
4. Let CLI-created sessions auto-open initial extension tabs when MCP owns the shared relay.
5. Send bearer tokens on remote extension status checks.
6. Handle concurrent relay startup races without surfacing `EADDRINUSE` crashes.
7. Improve browser names in multi-browser session selection.
8. Document `getLatestLogs()` as the preferred page diagnostics API for agents.

Fixes #75
Fixes #85
Fixes #92

Session: ses_unknown
2026-06-13 17:21:11 +02:00
Tommy D. Rossi a7af83b7c0 Fix CLI auto-enable when MCP owns relay
CLI sessions requested auto-enable only by setting PLAYWRITER_AUTO_ENABLE when they spawned the relay process. That fails when MCP already owns the shared relay, because the CLI reuses the existing process and no per-session intent reaches the CDP connection.

Send an explicit autoEnable flag when creating CLI extension sessions, carry it through the generated CDP URL, and let the relay auto-create an initial tab for that specific Playwright client even if the process env was not set at startup.

This keeps MCP behavior unchanged while making CLI sessions resilient after the last enabled tab is closed.

Session: ses_143538111ffe2cKnv4TqZ7ZoqQ
2026-06-13 17:17:46 +02:00
Tommy D. Rossi dade84e7bd Address oracle review: atomic rotation, flush(), input validation, changeset
Oracle review fixes for CDP log rotation:
- Wrap rotate() in try/catch so failures do not break the write queue
- Use temp file + fs.rename for atomic rotation (no corruption on crash)
- Add resolvePositiveInt() to validate maxEntries from env/options
- Remove redundant `rotating` flag (queue serialization is sufficient)
- Expose flush() on CdpLogger so tests can await the queue
- Rewrite tests to use flush() instead of setTimeout (14ms vs 1.3s)
- Assert exact retained entry IDs via inline snapshot
- Add test for atomic rename (no .tmp file left behind)
- Add patch changeset for playwriter package

Session: ses_1545c4395ffeRR6V3ko2KSiVHT
2026-06-09 11:16:50 +02:00
Tommy D. Rossi d6176d484d Add CDP log rotation to prevent unbounded cdp.jsonl growth (#92)
The cdp.jsonl file grew without bound during long relay sessions,
causing disk exhaustion (5.7 GB reported in issue #92).

Added rotation logic to createCdpLogger:
- New `maxEntries` option (default 10,000, env PLAYWRITER_CDP_LOG_MAX_ENTRIES)
- When line count exceeds maxEntries, file is trimmed to keep the last half
- Rotation is chained on the async write queue, safe with concurrent writes
- File oscillates between maxEntries/2 and maxEntries lines

Added unit tests:
- Rotation triggers correctly at threshold
- No rotation when under limit
- Multiple rotations work with small maxEntries

Fixes #92

Session: ses_1545c4395ffeRR6V3ko2KSiVHT
2026-06-09 11:12:52 +02:00
Tommy D. Rossi 0ac72e189d fix clearAllLogs stale cursor bug, add changeset for sinceLastCall
clearAllLogs() now also clears pageLogCursor so subsequent sinceLastCall
calls do not see stale cursors that would cause them to return empty arrays.

Added changeset describing the sinceLastCall option and persistent logs
behavior change.

Session: ses_154681872ffeqje784ZqFUspy5
2026-06-09 11:12:01 +02:00
Tommy D. Rossi cbf0d6ce35 add sinceLastCall option to getLatestLogs, persist logs across navigation
getLatestLogs() now supports a sinceLastCall option that returns only new
console logs and page errors since the previous call. The first call returns
all buffered logs including pre-existing ones. This lets agents check for
errors after every action without seeing duplicates.

Browser console logs no longer clear on page navigation (framenavigated).
Logs persist across gotos and reloads so errors from page transitions are
never missed. The existing 5000-entry-per-page cap prevents unbounded growth.
When entries are shifted out at the cap, per-page cursors are decremented to
stay in sync.

Updated skill.md interaction feedback loop to instruct agents to always call
getLatestLogs({ page: state.page, sinceLastCall: true }) after every action
(goto, click, submit) to catch hydration errors, failed network requests,
and runtime exceptions.

Updated tests: two assertions that expected logs to be cleared on reload now
expect logs to persist (lines 831 and 978).

Session: ses_154681872ffeqje784ZqFUspy5
2026-06-09 11:07:41 +02:00
Tommy D. Rossi 7235248b4d block process.exit() and process.chdir() in sandbox
process.exit() would kill the relay server, affecting all sessions.
process.chdir() would change the relay cwd, affecting all sessions.

Both now throw descriptive errors instead.
Also simplified the Proxy: always use it (no conditional on sessionCwd),
falling back to real process.cwd() when sessionCwd is null.

Session: ses_16234add5ffeXGeXpSdt8Jttuc
2026-06-06 19:06:45 +02:00
Tommy D. Rossi 27728bb8bc expose process global in sandbox with correct session cwd
- Add `process` to usefulGlobals so it is available in the VM sandbox
- Override `process.cwd()` via Proxy to return the session cwd
  (where `playwriter session new` was invoked) instead of the
  relay server cwd
- Document `process` in the skill.md context variables section
- Document sandboxed fs write restrictions (only session cwd,
  /tmp, os.tmpdir() are writable)
- Document unavailable globals: __dirname, __filename, import

Session: ses_16234add5ffeXGeXpSdt8Jttuc
2026-06-06 19:05:07 +02:00
Tommy D. Rossi e39122bc5b fix: handle EADDRINUSE race on first relay startup (#75)
When two CLI/MCP commands start simultaneously with no relay running,
both detect the relay is down and both spawn one. The second process
crashes with EADDRINUSE on port 19988.

Three changes fix this:

1. cdp-relay.ts: switch from serve() to createAdaptorServer() so we
   inject WebSocket upgrade handlers before binding, then explicitly
   call server.listen() with once(listening)/once(error) handlers.
   EADDRINUSE is now a catchable promise rejection instead of an
   unhandled async error event.

2. start-relay-server.ts: catch EADDRINUSE in the spawned relay
   process. Poll /version for up to 2 seconds to check if another
   relay won the race. If yes, exit cleanly (code 0) instead of
   crashing with a scary error in the logs.

3. relay-client.ts: add module-level promise dedup so concurrent
   ensureRelayServer() calls within the same process only spawn once.
   When a process is found on the port but /version does not respond,
   poll /version for 2 seconds before killing it (prevents accidentally
   killing a relay that is still starting during a race).

Shared waitForRelayVersion() helper extracted to avoid duplicating the
poll loop in two places.

Session: ses_16355a286ffekJzHDlexPGKG56
2026-06-06 13:40:58 +02:00
Tommy D. Rossi 25dc876686 document getLatestLogs for browser log inspection
Update the Playwriter skill to tell agents to use getLatestLogs when inspecting browser logs instead of manually attaching page.on('console') listeners.

Manual console listeners only see future events, which can miss logs emitted during page startup or React hydration. The docs now explain that getLatestLogs captures browser console logs and pageerror entries from page start, and the examples now show page-scoped and hydration-error lookups.

Also add a playwriter patch changeset for the docs change.

Verification:
- pnpm typecheck

Known local check limitations:
- lintcn still reports pre-existing unrelated errors in aria-snapshot.ts, cdp-relay.ts, mcp.ts, and wait-for-page-load.ts

Session: ses_1913ab7ecffeHGoDq9rqCrGbCK
2026-05-28 15:49:16 +02:00
Tommy D. Rossi d9226b66cf fix getLatestLogs page error capture
Capture uncaught page runtime errors in getLatestLogs by listening to Playwright pageerror events and storing browser logs by Page object instead of target id.

This fixes page-scoped log lookup for pages where useful runtime errors surface outside the main page target, including React hydration failures on unframer.co.

Also add a regression assertion for pageerror entries and a playwriter patch changeset.

Verification:
- pnpm typecheck
- manual playwriter CLI check on https://unframer.co/ returns [pageerror] Minified React error #418

Known local check limitations:
- targeted Vitest could not launch because the local Playwright Chromium binary is missing
- lintcn still reports pre-existing unrelated errors in other files

Session: ses_1913ab7ecffeHGoDq9rqCrGbCK
2026-05-28 15:41:06 +02:00
Tommy D. Rossi e9339a3ecc fix: send auth token on remote status check routes
When using `--token` with a remote `--host`, the CLI and executor
status check fetches (`/extensions/status`, `/extension/status`) were
not sending the bearer token. The host validation middleware rejects
non-localhost requests without a valid token, so remote `session new`
and `session list` would silently fail with "no connected browsers".

**cli.ts**: `fetchExtensionsStatus` now accepts `{ host, token }` and
passes auth headers via `buildAuthHeaders` to both status endpoints.

**executor.ts**: `checkExtensionStatus` reads `token` from
`this.cdpConfig` (falling back to `PLAYWRITER_TOKEN` env) and attaches
`Authorization: Bearer` header to all status fetch calls.

`relay-client.ts` left unchanged since those functions are hardcoded
to `127.0.0.1` (local-only, always passes host validation).

Closes #91
Fixes #85

Session: ses_1b4c572a4ffev2Dct63TjK77gJ
2026-05-21 17:53:18 +02:00
Tommy D. Rossi 70f8deac7b Add Host header validation to prevent DNS rebinding attacks
DNS rebinding lets an attacker serve a page from http://evil.com:19988,
then rebind the DNS to 127.0.0.1. The browser treats subsequent requests
as same-origin, bypassing Sec-Fetch-Site, CORS preflight, and
Content-Type enforcement. This gives full access to privileged HTTP
routes like /cli/execute without any existing defense catching it.

The new global middleware rejects requests whose Host header hostname is
not localhost, 127.0.0.1, [::1], or ::1. Requests carrying a valid
bearer/query token bypass the check so remote clients still work.

The Host parser handles IPv6 brackets, bare ::1, case-insensitive
hostnames, and port suffixes via indexOf instead of a fragile regex.

Unit tests use raw http module (Node fetch forbids overriding Host) to
verify evil.com is rejected and all localhost variants pass, including
uppercase LOCALHOST.

Session: ses_1bfec4b08ffeL3NJunw0EC9plq
2026-05-19 14:03:02 +02:00
Tommy D. Rossi 668d97eb00 release: playwriter@0.2.0
Merges all changes since 0.1.0 (last published npm version):

- New -f/--file flag for executing JS from files
- React component inspection for pinned elements
- Performance profiling guide resource
- Shell tab completions via goke
- Security: token required on all requests regardless of source
- --token works on every remote subcommand
- Fixed Next.js webpack layer prefixes in React source paths
- Skill docs require absolute paths for saved artifacts

Session: ses_1bff6e68fffe5Ls42bfN8RUyVp
2026-05-19 13:46:21 +02:00
Tommy D. Rossi 014fd520ce docs(skill): add -f/--file execute-from-file section
Add "Execute from file" subsection to skill.md documenting the -f flag
for executing JavaScript from a file instead of inline -e code.

Session: ses_1d0419706ffelv53Gc4OrfwZ4L
2026-05-16 09:48:52 +02:00
Tommy D. Rossi d02fa5df17 feat(cli): add -f/--file flag, shell completions, and unknown command errors
Add `-f, --file <path>` option so users can execute JavaScript from a file
instead of inline `-e` strings. The two flags are mutually exclusive.

Enable `cli.completions()` from goke for shell tab completion support.

Change `cli.parse()` to `await cli.parse()` so async command actions
properly propagate errors and exit codes.

Add tests for unknown command and unknown subcommand error handling,
verifying they exit with code 1 and print helpful messages.

Bump goke from ^6.6.0 to ^6.12.1 which includes the completions API
and improved error messages for unknown commands.
2026-05-16 09:45:25 +02:00
Tommy D. Rossi bcd1e25ea2 docs: document changesets workflow for agents
Replace the old changelog instructions with the repo changesets workflow so future package changes add .changeset entries instead of editing generated CHANGELOG.md files or manually bumping public package versions.

Keep the Chrome extension manifest bump requirement separate because extension releases are not driven by npm changesets, and add a .changeset/readme.md so agents can quickly see what belongs in the folder.

Move the pending playwriter typecheck emit change into a changeset and revert the manual package version bump from the prior commit, leaving version updates to the publish flow.

Session: ses_1f763e4ecffenZcbrWQbx0Oh5F
2026-05-09 08:38:06 +02:00
Tommy D. Rossi a3ce2146ff fix: keep playwriter typecheck emitting dist
Run the playwriter package typecheck script through plain tsc instead of tsc --noEmit so local validation keeps compiled dist files up to date.

Bump the package version to 0.1.7 so the local dist-backed shim reports the updated package version after rebuilding.

Session: ses_1f763e4ecffenZcbrWQbx0Oh5F
2026-05-08 19:43:31 +02:00
Tommy D. Rossi d1346e7ca4 docs: add profano CPU profiling tips to performance resource
Mention .cpuprofile recording via CDP Profiler.* commands and
profano CLI for inspecting hot functions when vitals alone are
not enough to diagnose performance issues.

Session: ses_208943d84ffenqdbjxK0cO0iak
2026-05-05 11:39:01 +02:00
Tommy D. Rossi 7195359b22 fix: strip Next.js webpack layer prefixes from React source paths
bippy.normalizeFileName strips "/app-pages-browser/" but not the
parenthesized form "/(app-pages-browser)/" that Next.js webpack
actually uses in source maps. Added cleanFileName/cleanName that
runs after normalizeFileName and strips any /(layer-name)/ prefix
using regex, plus trailing "./".

Covers all Next.js webpack layer names: (app-pages-browser), (ssr),
(rsc), (action-browser), (pages-dir-browser), (pages-dir-edge),
(pages-dir-node).

Applied in both:
- Extension context menu handler (background.ts)
- MCP react-source.ts (getReactSource + getReactComponentInfo)

Also applied oracle review fixes:
- userGesture: true on clipboard Runtime.evaluate calls to prevent
  NotAllowedError when page loses focus after context menu
- Defensive fileName null check before building clipboard text

Session: ses_208943d84ffenqdbjxK0cO0iak
2026-05-05 11:38:36 +02:00
Tommy D. Rossi 10790c7392 auth: drop loopback bypass, attach token from env in in-process callers
Reviewer flagged the loopback-bypass added in 0.1.5 as a full auth
bypass under the recommended remote-access topology: tunnel agents
(traforo, ngrok, cloudflared) run as local processes and forward
public traffic to localhost:19988, so every public request reaches
the relay from 127.0.0.1. The bypass made `/cli/execute` (RCE),
`/cli/session/*`, `/recording/*` (file writes via outputPath), and
`/mcp-log` callable by anyone with the public tunnel URL.

Fix
---
1. cdp-relay.ts: privilegedRouteMiddleware now requires the token on
   every request, regardless of source. Loopback bypass removed.
2. cli.ts (`serve` action): exports `process.env.PLAYWRITER_TOKEN` at
   startup so in-process callers can attach Authorization: Bearer ...
   when calling the relay's own privileged endpoints.
3. screen-recording.ts: new `recordingHeaders()` helper reads
   PLAYWRITER_TOKEN from env and attaches the Bearer header to all
   four /recording/* calls (start, stop, status, cancel). Mirrors
   what mcp.ts already does for /mcp-log.
4. cli.ts (`executeCode`): refactored the inline header-build into
   the shared `buildAuthHeaders` helper so the six /cli/* fetch
   sites stay in sync.
5. connect-cdp-demo.ts: replaced the hardcoded 'secret_token'
   fallback with a hard error so the demo can never accidentally be
   the source of a leaked credential warning.

Bumps playwriter to 0.1.6 + CHANGELOG entry. Adds a MEMORY.md note
so the next person doesn't re-introduce the bypass.

Session: ses_20d25dcc7ffeJhVK7aC8Zh7OkW
2026-05-04 17:09:28 +02:00
Tommy D. Rossi 3d6f2d4239 auth: thread token through every remote CLI command + protect /mcp-log
Builds on Ivan Leo's auth-header pass with the missing pieces needed to
actually run `playwriter serve --token` end to end against a remote relay.

Fixes
-----
1. `--token` (or PLAYWRITER_TOKEN) now plumbed through every remote
   subcommand: `session new/list/delete/reset` and `browser list`. Until
   now only `playwriter -e` sent the Bearer header, so against a token-
   protected relay every other subcommand returned 401.
2. In-process `ExecutorManager` carries `token` in its `cdpConfig`, so the
   executor's own `chromium.connectOverCDP(ws://127.0.0.1:19988/cdp/<id>)`
   call passes the relay's own auth check.
3. `/mcp-log` is now under `privilegedRouteMiddleware`. Previously open,
   so any reachable client could spam the log file.
4. `mcp.ts` `sendLogToRelayServer` sends `Authorization: Bearer …` when
   `PLAYWRITER_TOKEN` is set.
5. Loopback connections (127.0.0.1, ::1, ::ffff:127.0.0.1) bypass the
   token check on `/cli/*`, `/recording/*`, and `/mcp-log`. They can only
   originate from in-process executor code which already runs downstream
   of an authenticated `/cdp/*` WebSocket, so re-checking the token would
   mean smuggling it into every internal helper for no real security
   gain. Resolves the broken `/recording/*` calls from
   `screen-recording.ts`.
6. New `connect-cdp-demo.ts` script: minimal example showing an external
   Playwright client connecting through `/cdp/<id>?token=<token>` over a
   tunnel.

Bumps `playwriter` to 0.1.5 and updates CHANGELOG.

Co-authored-by: Ivan Leo <ivanleomk@gmail.com>

Session: ses_20d25dcc7ffeJhVK7aC8Zh7OkW
2026-05-04 14:10:42 +02:00
Tommy D. Rossi fabd25e8f6 feat: add React component info for pinned elements
Expose getReactComponentInfo so agents can inspect the React component name, parent hierarchy, sanitized props, and source metadata for a locator or element handle. Non-React elements return null so the helper is safe to call from generic DOM inspection flows.

Update toolbar and context-menu pinned element copy to use a short inspectPinnedElement(url, expression) call. The wrapper resolves the pinned element, logs outerHTML plus React info internally, and keeps copied prompts small.

Session: ses_221fdd7c5ffexkyCmOtfmgwjoI
2026-04-30 15:51:45 +02:00
Tommy D. Rossi 480c1b9d64 docs: add performance profiling guide resource
Add a generated website resource that shows how to profile a real site with Playwriter using CDP, Navigation Timing, and PerformanceObserver.

The new guide focuses on the questions people actually ask during performance work: whether rendering is good or bad, which requests dominate transfer cost, what usually blocks first paint or LCP, and what is most likely blocking interactivity. It also includes ready-to-run Playwriter commands plus reusable TypeScript examples so the profiling workflow can be copied directly.

Session: ses_22252b7b8ffee2VFXqmaCzA2NR
2026-04-30 12:24:34 +02:00
Tommy D. Rossi e9918fcadc docs: require absolute paths for saved artifacts
Document that Playwright artifact APIs resolve paths inside Playwright client code instead of the sandboxed fs, so relative paths can land under the relay cwd.

Update the skill examples to use absolute paths for screenshots, PDFs, downloads, resize inputs, and recordings, and bump playwriter to 0.1.1 so the guidance ships with the release notes.

Session: ses_2411fc3d3ffe2bK1VDhxJd0u15
2026-04-24 13:11:31 +02:00
Tommy D. Rossi b7a2811caf release: playwriter@0.1.0 2026-04-16 12:01:54 +02:00
Tommy D. Rossi 9c2bac37d3 fix: handle shared dialog auto-close races in dialog abstraction
Move the JavaScript dialog race fix down into the Playwright fork instead of teaching the relay about a specific CDP error.

When multiple connectOverCDP clients share the same Playwriter tab, they all observe the same dialog and may all attempt the automatic best-effort close. Chrome accepts the first Page.handleJavaScriptDialog and rejects later attempts with "No dialog is showing". That follow-up failure should not crash the process because the dialog is already closed and the cleanup goal was achieved.

This commit:
- updates @xmorse/playwright-core Dialog.close() to ignore benign duplicate close races
- bumps playwright-core to 1.59.9 and documents the behavior in its changelog
- bumps playwriter to 0.0.112 and documents the user-visible fix
- adds a relay integration test that attaches two CDP clients to the same tab, opens an alert, and verifies both clients remain usable without an unhandled rejection

Why this abstraction is better:
- Dialog.close() is already the best-effort cleanup boundary
- the relay no longer needs protocol-specific knowledge of Page.handleJavaScriptDialog failures
- the behavior also protects any other shared-target setup that triggers the same close race

Validation:
- pnpm playwright:build
- pnpm typecheck (playwriter)
- pnpm test --run relay-core.test.ts -t "should ignore duplicate dialog dismissals from multiple CDP clients"
2026-04-14 10:57:03 +02:00