@fusengine/browser-mcp 0.1.17 → 0.1.19

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/README.md CHANGED
@@ -89,9 +89,9 @@ fuse-browser probe 'https://www.booking.com/searchresults.html?ss=Tokyo' \
89
89
  # Use the installed Chrome
90
90
  fuse-browser probe https://example.com --channel chrome
91
91
 
92
- # SEO rank tracking across many keywords (one session)
92
+ # SEO rank tracking across many keywords (one session) — add --csv for a spreadsheet
93
93
  fuse-browser serp-batch "agence web vevey" "création site web lausanne" \
94
- --rank-domain fusengine.ch --serp-pages 2 --hl fr --gl ch
94
+ --rank-domain fusengine.ch --serp-pages 2 --hl fr --gl ch --csv > ranks.csv
95
95
 
96
96
  # Responsive screenshots (saved PNGs) — works on localhost too
97
97
  fuse-browser shots http://localhost:8000 --viewports mobile,desktop,1280x720
@@ -132,7 +132,7 @@ Supported: `FUSE_ENGINE`, `FUSE_CHANNEL`, `FUSE_CDP_ENDPOINT`, `FUSE_EXECUTABLE_
132
132
  `FUSE_HEADLESS`, `FUSE_COUNTRY`, `FUSE_CURRENCY`, `FUSE_USER_DATA_DIR`,
133
133
  `FUSE_STORAGE_STATE`, `FUSE_OUTPUT_DIR`.
134
134
 
135
- ### Tools (26)
135
+ ### Tools (27)
136
136
 
137
137
  | Group | Tools |
138
138
  | --- | --- |
@@ -144,7 +144,7 @@ Supported: `FUSE_ENGINE`, `FUSE_CHANNEL`, `FUSE_CDP_ENDPOINT`, `FUSE_EXECUTABLE_
144
144
  | **Agentic** | `browser_snapshot` (indexed refs), `browser_act` (by ref + page diff), `browser_run` (multi-step plan), `browser_collect` (exhaust a virtualized/infinite list) |
145
145
  | **Extract** | `browser_extract` (text/prices/hotels/challenges), `browser_extract_schema` (typed, by CSS selectors) |
146
146
  | **SERP** | `browser_serp_batch` — multi-query Google search in one session, per-query organic results + domain rank |
147
- | **Vision** | `browser_screenshot` (page, single element by `ref`, or responsive set via `viewport`/`viewports`) |
147
+ | **Vision** | `browser_screenshot` (page, single element by `ref`, or responsive set via `viewport`/`viewports`), `browser_visual_diff` (pixel diff vs baseline + changed-region boxes) |
148
148
 
149
149
  Key agentic patterns:
150
150
 
package/dist/bin/cli.js CHANGED
@@ -22,6 +22,7 @@ const { positionals, values } = parseArgs({
22
22
  "extract-serp": { type: "boolean" },
23
23
  "serp-pages": { type: "string" },
24
24
  "rank-domain": { type: "string" },
25
+ csv: { type: "boolean" },
25
26
  viewports: { type: "string" },
26
27
  "settle-ms": { type: "string" },
27
28
  hl: { type: "string" },
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/bin/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/bin/cli.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"serp-batch-cli.d.ts","sourceRoot":"","sources":["../../src/bin/serp-batch-cli.ts"],"names":[],"mappings":"AAQA,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAGtC,sDAAsD;AACtD,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBnF"}
1
+ {"version":3,"file":"serp-batch-cli.d.ts","sourceRoot":"","sources":["../../src/bin/serp-batch-cli.ts"],"names":[],"mappings":"AASA,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAGtC,sDAAsD;AACtD,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBnF"}
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { resolveConfig } from "../agent/config.js";
6
6
  import { serpBatch } from "../agent/serp-batch.js";
7
+ import { serpBatchToCsv } from "../lib/serp-csv.js";
7
8
  const str = (v) => (typeof v === "string" ? v : undefined);
8
9
  /** Run the `serp-batch` subcommand over `queries`. */
9
10
  export async function runSerpBatch(queries, values) {
@@ -26,6 +27,6 @@ export async function runSerpBatch(queries, values) {
26
27
  gl: str(values.gl),
27
28
  delayMs: values["delay-ms"] ? Number(values["delay-ms"]) : undefined,
28
29
  });
29
- process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
30
+ process.stdout.write(values.csv ? serpBatchToCsv(rows) : `${JSON.stringify(rows, null, 2)}\n`);
30
31
  }
31
32
  //# sourceMappingURL=serp-batch-cli.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"serp-batch-cli.js","sourceRoot":"","sources":["../../src/bin/serp-batch-cli.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAInD,MAAM,GAAG,GAAG,CAAC,CAAU,EAAsB,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAExF,sDAAsD;AACtD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAiB,EAAE,MAAc;IAClE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC9D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,MAAM,GAAG,aAAa,CAAC;QAC3B,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAA2B;QACpD,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC;QAChC,QAAQ,EAAE,CAAC,MAAM,CAAC,MAAM;QACxB,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QACpC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;KAC5B,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE;QACnC,OAAO;QACP,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACtC,KAAK,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;QACtE,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAClB,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;KACrE,CAAC,CAAC;IACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC7D,CAAC"}
1
+ {"version":3,"file":"serp-batch-cli.js","sourceRoot":"","sources":["../../src/bin/serp-batch-cli.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGpD,MAAM,GAAG,GAAG,CAAC,CAAU,EAAsB,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAExF,sDAAsD;AACtD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAiB,EAAE,MAAc;IAClE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC9D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,MAAM,GAAG,aAAa,CAAC;QAC3B,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAA2B;QACpD,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC;QAChC,QAAQ,EAAE,CAAC,MAAM,CAAC,MAAM;QACxB,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QACpC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;KAC5B,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE;QACnC,OAAO;QACP,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACtC,KAAK,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;QACtE,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAClB,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;KACrE,CAAC,CAAC;IACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AACjG,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Group a pixelmatch diff mask into changed-region bounding boxes. Quantizes
3
+ * diff pixels onto a coarse grid (cellSize) then flood-fills adjacent dirty
4
+ * cells into rectangles — O(cells), no per-pixel recursion. Pure & testable.
5
+ * @module lib/diff-regions
6
+ */
7
+ /** A rectangular zone (in pixels) that changed between two images. */
8
+ export interface DiffRegion {
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ }
14
+ /**
15
+ * Bounding boxes of changed clusters from an RGBA diff `mask` (pixelmatch marks
16
+ * changed pixels red). A grid cell is dirty when it holds >= `minPerCell` diff
17
+ * pixels; adjacent dirty cells (4-connected) merge into one region.
18
+ */
19
+ export declare function computeDiffRegions(mask: Uint8Array, width: number, height: number, cellSize?: number, minPerCell?: number): DiffRegion[];
20
+ //# sourceMappingURL=diff-regions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-regions.d.ts","sourceRoot":"","sources":["../../src/lib/diff-regions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,SAAK,EACb,UAAU,SAAI,GACb,UAAU,EAAE,CAgDd"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Group a pixelmatch diff mask into changed-region bounding boxes. Quantizes
3
+ * diff pixels onto a coarse grid (cellSize) then flood-fills adjacent dirty
4
+ * cells into rectangles — O(cells), no per-pixel recursion. Pure & testable.
5
+ * @module lib/diff-regions
6
+ */
7
+ /**
8
+ * Bounding boxes of changed clusters from an RGBA diff `mask` (pixelmatch marks
9
+ * changed pixels red). A grid cell is dirty when it holds >= `minPerCell` diff
10
+ * pixels; adjacent dirty cells (4-connected) merge into one region.
11
+ */
12
+ export function computeDiffRegions(mask, width, height, cellSize = 16, minPerCell = 2) {
13
+ const cols = Math.ceil(width / cellSize);
14
+ const rows = Math.ceil(height / cellSize);
15
+ const dirty = new Uint32Array(cols * rows);
16
+ for (let y = 0; y < height; y++) {
17
+ for (let x = 0; x < width; x++) {
18
+ const p = (y * width + x) * 4;
19
+ if (mask[p] && mask[p + 1] === 0 && mask[p + 2] === 0) {
20
+ const ci = Math.floor(y / cellSize) * cols + Math.floor(x / cellSize);
21
+ dirty[ci] = (dirty[ci] ?? 0) + 1;
22
+ }
23
+ }
24
+ }
25
+ const flagged = dirty.map((n) => (n >= minPerCell ? 1 : 0));
26
+ const seen = new Uint8Array(cols * rows);
27
+ const regions = [];
28
+ for (let cy = 0; cy < rows; cy++) {
29
+ for (let cx = 0; cx < cols; cx++) {
30
+ if (!flagged[cy * cols + cx] || seen[cy * cols + cx])
31
+ continue;
32
+ let minX = cx;
33
+ let maxX = cx;
34
+ let minY = cy;
35
+ let maxY = cy;
36
+ const stack = [[cx, cy]];
37
+ seen[cy * cols + cx] = 1;
38
+ while (stack.length) {
39
+ const [x, y] = stack.pop();
40
+ minX = Math.min(minX, x);
41
+ maxX = Math.max(maxX, x);
42
+ minY = Math.min(minY, y);
43
+ maxY = Math.max(maxY, y);
44
+ for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {
45
+ const i = ny * cols + nx;
46
+ if (nx >= 0 && nx < cols && ny >= 0 && ny < rows && flagged[i] && !seen[i]) {
47
+ seen[i] = 1;
48
+ stack.push([nx, ny]);
49
+ }
50
+ }
51
+ }
52
+ regions.push({
53
+ x: minX * cellSize,
54
+ y: minY * cellSize,
55
+ width: Math.min((maxX + 1) * cellSize, width) - minX * cellSize,
56
+ height: Math.min((maxY + 1) * cellSize, height) - minY * cellSize,
57
+ });
58
+ }
59
+ }
60
+ return regions;
61
+ }
62
+ //# sourceMappingURL=diff-regions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-regions.js","sourceRoot":"","sources":["../../src/lib/diff-regions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAgB,EAChB,KAAa,EACb,MAAc,EACd,QAAQ,GAAG,EAAE,EACb,UAAU,GAAG,CAAC;IAEd,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YAC9B,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;gBACtD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,QAAQ,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC;gBACtE,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACzC,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC;QACjC,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC;YACjC,IAAI,CAAC,OAAO,CAAC,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC;gBAAE,SAAS;YAC/D,IAAI,IAAI,GAAG,EAAE,CAAC;YACd,IAAI,IAAI,GAAG,EAAE,CAAC;YACd,IAAI,IAAI,GAAG,EAAE,CAAC;YACd,IAAI,IAAI,GAAG,EAAE,CAAC;YACd,MAAM,KAAK,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;YACzB,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;YACzB,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;gBACpB,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,EAAsB,CAAC;gBAC/C,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBACzB,KAAK,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAA4B,EAAE,CAAC;oBACnG,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC;oBACzB,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;wBAC3E,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;wBACZ,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;oBACvB,CAAC;gBACH,CAAC;YACH,CAAC;YACD,OAAO,CAAC,IAAI,CAAC;gBACX,CAAC,EAAE,IAAI,GAAG,QAAQ;gBAClB,CAAC,EAAE,IAAI,GAAG,QAAQ;gBAClB,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,GAAG,IAAI,GAAG,QAAQ;gBAC/D,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,QAAQ;aAClE,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/dist/lib/fs.d.ts CHANGED
@@ -4,6 +4,8 @@ export declare function ensureDir(dir: string): void;
4
4
  export declare function writeFileEnsured(filePath: string, content: string): void;
5
5
  /** Sérialise en JSON indenté et écrit le fichier. */
6
6
  export declare function writeJson(filePath: string, value: unknown): void;
7
+ /** Écrit des octets bruts (PNG, etc.) en garantissant le dossier parent. */
8
+ export declare function writeFileBytes(filePath: string, data: Uint8Array): void;
7
9
  /** Lit et parse un JSON ; renvoie `fallback` en cas d'erreur/absence. */
8
10
  export declare function readJsonSafe<T>(filePath: string, fallback: T): T;
9
11
  /** SHA-1 hexadécimal d'une chaîne (signatures DOM, run ids — usage non cryptographique). */
@@ -1 +1 @@
1
- {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/lib/fs.ts"],"names":[],"mappings":"AAQA,8CAA8C;AAC9C,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE3C;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAGxE;AAED,qDAAqD;AACrD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAEhE;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAMhE;AAED,4FAA4F;AAC5F,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1C"}
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/lib/fs.ts"],"names":[],"mappings":"AAQA,8CAA8C;AAC9C,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE3C;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAGxE;AAED,qDAAqD;AACrD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAEhE;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAGvE;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAMhE;AAED,4FAA4F;AAC5F,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1C"}
package/dist/lib/fs.js CHANGED
@@ -18,6 +18,11 @@ export function writeFileEnsured(filePath, content) {
18
18
  export function writeJson(filePath, value) {
19
19
  writeFileEnsured(filePath, JSON.stringify(value, null, 2));
20
20
  }
21
+ /** Écrit des octets bruts (PNG, etc.) en garantissant le dossier parent. */
22
+ export function writeFileBytes(filePath, data) {
23
+ ensureDir(dirname(filePath));
24
+ writeFileSync(filePath, data);
25
+ }
21
26
  /** Lit et parse un JSON ; renvoie `fallback` en cas d'erreur/absence. */
22
27
  export function readJsonSafe(filePath, fallback) {
23
28
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/lib/fs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,8CAA8C;AAC9C,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,OAAe;IAChE,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7B,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAC5C,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,KAAc;IACxD,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAAI,QAAgB,EAAE,QAAW;IAC3D,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAM,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,IAAI,CAAC,KAAa;IAChC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACxD,CAAC"}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/lib/fs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,8CAA8C;AAC9C,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,OAAe;IAChE,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7B,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAC5C,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,KAAc;IACxD,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,IAAgB;IAC/D,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7B,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAChC,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAAI,QAAgB,EAAE,QAAW;IAC3D,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAM,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,IAAI,CAAC,KAAa;IAChC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { type DiffRegion } from "./diff-regions.js";
2
+ /** Result of comparing two PNG buffers. */
3
+ export interface PixelDiff {
4
+ width: number;
5
+ height: number;
6
+ diffPixels: number;
7
+ diffRatio: number;
8
+ regions: DiffRegion[];
9
+ diffPng: Uint8Array;
10
+ }
11
+ /** Compare two PNG buffers; `threshold` is pixelmatch's color tolerance (0..1). */
12
+ export declare function diffPng(baseline: Uint8Array, current: Uint8Array, threshold?: number): PixelDiff;
13
+ //# sourceMappingURL=pixel-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pixel-diff.d.ts","sourceRoot":"","sources":["../../src/lib/pixel-diff.ts"],"names":[],"mappings":"AAQA,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AA+BxE,2CAA2C;AAC3C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,mFAAmF;AACnF,wBAAgB,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,SAAM,GAAG,SAAS,CAkB7F"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * PNG pixel diff via fast-png (decode/encode) + pixelmatch — pure JS, no native
3
+ * build. Returns diff stats, changed-region boxes and the highlighted diff PNG.
4
+ * Throws `dimension_mismatch` when sizes differ (no silent resize).
5
+ * @module lib/pixel-diff
6
+ */
7
+ import { decode, encode } from "fast-png";
8
+ import pixelmatch from "pixelmatch";
9
+ import { computeDiffRegions } from "./diff-regions.js";
10
+ /**
11
+ * Normalize any channel/depth layout to packed 8-bit RGBA (alpha defaults to
12
+ * 255). 16-bit samples are scaled down to 8-bit so pixelmatch sees byte values.
13
+ */
14
+ function toRgba(img) {
15
+ const { width, height, data, channels, depth } = img;
16
+ const scale = (v) => (depth === 16 ? v >> 8 : v);
17
+ if (channels === 4 && depth === 8)
18
+ return data;
19
+ const out = new Uint8Array(width * height * 4);
20
+ for (let i = 0; i < width * height; i++) {
21
+ const g = scale(data[i * channels] ?? 0);
22
+ out[i * 4] = channels >= 3 ? scale(data[i * channels] ?? 0) : g;
23
+ out[i * 4 + 1] = channels >= 3 ? scale(data[i * channels + 1] ?? 0) : g;
24
+ out[i * 4 + 2] = channels >= 3 ? scale(data[i * channels + 2] ?? 0) : g;
25
+ out[i * 4 + 3] =
26
+ channels === 2 ? scale(data[i * channels + 1] ?? 255) : channels === 4 ? scale(data[i * channels + 3] ?? 255) : 255;
27
+ }
28
+ return out;
29
+ }
30
+ /** Compare two PNG buffers; `threshold` is pixelmatch's color tolerance (0..1). */
31
+ export function diffPng(baseline, current, threshold = 0.1) {
32
+ const a = decode(baseline);
33
+ const b = decode(current);
34
+ if (a.width !== b.width || a.height !== b.height) {
35
+ throw new Error(`dimension_mismatch: ${a.width}x${a.height} vs ${b.width}x${b.height}`);
36
+ }
37
+ const { width, height } = a;
38
+ const out = new Uint8Array(width * height * 4);
39
+ // Playwright PNGs are often RGB (no alpha); normalize both to packed RGBA.
40
+ const diffPixels = pixelmatch(toRgba(a), toRgba(b), out, width, height, { threshold, includeAA: false });
41
+ return {
42
+ width,
43
+ height,
44
+ diffPixels,
45
+ diffRatio: width * height ? diffPixels / (width * height) : 0,
46
+ regions: computeDiffRegions(out, width, height),
47
+ diffPng: encode({ width, height, data: out }),
48
+ };
49
+ }
50
+ //# sourceMappingURL=pixel-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pixel-diff.js","sourceRoot":"","sources":["../../src/lib/pixel-diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * RFC 4180 CSV export for SERP/rank batches. Dependency-free: one row per query
3
+ * with rank-tracking columns; array cells (organic/ads positions) are joined
4
+ * with ";" inside a quoted field. `toCsv` is generic and reused/tested directly.
5
+ * @module lib/serp-csv
6
+ */
7
+ import type { SerpBatchRow } from "../interfaces/serp.js";
8
+ /** Format rows of plain objects as RFC 4180 CSV (CRLF, header from `columns`). */
9
+ export declare function toCsv(rows: Record<string, unknown>[], columns: string[]): string;
10
+ /** Render SERP batch rows as a rank-tracking CSV (one row per query). */
11
+ export declare function serpBatchToCsv(rows: SerpBatchRow[]): string;
12
+ //# sourceMappingURL=serp-csv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serp-csv.d.ts","sourceRoot":"","sources":["../../src/lib/serp-csv.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAQ1D,kFAAkF;AAClF,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAIhF;AAKD,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,MAAM,CAY3D"}
@@ -0,0 +1,28 @@
1
+ /** Quote a CSV field per RFC 4180 (arrays joined with ";"). */
2
+ function field(value) {
3
+ const s = value === null || value === undefined ? "" : Array.isArray(value) ? value.join(";") : String(value);
4
+ return /[",\r\n]/.test(s) || s !== s.trim() ? `"${s.replaceAll('"', '""')}"` : s;
5
+ }
6
+ /** Format rows of plain objects as RFC 4180 CSV (CRLF, header from `columns`). */
7
+ export function toCsv(rows, columns) {
8
+ const header = columns.map(field).join(",");
9
+ const body = rows.map((row) => columns.map((c) => field(row[c])).join(","));
10
+ return [header, ...body].join("\r\n") + "\r\n";
11
+ }
12
+ /** Columns emitted for a SERP batch CSV (rank-tracking view, one row per query). */
13
+ const COLUMNS = ["query", "domain", "found", "best", "organic", "ads", "results", "error"];
14
+ /** Render SERP batch rows as a rank-tracking CSV (one row per query). */
15
+ export function serpBatchToCsv(rows) {
16
+ const flat = rows.map((r) => ({
17
+ query: r.query,
18
+ domain: r.rank?.domain ?? "",
19
+ found: r.rank ? r.rank.found : "",
20
+ best: r.rank?.best ?? "",
21
+ organic: r.rank?.organic ?? [],
22
+ ads: r.rank?.ads ?? [],
23
+ results: r.results.length,
24
+ error: r.error ?? "",
25
+ }));
26
+ return toCsv(flat, COLUMNS);
27
+ }
28
+ //# sourceMappingURL=serp-csv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serp-csv.js","sourceRoot":"","sources":["../../src/lib/serp-csv.ts"],"names":[],"mappings":"AAQA,+DAA+D;AAC/D,SAAS,KAAK,CAAC,KAAc;IAC3B,MAAM,CAAC,GAAG,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9G,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,KAAK,CAAC,IAA+B,EAAE,OAAiB;IACtE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5E,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;AACjD,CAAC;AAED,oFAAoF;AACpF,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;AAE3F,yEAAyE;AACzE,MAAM,UAAU,cAAc,CAAC,IAAoB;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5B,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE;QAC5B,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACjC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE;QACxB,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE;QAC9B,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,EAAE;QACtB,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM;QACzB,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;KACrB,CAAC,CAAC,CAAC;IACJ,OAAO,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC9B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAiBvD,+DAA+D;AAC/D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,IAAI,WAAW,CAmB1C"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAkBvD,+DAA+D;AAC/D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,IAAI,WAAW,CAoB1C"}
@@ -19,6 +19,7 @@ import { registerRunTool } from "./tools/run.js";
19
19
  import { registerScreenshotTool } from "./tools/screenshot.js";
20
20
  import { registerSessionTools } from "./tools/session.js";
21
21
  import { registerSnapshotTools } from "./tools/snapshot.js";
22
+ import { registerVisualDiffTool } from "./tools/visual-diff.js";
22
23
  import { registerWaitTool } from "./tools/wait.js";
23
24
  /** Create the fuse-browser MCP server with every tool and resource wired. */
24
25
  export function createServer() {
@@ -38,6 +39,7 @@ export function createServer() {
38
39
  registerExtractTool(server, sessions);
39
40
  registerExtractSchemaTool(server, sessions);
40
41
  registerScreenshotTool(server, sessions);
42
+ registerVisualDiffTool(server, sessions);
41
43
  registerResources(server);
42
44
  return { server, sessions };
43
45
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAQnD,6EAA6E;AAC7E,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;IACtC,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvC,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtC,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvC,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACnC,qBAAqB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxC,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtC,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACnC,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAClC,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtC,yBAAyB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC5C,sBAAsB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACzC,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAQnD,6EAA6E;AAC7E,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;IACtC,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvC,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtC,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvC,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACnC,qBAAqB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxC,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtC,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACnC,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAClC,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtC,yBAAyB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC5C,sBAAsB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACzC,sBAAsB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACzC,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SessionManager } from "../../session/manager.js";
3
+ /** Register `browser_visual_diff`. */
4
+ export declare function registerVisualDiffTool(server: McpServer, sessions: SessionManager): void;
5
+ //# sourceMappingURL=visual-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visual-diff.d.ts","sourceRoot":"","sources":["../../../src/server/tools/visual-diff.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAI/D,sCAAsC;AACtC,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc,GAAG,IAAI,CAwCxF"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `browser_visual_diff` tool: pixel-compare the live page against a baseline PNG
3
+ * (created on first run), or two explicit PNG paths. Returns diff stats +
4
+ * changed-region boxes; writes the highlighted diff PNG next to the baseline.
5
+ * @module server/tools/visual-diff
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { z } from "zod";
9
+ import { writeFileBytes } from "../../lib/fs.js";
10
+ import { diffPng } from "../../lib/pixel-diff.js";
11
+ import { errorResult, jsonResult } from "../result.js";
12
+ import { withSession } from "./with-session.js";
13
+ /** Register `browser_visual_diff`. */
14
+ export function registerVisualDiffTool(server, sessions) {
15
+ server.registerTool("browser_visual_diff", {
16
+ title: "Visual diff",
17
+ description: "Pixel-compare the live page against a `baseline` PNG (created on first call, then diffed on later calls), or two explicit PNGs (`a` + `b`). Returns diffPixels/diffRatio and changed-region boxes, and writes a highlighted diff PNG. Fails if image sizes differ. For visual regression / page-change monitoring.",
18
+ inputSchema: {
19
+ sessionId: z.string().optional(),
20
+ baseline: z.string().optional(),
21
+ a: z.string().optional(),
22
+ b: z.string().optional(),
23
+ fullPage: z.boolean().optional(),
24
+ threshold: z.number().optional(),
25
+ },
26
+ }, async (args) => {
27
+ const x = args;
28
+ const threshold = typeof x.threshold === "number" ? x.threshold : 0.1;
29
+ if (typeof x.a === "string" && typeof x.b === "string") {
30
+ const d = diffPng(readFileSync(x.a), readFileSync(x.b), threshold);
31
+ writeFileBytes(`${x.b}.diff.png`, d.diffPng);
32
+ return jsonResult({ ...stats(d), diffImage: `${x.b}.diff.png` });
33
+ }
34
+ if (typeof x.sessionId !== "string" || typeof x.baseline !== "string") {
35
+ return errorResult("browser_visual_diff needs either `a`+`b` paths, or `sessionId`+`baseline`");
36
+ }
37
+ const baseline = x.baseline;
38
+ return withSession(sessions, x.sessionId, async (s) => {
39
+ const shot = await s.page.screenshot({ fullPage: x.fullPage === true });
40
+ if (!existsSync(baseline)) {
41
+ writeFileBytes(baseline, shot);
42
+ return jsonResult({ baselineCreated: true, baseline });
43
+ }
44
+ const d = diffPng(readFileSync(baseline), shot, threshold);
45
+ writeFileBytes(`${baseline}.diff.png`, d.diffPng);
46
+ return jsonResult({ ...stats(d), baseline, diffImage: `${baseline}.diff.png` });
47
+ });
48
+ });
49
+ }
50
+ /** Serializable diff stats (without the raw PNG bytes). */
51
+ function stats(d) {
52
+ return { width: d.width, height: d.height, diffPixels: d.diffPixels, diffRatio: d.diffRatio, regions: d.regions };
53
+ }
54
+ //# sourceMappingURL=visual-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visual-diff.js","sourceRoot":"","sources":["../../../src/server/tools/visual-diff.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/browser-mcp",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "MCP server + CLI giving AI agents a real, stealth browser (Patchright/Playwright) — per-country identity, self-healing actions, snapshots, multi-step plans, structured extraction, CDP attach.",
5
5
  "license": "MIT",
6
6
  "author": "Fusengine",
@@ -54,7 +54,7 @@
54
54
  "build": "tsc -p tsconfig.json",
55
55
  "typecheck": "tsc -p tsconfig.json --noEmit",
56
56
  "test": "bun test tests/unit",
57
- "test:integration": "node --test --import tsx tests/integration/mcp.test.ts tests/integration/probe.test.ts tests/integration/snapshot.test.ts tests/integration/snapshot-frames.test.ts tests/integration/collect.test.ts tests/integration/selectors.test.ts tests/integration/run.test.ts tests/integration/extract-schema.test.ts",
57
+ "test:integration": "node --test --import tsx tests/integration/mcp.test.ts tests/integration/probe.test.ts tests/integration/snapshot.test.ts tests/integration/snapshot-frames.test.ts tests/integration/collect.test.ts tests/integration/selectors.test.ts tests/integration/visual-diff.test.ts tests/integration/run.test.ts tests/integration/extract-schema.test.ts",
58
58
  "browsers": "patchright install chromium",
59
59
  "mcp": "node --import tsx src/bin/mcp.ts",
60
60
  "cli": "node --import tsx src/bin/cli.ts"
@@ -65,9 +65,11 @@
65
65
  ],
66
66
  "dependencies": {
67
67
  "@modelcontextprotocol/sdk": "^1.29.0",
68
+ "fast-png": "^8.0.0",
68
69
  "impit": "^0.14.1",
69
70
  "linkedom": "^0.18.12",
70
71
  "patchright": "^1.60.1",
72
+ "pixelmatch": "^7.2.0",
71
73
  "playwright": "^1.60.0",
72
74
  "zod": "^4.4.3"
73
75
  },