scenescout 1.1.0 โ†’ 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # scenescout
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2932164: `scenescout install --browsers <list>` chooses what to download: `chromium` (the default, unchanged), `chromium-headless-shell` for the smallest working setup, `firefox`, `webkit`, or `all`. `scout_attach` takes a `browser` option, and `SCENESCOUT_BROWSER` sets the default. In Firefox and WebKit the engine keeps service workers from registering, because a request issued by one cannot be intercepted there and would pass the write policy. The missing-browser message and `doctor` now name the build that is actually missing, and the sizes quoted are the sizes on disk.
8
+
9
+ Pages are no longer given shared workers unless the mode is `destructive`. A request a shared worker sends cannot be intercepted in any browser, and a `DELETE` sent from one reached the server in read-only mode.
10
+
11
+ Hover no longer reports text that was already on the page and only moved to a new line, and the keyboard focus audit uses Option+Tab in WebKit on macOS, where plain Tab skips buttons and links.
12
+ - ad0f31d: `scenescout install --client <list>` registers the server with clients other than Claude Code: `cursor`, `vscode`, `codex`, `gemini`, `copilot` and `windsurf`. Clients that have a command for adding a server are registered through it; Cursor and Windsurf get an entry added to their JSON server list, with every other entry kept and an unreadable file left untouched. For VS Code, a `code` command that belongs to another editor is not used. The skill is installed only when `claude-code` is among the clients.
13
+ - 5504c63: The testing method now reaches every MCP client, not only Claude Code. A new `scout_playbook` tool returns it, the server's instructions tell an agent to call that tool before its first attach, and an `explore` prompt loads the method together with the target URL for clients that list server prompts as commands. It is the same text Claude Code loads as a skill, read from the same file.
14
+
15
+ ### Patch Changes
16
+
17
+ - d0aa6aa: The package description, keywords and README now present SceneScout as a tool for any MCP client, with Claude Code as one of them. `doctor --engine` ends with what to ask an agent instead of a Claude Code command.
18
+
3
19
  ## 1.1.0
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  # ๐Ÿ”ญ SceneScout
4
4
 
5
- **Exploratory UI testing, driven by an AI agent.**
5
+ **Exploratory UI testing, driven by the AI agent you already use.**
6
+
7
+ Works with Claude Code ยท Cursor ยท VS Code (Copilot) ยท Codex CLI ยท Gemini CLI ยท Copilot CLI ยท Windsurf ยท any [MCP](https://modelcontextprotocol.io) client
6
8
 
7
9
  [![test](https://github.com/brunoboto96/SceneScout/actions/workflows/test.yml/badge.svg)](https://github.com/brunoboto96/SceneScout/actions/workflows/test.yml)
8
10
  [![npm](https://img.shields.io/npm/v/scenescout.svg)](https://www.npmjs.com/package/scenescout)
@@ -14,11 +16,11 @@
14
16
 
15
17
  </div>
16
18
 
17
- SceneScout is an [MCP](https://modelcontextprotocol.io) server that hands an agent a *structured view* of a running web app โ€” every element, its geometry, and a set of always-on correctness oracles โ€” and lets the agent explore it like a curious user. Claude Code is the brain; SceneScout is the hands, eyes, and memory.
19
+ SceneScout is an [MCP](https://modelcontextprotocol.io) server that hands an agent a *structured view* of a running web app โ€” every element, its geometry, and a set of always-on correctness oracles โ€” and lets the agent explore it like a curious user. Your coding agent is the brain; SceneScout is the hands, eyes, and memory. Any MCP client can drive it, and the testing method comes with the server, so the agent knows how to use the tools wherever it runs.
18
20
 
19
21
  ```
20
22
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” MCP (stdio) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
21
- โ”‚ Claude Code + skill โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚ SceneScout engine โ”‚
23
+ โ”‚ Your coding agent โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚ SceneScout engine โ”‚
22
24
  โ”‚ (intent, judgment, โ”‚ โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ Playwright ยท oracles ยท memory โ”‚
23
25
  โ”‚ your subscription) โ”‚ tool results โ”‚ findings ยท report โ€” no LLM โ”‚
24
26
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
@@ -58,7 +60,7 @@ Every finding comes with a repro trace and a Playwright regression-test skeleton
58
60
 
59
61
  ## โœจ Why it's different
60
62
 
61
- - ๐Ÿง  **Claude is the brain โ€” no API key.** The engine contains no LLM. Exploration runs on your Claude Code subscription; SceneScout just gives it deterministic tools.
63
+ - ๐Ÿง  **Your agent is the brain โ€” no API key.** The engine contains no LLM. Exploration runs on the agent and subscription you already have (Claude Code, Cursor, Copilot, Codex, Gemini CLI and others); SceneScout just gives it deterministic tools and the method for using them.
62
64
  - ๐Ÿ“ **Structured scene, not pixels.** The agent reads element lists *with layout geometry*, not screenshots. Overlap and off-screen bugs are computed from boxes โ€” deterministic, no vision guessing. Images that failed to load are read from the DOM too. (Screenshots exist only for pixel-native residue like a canvas or a rendering glitch.)
63
65
  - ๐Ÿ›ก๏ธ **Read-only by default, enforced on the wire.** Destructive actions are blocked at the network layer, not by asking the model nicely. Opt into writes only against disposable data.
64
66
  - โœ… **Completion is a contract, not a vibe.** The engine knows the app's routes and *refuses* to file an "extensive" report while any known route is unvisited, unexercised, or un-audited. "Explored a bit and stopped" is structurally impossible.
@@ -80,7 +82,7 @@ SceneScout needs only a URL. Give it the source code as well and it gets noticea
80
82
  | **What a finding looks like** | the symptom, **plus** the file behind it and a suggested fix | the symptom, a repro trace, and a regression-test skeleton |
81
83
  | **Typical target** | `localhost` while you build | staging, a preview deploy, a client's site |
82
84
 
83
- **Why the codebase helps.** The agent driving SceneScout is Claude Code, which can already read your repository. With the source at hand it knows the app's static routes before opening the browser, so coverage is measured against the real app instead of whatever happened to be linked. It can also check a suspicion against the code before reporting it: "there is no way to export this table" is a much stronger finding once the agent has confirmed no export handler exists. And when something breaks it can open the component or handler responsible and tell you *where* and *how* to fix it โ€” "the save button does nothing" becomes "`OrderForm` swallows the rejected promise in `onSubmit`; surface the error and re-enable the button".
85
+ **Why the codebase helps.** The agent driving SceneScout is a coding agent, which can already read your repository. With the source at hand it knows the app's static routes before opening the browser, so coverage is measured against the real app instead of whatever happened to be linked. It can also check a suspicion against the code before reporting it: "there is no way to export this table" is a much stronger finding once the agent has confirmed no export handler exists. And when something breaks it can open the component or handler responsible and tell you *where* and *how* to fix it โ€” "the save button does nothing" becomes "`OrderForm` swallows the rejected promise in `onSubmit`; surface the error and re-enable the button".
84
86
 
85
87
  **Why it still works without it.** Everything SceneScout *observes* comes from the running page โ€” elements, layout geometry, console and network errors, design-audit scores, task-ease measurements โ€” and none of that needs source code. Point it at a URL you are allowed to test and it behaves like a thorough QA tester: it explores, reproduces, and files findings with evidence.
86
88
 
@@ -104,7 +106,7 @@ SceneScout needs only a URL. Give it the source code as well and it gets noticea
104
106
  | | |
105
107
  |---|---|
106
108
  | **Node** | โ‰ฅ 20 |
107
- | **An MCP client** | [Claude Code](https://claude.ai/code) is the first-class one (it loads the skill); [others work too](#-other-mcp-clients) |
109
+ | **An MCP client** | Claude Code, Cursor, VS Code with Copilot, Codex CLI, Gemini CLI, GitHub Copilot CLI, Windsurf, or [any other](#-other-mcp-clients) |
108
110
  | **A web app to test** | SceneScout tests a *live* app: start yours locally first (e.g. `npm run dev`, `make dev-up`), or have the URL of a deployed one you're allowed to test |
109
111
 
110
112
  ### 1๏ธโƒฃ Install
@@ -112,9 +114,12 @@ SceneScout needs only a URL. Give it the source code as well and it gets noticea
112
114
  It is on npm. Nothing to clone:
113
115
 
114
116
  ```bash
115
- npx -y scenescout install # skill + Chromium (~150 MB, one-time) + registers the server with Claude Code
117
+ npx -y scenescout install # Claude Code: skill + server + Chromium (one-time download)
118
+ npx -y scenescout install --client cursor # or: vscode, codex, gemini, copilot, windsurf (comma-separated for several)
116
119
  ```
117
120
 
121
+ Either way it downloads the browser and registers the server with the client you named. Claude Code also gets the method as a skill; every other client receives the same method from the server. [What each client gets](#-other-mcp-clients).
122
+
118
123
  **Prefer a Claude Code plugin?** The skill and the server arrive together:
119
124
 
120
125
  ```
@@ -124,13 +129,13 @@ npx -y scenescout install # skill + Chromium (~150 MB, one-time) + register
124
129
 
125
130
  Then download the browser once with `npx -y scenescout install --browser-only`. The command becomes `/scenescout:scenescout`. A plugin's skill comes from this repository and its server from the latest npm release, so right after a release lands here the two can differ for a short while; `/plugin marketplace update scenescout-marketplace` brings the skill up to date.
126
131
 
127
- **Another MCP client?** Run `npx -y scenescout install --browser-only` and [add the server to its config](#-other-mcp-clients).
132
+ **A client that is not in that list?** Run `npx -y scenescout install --browser-only` and [add the server to its config by hand](#-other-mcp-clients).
128
133
 
129
134
  <details>
130
135
  <summary>What <code>install</code> actually does</summary>
131
136
 
132
137
  1. puts the `/scenescout` skill into `~/.claude/skills/` (or `$CLAUDE_CONFIG_DIR/skills/`) โ€” a `scenescout` folder it didn't create is moved aside to a `.backup-โ€ฆ` copy, never deleted,
133
- 2. downloads the Chromium build SceneScout drives (skipped if you already have it),
138
+ 2. downloads the browser SceneScout drives (skipped if you already have it). By default that is Chromium, as two builds: the full browser for headed runs and the headless shell every other run uses. [Choose something else](#-choosing-browsers) with `--browsers`,
134
139
  3. registers the MCP server with Claude Code at user scope. Run through `npx`, the launcher is `npx -y scenescout serve`, with the absolute path of `npx` where one sits beside node, so it works under nvm/fnm. From a clone or a global install it is the absolute node path plus that install's `dist/mcp-server.js`.
135
140
 
136
141
  Re-run it any time: after moving the folder or switching node versions it refreshes the stored paths. It exits non-zero if any step failed, so it is safe to chain. Opt out of a step with `--no-register` or `--skip-browser`.
@@ -146,8 +151,8 @@ claude mcp add --scope user scenescout -- npx -y scenescout serve
146
151
  ### 2๏ธโƒฃ Check it
147
152
 
148
153
  ```bash
149
- npx -y scenescout doctor # everything, for the default install
150
- npx -y scenescout doctor --engine # plugin install or another MCP client: node + build + browser only
154
+ npx -y scenescout doctor --engine # any client: node + build + browser
155
+ npx -y scenescout doctor # Claude Code: the above, plus the skill and the registration
151
156
  ```
152
157
 
153
158
  Every line should be a โœ“. Anything that isn't prints the exact command that fixes it. Then **start a fresh session** in your client so it picks up the new tools.
@@ -156,13 +161,19 @@ Every line should be a โœ“. Anything that isn't prints the exact command that fi
156
161
 
157
162
  No app handy? Clone this repository and run `npm run demo:serve`: the [demo app](demo-app/) starts on `http://127.0.0.1:4173`.
158
163
 
159
- From Claude Code, inside the project you want to test (or, for a [remote URL](#-two-ways-to-use-it), any folder):
164
+ Open your agent inside the project you want to test (or, for a [remote URL](#-two-ways-to-use-it), any folder) and ask:
165
+
166
+ ```
167
+ Use SceneScout to test http://localhost:3000 at medium level
168
+ ```
169
+
170
+ In Claude Code the skill gives you a command with flags for the same thing:
160
171
 
161
172
  ```
162
173
  /scenescout --level medium --url http://localhost:3000 --role qa
163
174
  ```
164
175
 
165
- The skill scans the project (if there is one), attaches read-only, explores, and writes findings to `.scenescout/report.md`. That's it.
176
+ The agent scans the project (if there is one), attaches read-only, explores, and writes findings to `.scenescout/report.md`. That's it.
166
177
 
167
178
  **Common flags** โ€” `--level minimal|medium|extensive` ยท `--url <app>` ยท `--role <name\|path>` (a Playwright storage-state to explore as: a name found by the scan, or a path to the JSON file) ยท `--observe` / `--safe-write` / `--allow-destructive`.
168
179
 
@@ -192,11 +203,11 @@ Snapshots are cheap: re-snapshotting a route returns only *what changed*, with s
192
203
 
193
204
  ## ๐Ÿงฐ The toolbox
194
205
 
195
- 24 deterministic tools. The agent picks; you rarely call these by hand.
206
+ 25 deterministic tools. The agent picks; you rarely call these by hand.
196
207
 
197
208
  | Phase | Tools | What they do |
198
209
  |---|---|---|
199
- | **Set up** | `scout_scan` `scout_attach` `scout_session` | Discover routes; launch a browser in a write-mode; keep several authenticated roles alive at once |
210
+ | **Set up** | `scout_playbook` `scout_scan` `scout_attach` `scout_session` | Hand the testing method to an agent that has no skill loaded; discover routes; launch a browser in a write-mode; keep several authenticated roles alive at once |
200
211
  | **Explore** | `scout_crawl` `scout_coverage` | Sweep every route in one call; ask what's still untested |
201
212
  | **Look** | `scout_snapshot` `scout_hover` `scout_screenshot` | Read the structured scene (diffed); reveal tooltips/hover cards; capture pixels only when needed |
202
213
  | **Act** | `scout_click` `scout_type` `scout_select` `scout_upload` `scout_press` `scout_scroll` `scout_navigate` `scout_back` `scout_run_plan` | Drive the UI like a user; `scout_run_plan` batches a whole mechanical sequence into one call |
@@ -263,7 +274,7 @@ Run `npx -y scenescout doctor` first โ€” it checks every setup item below (every
263
274
  | The `scout_*` tools don't appear | The MCP server isn't registered, or points at an old path. `npx -y scenescout install` re-registers it; `claude mcp list` should show `scenescout` as connected. |
264
275
  | *"Executable not found in $PATH"* | The server was registered with a bare `node`. `npx -y scenescout install` registers an absolute path. |
265
276
  | Installed as a plugin, and the tools fail with *"Executable not found in $PATH: npx"* | A plugin starts the server with a bare `npx`, which Claude Code can only find if it was launched from an environment that has Node on its `PATH`. Under nvm or fnm that means starting Claude Code from a terminal, not from a dock or launcher. Or use `npx -y scenescout install` instead, which registers the absolute path of `npx`. |
266
- | *"Executable doesn't exist โ€ฆ chromium"* | The browser download was skipped or failed. `npx playwright install chromium` (on Linux add `--with-deps`). |
277
+ | *"โ€ฆ build has not been downloaded yet"* on attach | The browser download was skipped or failed, or the run asked for a browser you did not install. Run the command the message names, for example `npx -y scenescout install --browser-only --browsers firefox`. On Linux, system libraries may be missing too: `npx playwright install --with-deps chromium`. |
267
278
  | Tools broke after moving the folder or changing node version | The registration stores absolute paths. `npx -y scenescout install` refreshes them. |
268
279
  | Attach fails or every route lands on the login page | Your app isn't running at `--url`, or the `--role` storage state has expired โ€” regenerate it the way your project's Playwright setup does. |
269
280
 
@@ -275,17 +286,69 @@ Run `npx -y scenescout doctor` first โ€” it checks every setup item below (every
275
286
  ### ๐Ÿงน Uninstall
276
287
 
277
288
  ```bash
289
+ # Claude Code
278
290
  claude mcp remove --scope user scenescout
279
291
  rm -rf ~/.claude/skills/scenescout
292
+ # Codex / Gemini / Copilot CLI
293
+ codex mcp remove scenescout # likewise: gemini mcp remove โ€ฆ, copilot mcp remove โ€ฆ
280
294
  ```
281
295
 
296
+ For Cursor, Windsurf and VS Code, delete the `scenescout` entry from the client's MCP server list.
297
+
282
298
  Nothing else is installed: `npx` runs the package from npm's cache. Per-project memory lives in each tested project's `.scenescout/` folder; delete it there if you want it gone.
283
299
 
284
300
  ---
285
301
 
302
+ ## ๐ŸŒ Choosing browsers
303
+
304
+ `install` downloads Chromium and nothing else unless you ask. `--browsers` takes one name, a comma-separated list, or `all`:
305
+
306
+ | `--browsers` | What is downloaded | About, on disk |
307
+ |---|---|---|
308
+ | `chromium` *(default)* | the full browser and the headless shell | 550 MB |
309
+ | `chromium-headless-shell` | the headless shell only: every run works except `headed` | 200 MB |
310
+ | `firefox` | Firefox | 270 MB |
311
+ | `webkit` | WebKit, the engine behind Safari | 290 MB |
312
+ | `all` | Chromium, Firefox and WebKit | 1.1 GB |
313
+
314
+ ```bash
315
+ npx -y scenescout install --browsers chromium-headless-shell # the smallest working setup
316
+ npx -y scenescout install --browser-only --browsers firefox,webkit # add two more later
317
+ ```
318
+
319
+ Sizes vary by platform. The builds go to Playwright's shared cache, so a build another tool already fetched is not downloaded again.
320
+
321
+ To drive another browser, pass `browser` when attaching (`scout_attach { browser: "firefox" }`), or set `SCENESCOUT_BROWSER=webkit` in the server's environment to change the default. `scenescout doctor` checks the browser named by that variable in the shell it runs from, so check another one with `SCENESCOUT_BROWSER=webkit scenescout doctor`. Two things differ outside Chromium:
322
+
323
+ - **Service workers are not allowed to register** in Firefox and WebKit. The write policy works by intercepting requests, and only Chromium lets a request issued by a service worker be intercepted. An app that depends on its worker may behave differently there.
324
+ - **A Firefox or WebKit left behind by a crash is not cleaned up** on the next start the way a leftover Chromium is.
325
+
326
+ In every browser, pages are not given shared workers unless the mode is `destructive`: a request a shared worker sends cannot be intercepted anywhere, so the app is made to do that work on the page, where the policy sees it.
327
+
286
328
  ## ๐Ÿ”Œ Other MCP clients
287
329
 
288
- The engine is a plain MCP server over stdio, so any client can drive it. The Claude Code route is the one this project tests. The entries below follow each client's own documentation for a local stdio server and have not been run by the maintainer; if one is out of date, a correction is welcome (say which client version you checked). The server entry is always the same command โ€” `npx -y scenescout serve` โ€” only the config file differs. Download the browser once with `npx -y scenescout install --browser-only`.
330
+ The engine is a plain MCP server over stdio, so any client can drive it, and the testing method reaches the agent through the server itself (see the end of this section). `install` can register it for you:
331
+
332
+ ```bash
333
+ npx -y scenescout install --client cursor # one client
334
+ npx -y scenescout install --client vscode,codex # several; add claude-code to keep that one too
335
+ ```
336
+
337
+ | `--client` | How it is registered |
338
+ |---|---|
339
+ | `claude-code` *(default)* | `claude mcp add`, plus the skill |
340
+ | `cursor` | adds an entry to `~/.cursor/mcp.json`, keeping the others |
341
+ | `vscode` | VS Code's own `code --add-mcp`. A `code` command that belongs to another editor is not used |
342
+ | `codex` | `codex mcp add` |
343
+ | `gemini` | `gemini mcp add --scope user` |
344
+ | `copilot` | `copilot mcp add` (GitHub Copilot CLI) |
345
+ | `windsurf` | adds an entry to `~/.codeium/windsurf/mcp_config.json`, keeping the others |
346
+
347
+ A config file that is not valid JSON is left untouched, and the entry to add by hand is printed instead; a config that is a link into a dotfiles repository is written through the link. When a client that is registered through its own command is not installed, `install` says so and prints the command to run later. Cursor and Windsurf are files, so their entry is written whether or not the editor is installed yet. On Windows, a client installed through npm is a `.cmd` shim that `install` cannot start; it prints the command for you to run instead. Then restart the client and ask its agent: *"Use SceneScout to test http://localhost:3000"*.
348
+
349
+ What has been checked: registering through each command above was run against Codex CLI, Gemini CLI, GitHub Copilot CLI and VS Code, and Cursor's command line agent read the entry `install` wrote, connected and listed the tools. The Windsurf path follows its documentation. A full test session has been run in Claude Code, with and without the skill. If a client behaves differently for you, a correction is welcome (say which client version you checked).
350
+
351
+ To register by hand instead, the server entry is always the same command, `npx -y scenescout serve`:
289
352
 
290
353
  <details>
291
354
  <summary><strong>Cursor</strong> โ€” <code>~/.cursor/mcp.json</code> (or <code>.cursor/mcp.json</code> in a project)</summary>
@@ -383,7 +446,12 @@ Most clients accept the same `mcpServers` JSON shape shown for Cursor.
383
446
 
384
447
  </details>
385
448
 
386
- **The skill is what makes it good.** The tools are only hands and eyes; [`skills/scenescout/SKILL.md`](skills/scenescout/SKILL.md) is the method โ€” what to look at first, when to stop, what counts as a finding. Claude Code loads it as a skill. In another client, give the agent that file as its instructions (a rule, a custom mode, or pasted into the first message).
449
+ **The method travels with the server.** The tools are only hands and eyes; [`skills/scenescout/SKILL.md`](skills/scenescout/SKILL.md) is the method: what to look at first, when to stop, what counts as a finding. Claude Code loads it as a skill. Every other client gets the same text from the server, with nothing to copy:
450
+
451
+ - the server's instructions tell the agent to call `scout_playbook` before its first attach, and that tool returns the method,
452
+ - clients that list server prompts as commands also get an `explore` prompt, which loads the method and takes an optional URL, level and focus.
453
+
454
+ So in any client, a first message like *"Use SceneScout to test http://localhost:3000"* is enough. If an agent starts clicking without having called `scout_playbook`, tell it to call that first; how closely a model follows server instructions varies by client.
387
455
 
388
456
  The CLI is also useful on its own:
389
457
 
@@ -398,7 +466,7 @@ npx -y scenescout status <path> # what a running engine is doing right now
398
466
 
399
467
  ```
400
468
  src/
401
- mcp-server.ts the 24 tools + per-session dispatch
469
+ mcp-server.ts the 25 tools + per-session dispatch
402
470
  scan.ts project discovery (framework, routes, auth)
403
471
  cli.ts scan ยท serve ยท install ยท doctor ยท status
404
472
  installer.ts setup logic (skill link, MCP registration, diagnostics)
@@ -417,7 +485,7 @@ src/
417
485
  โ€ฆ collector ยท dispatch ยท fixtures ยท authloss ยท reaper
418
486
  scripts/ the 12 test suites (smoke/ holds the real-browser ones)
419
487
  test-app/ fixtures for the real-browser smoke tests
420
- skills/scenescout/ the Claude Code skill (SKILL.md)
488
+ skills/scenescout/ the testing method (SKILL.md): a skill in Claude Code, served by the server everywhere else
421
489
  docs/adr/ why it's built this way
422
490
  ```
423
491
 
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Which browsers SceneScout can drive, which builds `scenescout install`
3
+ * downloads for them, and which build a given launch needs.
4
+ *
5
+ * Kept free of Playwright so the rules can be table-tested: the CLI and the
6
+ * engine pass in the paths Playwright reports and get decisions back.
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ /** A browser the engine can launch. */
11
+ export const BROWSER_ENGINES = ["chromium", "firefox", "webkit"];
12
+ /**
13
+ * A build Playwright downloads. Chromium comes as two: the full browser, which
14
+ * a headed run opens, and the headless shell, which every headless run uses.
15
+ * Installing "chromium" brings both, which is what install has always done.
16
+ */
17
+ export const INSTALL_TARGETS = ["chromium", "chromium-headless-shell", "firefox", "webkit"];
18
+ export const DEFAULT_ENGINE = "chromium";
19
+ /** Environment variable naming the browser an attach uses when it does not name one. */
20
+ export const DEFAULT_ENGINE_ENV = "SCENESCOUT_BROWSER";
21
+ /** Approximate size on disk, so the install output can say what it is about to fetch. */
22
+ export const APPROX_DISK_MB = {
23
+ chromium: 550,
24
+ "chromium-headless-shell": 200,
25
+ firefox: 270,
26
+ webkit: 290,
27
+ };
28
+ export function isBrowserEngine(value) {
29
+ return BROWSER_ENGINES.includes(value);
30
+ }
31
+ /** The engine an attach uses when the caller names none: the environment's choice, else Chromium. */
32
+ export function defaultEngine(env) {
33
+ const raw = env[DEFAULT_ENGINE_ENV]?.trim().toLowerCase();
34
+ if (!raw)
35
+ return DEFAULT_ENGINE;
36
+ if (!isBrowserEngine(raw)) {
37
+ throw new Error(`${DEFAULT_ENGINE_ENV}="${env[DEFAULT_ENGINE_ENV]}" is not a browser SceneScout can drive. Use one of: ${BROWSER_ENGINES.join(", ")}.`);
38
+ }
39
+ return raw;
40
+ }
41
+ /**
42
+ * Read the value of `--browsers`. Absent means what install has always done.
43
+ * `all` is every engine; a comma-separated list picks several. "chromium"
44
+ * already includes the headless shell, so naming both is the same as naming it.
45
+ */
46
+ export function parseBrowserSelection(value) {
47
+ if (value === undefined)
48
+ return { targets: ["chromium"] };
49
+ const names = value
50
+ .split(",")
51
+ .map((s) => s.trim().toLowerCase())
52
+ .filter(Boolean);
53
+ const choices = `${INSTALL_TARGETS.join(", ")}, all`;
54
+ if (names.length === 0)
55
+ return { error: `--browsers needs a value. Choose from: ${choices}.` };
56
+ const picked = new Set();
57
+ for (const name of names) {
58
+ if (name === "all") {
59
+ for (const t of ["chromium", "firefox", "webkit"])
60
+ picked.add(t);
61
+ }
62
+ else if (INSTALL_TARGETS.includes(name)) {
63
+ picked.add(name);
64
+ }
65
+ else {
66
+ return { error: `"${name}" is not a browser SceneScout can install. Choose from: ${choices}.` };
67
+ }
68
+ }
69
+ if (picked.has("chromium"))
70
+ picked.delete("chromium-headless-shell");
71
+ return { targets: INSTALL_TARGETS.filter((t) => picked.has(t)) };
72
+ }
73
+ /** The arguments for Playwright's own installer. Target names are Playwright's names. */
74
+ export function playwrightInstallArgs(targets) {
75
+ return ["install", ...targets];
76
+ }
77
+ /** The build a launch needs on disk. */
78
+ export function launchTarget(engine, headed) {
79
+ if (engine !== "chromium")
80
+ return engine;
81
+ return headed ? "chromium" : "chromium-headless-shell";
82
+ }
83
+ /** The engine each build belongs to. */
84
+ export function engineOf(target) {
85
+ return target === "chromium-headless-shell" ? "chromium" : target;
86
+ }
87
+ /**
88
+ * Where the headless shell lives, worked out from the full browser's path.
89
+ * Playwright exposes no path for the shell, but it keeps the two side by side
90
+ * under one revision: `chromium-1234/โ€ฆ` next to `chromium_headless_shell-1234/`.
91
+ * Null when the path does not have that shape.
92
+ */
93
+ export function headlessShellDir(chromiumExecutable) {
94
+ if (!chromiumExecutable)
95
+ return null;
96
+ const parts = chromiumExecutable.split(/[\\/]/);
97
+ // The LAST such segment: a cache kept under a directory that happens to be
98
+ // named the same way must not be mistaken for the build directory.
99
+ let at = -1;
100
+ for (let i = parts.length - 1; i >= 0 && at < 0; i--)
101
+ if (/^chromium-\d+$/.test(parts[i]))
102
+ at = i;
103
+ if (at < 0)
104
+ return null;
105
+ const sep = chromiumExecutable.includes("\\") && !chromiumExecutable.includes("/") ? "\\" : "/";
106
+ return [...parts.slice(0, at), parts[at].replace(/^chromium-/, "chromium_headless_shell-")].join(sep);
107
+ }
108
+ /**
109
+ * Which builds are on disk. `executables` is what Playwright reports for each
110
+ * engine; a path Playwright names is not proof the file is there. The shell is
111
+ * counted only once Playwright has marked its download complete.
112
+ */
113
+ export function browserPresence(executables, exists = fs.existsSync) {
114
+ const at = (p) => ({ installed: !!p && exists(p), path: p });
115
+ const shellDir = headlessShellDir(executables.chromium);
116
+ const shellMarker = shellDir ? path.join(shellDir, "INSTALLATION_COMPLETE") : null;
117
+ const chromium = at(executables.chromium);
118
+ return {
119
+ chromium: { installed: chromium.installed && !!shellMarker && exists(shellMarker), path: chromium.path },
120
+ "chromium-headless-shell": { installed: !!shellMarker && exists(shellMarker), path: shellDir },
121
+ firefox: at(executables.firefox),
122
+ webkit: at(executables.webkit),
123
+ };
124
+ }
125
+ /** The command that downloads one build, for the way this copy of SceneScout was installed. */
126
+ export function installCommandFor(target, fromCheckout) {
127
+ const flags = `--browser-only --browsers ${target}`;
128
+ return fromCheckout ? `node dist/cli.js install ${flags}` : `npx -y scenescout install ${flags}`;
129
+ }
130
+ /**
131
+ * Whether pages may register service workers in this browser.
132
+ *
133
+ * The write policy works by intercepting requests. Only Chromium lets the
134
+ * driver intercept a request a service worker issues; in Firefox and WebKit
135
+ * such a request goes straight to the network, so an app that syncs its writes
136
+ * from a worker would send a DELETE through read-only mode with nothing
137
+ * logged. Blocking registration there makes the page send those requests
138
+ * itself, where the policy sees them.
139
+ */
140
+ export function serviceWorkerPolicy(engine) {
141
+ return engine === "chromium" ? "allow" : "block";
142
+ }
143
+ /**
144
+ * The key that moves keyboard focus to the next control, links and buttons
145
+ * included. WebKit on macOS follows Safari: plain Tab stops only at text
146
+ * fields, and Option+Tab stops everywhere. A focus audit that pressed Tab
147
+ * there would walk past every button and report nothing.
148
+ */
149
+ export function focusAdvanceKey(engine, platform) {
150
+ return engine === "webkit" && platform === "darwin" ? "Alt+Tab" : "Tab";
151
+ }
152
+ /**
153
+ * Run in every page before its own scripts: takes shared workers away.
154
+ *
155
+ * A request issued by a shared worker cannot be intercepted in any browser, so
156
+ * a write sent from one passes the policy unseen and unlogged. Removing the
157
+ * constructor makes feature detection fail, and an app then does that work on
158
+ * the page, where the policy sees it. Not applied in `destructive` mode, where
159
+ * the policy blocks nothing and the person has opted in to everything.
160
+ */
161
+ export const REMOVE_SHARED_WORKER_SCRIPT = `(() => {
162
+ try { delete globalThis.SharedWorker; } catch {}
163
+ if ("SharedWorker" in globalThis) {
164
+ try { Object.defineProperty(globalThis, "SharedWorker", { value: undefined, configurable: true, writable: true }); } catch {}
165
+ }
166
+ })()`;
167
+ /** Whether pages may use shared workers in this write mode. */
168
+ export function sharedWorkersAllowed(mode) {
169
+ return mode === "destructive";
170
+ }
171
+ /**
172
+ * What install says when the browsers it was asked for do not include the one
173
+ * a default attach launches, and that one is not on disk either. Without it,
174
+ * `install --browsers firefox` on a fresh machine ends in "ready" and the first
175
+ * attach fails. Null when there is nothing to say.
176
+ */
177
+ export function defaultAttachNote(opts) {
178
+ if (opts.defaultInstalled || opts.selected.length === 0)
179
+ return null;
180
+ if (opts.selected.some((t) => engineOf(t) === opts.defaultEngine))
181
+ return null;
182
+ const engine = engineOf(opts.selected[0]);
183
+ return (`an attach drives ${opts.defaultEngine} unless told otherwise, and that build is not installed. ` +
184
+ `Pass browser: "${engine}" when attaching, or set ${DEFAULT_ENGINE_ENV}=${engine} in the MCP server's environment.`);
185
+ }