scenescout 3.23.3 → 3.23.4

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +20 -3
  3. package/dist/browsers.js +7 -6
  4. package/dist/ci-run.js +3 -2
  5. package/dist/cli.js +2 -1
  6. package/dist/clients.js +3 -3
  7. package/dist/code-routes.js +2 -2
  8. package/dist/commands.js +4 -4
  9. package/dist/engine/args.js +21 -0
  10. package/dist/engine/bench.js +1 -1
  11. package/dist/engine/brief.js +1 -1
  12. package/dist/engine/browser.js +59 -15
  13. package/dist/engine/capture.js +4 -4
  14. package/dist/engine/check-replay.js +82 -8
  15. package/dist/engine/check-report.js +8 -8
  16. package/dist/engine/check.js +15 -15
  17. package/dist/engine/ci.js +20 -20
  18. package/dist/engine/claims.js +4 -4
  19. package/dist/engine/collector.js +7 -7
  20. package/dist/engine/crawl.js +1 -1
  21. package/dist/engine/dedup.js +4 -4
  22. package/dist/engine/design.js +7 -7
  23. package/dist/engine/expiry.js +1 -1
  24. package/dist/engine/export.js +7 -7
  25. package/dist/engine/flow.js +10 -10
  26. package/dist/engine/forms.js +1 -1
  27. package/dist/engine/from-run.js +6 -6
  28. package/dist/engine/injection.js +1 -1
  29. package/dist/engine/lane.js +1 -1
  30. package/dist/engine/limits.js +2 -2
  31. package/dist/engine/live.js +30 -0
  32. package/dist/engine/memory.js +24 -7
  33. package/dist/engine/open.js +1 -1
  34. package/dist/engine/oracles.js +2 -2
  35. package/dist/engine/plain.js +1 -1
  36. package/dist/engine/png.js +2 -2
  37. package/dist/engine/policy.js +2 -2
  38. package/dist/engine/postmessage.js +7 -7
  39. package/dist/engine/project-folder.js +1 -1
  40. package/dist/engine/provider.js +1 -1
  41. package/dist/engine/replay.js +3 -1
  42. package/dist/engine/report.js +3 -3
  43. package/dist/engine/sarif.js +2 -2
  44. package/dist/engine/script-literal.js +11 -0
  45. package/dist/engine/sensitive.js +254 -0
  46. package/dist/engine/signed-in.js +2 -2
  47. package/dist/engine/status-pane-page.js +4 -3
  48. package/dist/engine/task.js +1 -1
  49. package/dist/engine/teardown.js +2 -0
  50. package/dist/engine/tickets.js +6 -6
  51. package/dist/engine/verify.js +3 -3
  52. package/dist/export-run.js +1 -1
  53. package/dist/installer.js +4 -4
  54. package/dist/login-run.js +2 -2
  55. package/dist/mcp-server.js +6 -9
  56. package/dist/playbook.js +1 -1
  57. package/package.json +2 -2
@@ -26,7 +26,7 @@ import { normalizePath } from "./fingerprint.js";
26
26
  import { failingSignatures, isWorthALook } from "./memory.js";
27
27
  /** What a re-test found. */
28
28
  export const VERDICTS = ["gone", "present", "changed"];
29
- export const VERDICT_MEANING = {
29
+ const VERDICT_MEANING = {
30
30
  gone: "re-tested and the evidence no longer reproduces — resolved",
31
31
  present: "re-tested and still reproduces exactly as filed",
32
32
  changed: "re-tested and something is different — still broken, but not as described",
@@ -172,7 +172,7 @@ const LOOK_ONLY = new Set([
172
172
  "record:full",
173
173
  ]);
174
174
  /** How the finding's page is reached again, or null when a check cannot re-test it deterministically. */
175
- export function loadRetest(item) {
175
+ function loadRetest(item) {
176
176
  const signatures = [...failingSignatures(item.evidence)];
177
177
  // A POST that failed is not re-sent by loading a page, and a check would refuse to send it anyway.
178
178
  if (signatures.length === 0 || signatures.some((s) => !s.startsWith("GET ")))
@@ -196,7 +196,7 @@ export function loadRetest(item) {
196
196
  return null;
197
197
  return { item, path: `${url.pathname}${url.search}`, signatures };
198
198
  }
199
- export const RETEST_VERDICTS = ["reproduces", "possibly-fixed", "not-reached"];
199
+ const RETEST_VERDICTS = ["reproduces", "possibly-fixed", "not-reached"];
200
200
  /**
201
201
  * Each re-testable finding against the page load that re-tests it.
202
202
  *
@@ -20,7 +20,7 @@ import { resolveFrame } from "./engine/replay.js";
20
20
  * the request before it failed (a create that timed out or got a 5xx), so
21
21
  * whether the issue exists has to be asked again rather than assumed.
22
22
  */
23
- export class ExportError extends Error {
23
+ class ExportError extends Error {
24
24
  uncertain;
25
25
  constructor(message, uncertain = false) {
26
26
  super(message);
package/dist/installer.js CHANGED
@@ -11,7 +11,7 @@ import fs from "node:fs";
11
11
  import { createRequire } from "node:module";
12
12
  import path from "node:path";
13
13
  import { APPROX_DISK_MB, BROWSER_ENGINES, browserPresence, engineOf, playwrightInstallArgs, } from "./browsers.js";
14
- export const SKILL_NAME = "scenescout";
14
+ const SKILL_NAME = "scenescout";
15
15
  export const MCP_NAME = "scenescout";
16
16
  /** Names this tool's skill and MCP server had before renames; cleaned up on install so the old slash command and a duplicate tool set do not linger. */
17
17
  const LEGACY_SKILL_NAMES = ["frontend-tester", "scenecraft"];
@@ -242,7 +242,7 @@ export function launchCommand(opts) {
242
242
  // resolves on PATH is better than an absolute path to nothing.
243
243
  return [fs.existsSync(npx) ? npx : "npx", ...NPX_SERVE_ARGS];
244
244
  }
245
- export function mcpAddArgs(launch) {
245
+ function mcpAddArgs(launch) {
246
246
  return ["mcp", "add", "--scope", "user", MCP_NAME, "--", ...launch];
247
247
  }
248
248
  export function manualRegisterCommand(launch) {
@@ -340,7 +340,7 @@ function removeLegacyRegistrations(opts) {
340
340
  * symlink, and on a case-insensitive filesystem through a differently-cased
341
341
  * path. Comparing strings reported both as "pointing elsewhere".
342
342
  */
343
- export function samePath(a, b) {
343
+ function samePath(a, b) {
344
344
  const real = (p) => {
345
345
  try {
346
346
  return fs.realpathSync.native(p);
@@ -568,7 +568,7 @@ export function installerFailure(stderr) {
568
568
  : null;
569
569
  }
570
570
  /** How long a browser download may run before it is stopped and reported as failed. */
571
- export const BROWSER_DOWNLOAD_TIMEOUT_MS = 15 * 60_000;
571
+ const BROWSER_DOWNLOAD_TIMEOUT_MS = 15 * 60_000;
572
572
  /**
573
573
  * The environment Playwright's installer runs in. A host that runs this server
574
574
  * inside its own Electron binary (a desktop app's bundled runtime) makes
package/dist/login-run.js CHANGED
@@ -104,7 +104,7 @@ const NOT_SAVED = {
104
104
  "timed-out": "The sign-in did not finish in time, so the window was closed and nothing was saved.",
105
105
  };
106
106
  /** Open the window a person signs in in. Throws a sentence when the browser cannot start or the page cannot load. */
107
- export async function openLoginWindow(options, opts = {}) {
107
+ async function openLoginWindow(options, opts = {}) {
108
108
  const engine = options.browser ?? defaultEngine(process.env);
109
109
  const types = { chromium, firefox, webkit };
110
110
  // Checked before a window opens: a limit out of bounds is a sentence, not a browser left behind.
@@ -199,7 +199,7 @@ async function lookAt(win, watch) {
199
199
  * when), the window closes, or `signal` aborts. Resolves with how it ended;
200
200
  * the caller saves and closes.
201
201
  */
202
- export async function watchForSignIn(win, options, opts = {}) {
202
+ async function watchForSignIn(win, options, opts = {}) {
203
203
  let closed = false;
204
204
  void win.closed.then(() => {
205
205
  closed = true;
@@ -52,7 +52,7 @@ import { decodedEntitiesNote, ignoredConventionsNote, LANE_CONVENTION_MAX, LANE_
52
52
  import { MAX_UNFILED_NAMED, unfiledDefects } from "./engine/calibration.js";
53
53
  import { CallWork, SessionQueue, watchdogCap, withWatchdog } from "./engine/dispatch.js";
54
54
  import { FIXTURE_KINDS } from "./engine/fixtures.js";
55
- import { feedForSession, LIVE_ENV, writeStatusFile, LIVE_TOKEN_FILE, liveEngines, liveTokenFileName, pidAlive, statusFileName, LiveServer, StatusBoard, } from "./engine/live.js";
55
+ import { feedForSession, LIVE_ENV, writeLiveToken, writeStatusFile, LIVE_TOKEN_FILE, liveEngines, liveTokenFileName, pidAlive, statusFileName, LiveServer, StatusBoard, } from "./engine/live.js";
56
56
  import { liveViewUrl, MCP_APP_MIME, paneData, paneText, STATUS_PANE_URI, STATUS_POLL_TOOL, STATUS_TOOL, } from "./engine/status-pane.js";
57
57
  import { statusPanePage } from "./engine/status-pane-page.js";
58
58
  import { formatBriefs, MAX_LANES, planLanes, replayBriefs } from "./engine/brief.js";
@@ -309,17 +309,13 @@ function publishLiveToken(dir) {
309
309
  // Named for this process: two engines on one project would otherwise hand
310
310
  // `watch` one token for two ports, and whichever wrote last would win.
311
311
  const file = path.join(dir, liveTokenFileName(process.pid));
312
- const write = fs.promises
313
- .writeFile(file, liveAddress.token, { mode: 0o600 })
314
- // The shared name stays too, for a `watch` from before per-pid files.
315
- .then(() => fs.promises.writeFile(path.join(dir, LIVE_TOKEN_FILE), liveAddress.token, { mode: 0o600 }))
316
- // `mode` applies only when the file is created; a leftover one keeps its old bits.
317
- .then(() => fs.promises.chmod(file, 0o600))
312
+ // The shared name is written too, for a `watch` from before per-pid files.
313
+ const write = writeLiveToken(dir, process.pid, liveAddress.token)
318
314
  .then(() => {
319
315
  liveDirs.add(dir);
320
316
  })
321
317
  .catch((err) => {
322
- // A file with the wrong bits, or none: either way nothing to take back later.
318
+ // Whatever was written before the failure is taken back: nothing is left to remove at shutdown.
323
319
  void fs.promises.rm(file, { force: true }).catch(() => { });
324
320
  if (!liveTokenWarned)
325
321
  console.error(`[scenescout] could not write the live view's token file in ${dir}: ${err instanceof Error ? err.message : String(err)}`);
@@ -553,6 +549,7 @@ function serializedControl(fn) {
553
549
  // The SDK passes the request's extra (progress token, notifications) after the arguments; it is kept for the call, as requestExtra.
554
550
  const call = () => requestExtra.run(asRequestExtra(args[1]), () => fn(...args));
555
551
  const run = controlChain.then(call, call);
552
+ // The caller gets `run` and its error; the chain only needs to know the call has finished, so a failure must not block the next control call.
556
553
  controlChain = run.catch(() => { });
557
554
  return run;
558
555
  };
@@ -1565,7 +1562,7 @@ server.registerTool("scout_click", {
1565
1562
  }
1566
1563
  }));
1567
1564
  server.registerTool("scout_type", {
1568
- description: "Type into a text input/textarea/composer by ref, the way a real user does: if the field already holds content (e.g. an @-mention chip a menu click inserted), the text is APPENDED at the end — preserving that content — and the result reports what was already there (a separating space is added only at a word-to-word boundary). Pass replace=true to clear the field first (correcting a previous entry); an empty textValue always clears. Appending fires input events but not keydown, so keydown-driven triggers (slash/mention menus) will not react to appended text. Use for both valid values and boundary/fuzz values (empty, very long, unicode, script tags).",
1565
+ description: "Type into a text input/textarea/composer by ref, the way a real user does: if the field already holds content (e.g. an @-mention chip a menu click inserted), the text is APPENDED at the end — preserving that content — and the result reports what was already there (a separating space is added only at a word-to-word boundary). Pass replace=true to clear the field first (correcting a previous entry); an empty textValue always clears. Appending fires input events but not keydown, so keydown-driven triggers (slash/mention menus) will not react to appended text. Use for both valid values and boundary/fuzz values (empty, very long, unicode, script tags). A value typed into a secret field (a password, a one-time code, a card number, a field labelled secret or token) is masked in the result and the log, as is what the field held.",
1569
1566
  inputSchema: {
1570
1567
  ref: z.string().describe("Element ref, e.g. e12"),
1571
1568
  textValue: z.string().optional().describe("Text to type"),
package/dist/playbook.js CHANGED
@@ -23,7 +23,7 @@ export const SERVER_INSTRUCTIONS = `SceneScout explores a running web app in a r
23
23
  `If this client offers a SceneScout skill, load that. Otherwise call ${PLAYBOOK_TOOL} once, before the first scout_attach in a conversation, and follow what it returns. ` +
24
24
  `They are the same text, so never read both. ` +
25
25
  `Never choose mode="destructive" yourself: that needs the user's explicit opt-in.`;
26
- export const LEVELS = ["minimal", "medium", "extensive"];
26
+ const LEVELS = ["minimal", "medium", "extensive"];
27
27
  /** What the `explore` prompt accepts, as MCP lists it. Every argument is optional. */
28
28
  export const EXPLORE_PROMPT_ARGUMENTS = [
29
29
  { name: "url", description: "URL of the running app, e.g. http://localhost:3000", required: false },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenescout",
3
- "version": "3.23.3",
3
+ "version": "3.23.4",
4
4
  "description": "SceneScout — exploratory UI testing for AI coding agents. An MCP server that gives any agent (Claude Code, Cursor, VS Code Copilot, Codex, Gemini CLI and others) a structured view of a running web app, always-on oracles, a network-level write policy, memory across runs and a gap-checked report.",
5
5
  "license": "MIT",
6
6
  "author": "brunoboto96",
@@ -119,7 +119,7 @@
119
119
  "devDependencies": {
120
120
  "@changesets/cli": "3.0.3",
121
121
  "@types/node": "^20.19.0",
122
- "prettier": "3.9.9",
122
+ "prettier": "3.9.10",
123
123
  "tsx": "^4.23.15",
124
124
  "typescript": "^5.9.3",
125
125
  "yaml": "^2.9.1"