framewatch-mcp-server 0.1.0 → 0.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/README.md +895 -15
- package/dist/constants.d.ts +274 -0
- package/dist/constants.js +279 -0
- package/dist/constants.js.map +1 -1
- package/dist/engine/browser.d.ts +20 -4
- package/dist/engine/browser.js +26 -9
- package/dist/engine/browser.js.map +1 -1
- package/dist/engine/clicks.d.ts +221 -0
- package/dist/engine/clicks.js +801 -0
- package/dist/engine/clicks.js.map +1 -0
- package/dist/engine/forms.d.ts +137 -0
- package/dist/engine/forms.js +474 -0
- package/dist/engine/forms.js.map +1 -0
- package/dist/engine/hmr.d.ts +41 -0
- package/dist/engine/hmr.js +91 -0
- package/dist/engine/hmr.js.map +1 -0
- package/dist/engine/inspect.d.ts +31 -0
- package/dist/engine/inspect.js +383 -0
- package/dist/engine/inspect.js.map +1 -0
- package/dist/engine/interaction.d.ts +12 -7
- package/dist/engine/interaction.js +110 -18
- package/dist/engine/interaction.js.map +1 -1
- package/dist/engine/links.d.ts +134 -0
- package/dist/engine/links.js +384 -0
- package/dist/engine/links.js.map +1 -0
- package/dist/engine/mocks.d.ts +53 -0
- package/dist/engine/mocks.js +148 -0
- package/dist/engine/mocks.js.map +1 -0
- package/dist/engine/rtl.d.ts +129 -0
- package/dist/engine/rtl.js +540 -0
- package/dist/engine/rtl.js.map +1 -0
- package/dist/engine/seo.d.ts +189 -0
- package/dist/engine/seo.js +398 -0
- package/dist/engine/seo.js.map +1 -0
- package/dist/engine/snapshot.d.ts +29 -0
- package/dist/engine/snapshot.js +10 -0
- package/dist/engine/snapshot.js.map +1 -0
- package/dist/engine/vue.d.ts +54 -0
- package/dist/engine/vue.js +419 -0
- package/dist/engine/vue.js.map +1 -0
- package/dist/index.js +45 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/accessibility.d.ts +4 -0
- package/dist/tools/accessibility.js +9 -2
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/api-mock.d.ts +405 -0
- package/dist/tools/api-mock.js +186 -0
- package/dist/tools/api-mock.js.map +1 -0
- package/dist/tools/capture.d.ts +90 -26
- package/dist/tools/capture.js +109 -58
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/compare.d.ts +4 -0
- package/dist/tools/compare.js +16 -5
- package/dist/tools/compare.js.map +1 -1
- package/dist/tools/dead-clicks.d.ts +128 -0
- package/dist/tools/dead-clicks.js +570 -0
- package/dist/tools/dead-clicks.js.map +1 -0
- package/dist/tools/form-test.d.ts +112 -0
- package/dist/tools/form-test.js +477 -0
- package/dist/tools/form-test.js.map +1 -0
- package/dist/tools/index.d.ts +17 -1
- package/dist/tools/index.js +45 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inspect.d.ts +78 -0
- package/dist/tools/inspect.js +136 -0
- package/dist/tools/inspect.js.map +1 -0
- package/dist/tools/interact.d.ts +37 -18
- package/dist/tools/interact.js +113 -13
- package/dist/tools/interact.js.map +1 -1
- package/dist/tools/links.d.ts +129 -0
- package/dist/tools/links.js +640 -0
- package/dist/tools/links.js.map +1 -0
- package/dist/tools/responsive.d.ts +10 -6
- package/dist/tools/responsive.js +21 -4
- package/dist/tools/responsive.js.map +1 -1
- package/dist/tools/rtl.d.ts +241 -0
- package/dist/tools/rtl.js +410 -0
- package/dist/tools/rtl.js.map +1 -0
- package/dist/tools/save-auth.d.ts +263 -0
- package/dist/tools/save-auth.js +253 -0
- package/dist/tools/save-auth.js.map +1 -0
- package/dist/tools/screenshot.d.ts +4 -0
- package/dist/tools/screenshot.js +15 -4
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/seo.d.ts +113 -0
- package/dist/tools/seo.js +281 -0
- package/dist/tools/seo.js.map +1 -0
- package/dist/tools/snapshot.d.ts +122 -0
- package/dist/tools/snapshot.js +183 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/tools/wait-for.d.ts +107 -0
- package/dist/tools/wait-for.js +167 -0
- package/dist/tools/wait-for.js.map +1 -0
- package/dist/utils/arabic-text.d.ts +14 -0
- package/dist/utils/arabic-text.js +193 -0
- package/dist/utils/arabic-text.js.map +1 -0
- package/dist/utils/budget.d.ts +41 -0
- package/dist/utils/budget.js +182 -0
- package/dist/utils/budget.js.map +1 -0
- package/dist/utils/format.d.ts +11 -1
- package/dist/utils/format.js +27 -4
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/highlight.d.ts +69 -0
- package/dist/utils/highlight.js +181 -0
- package/dist/utils/highlight.js.map +1 -0
- package/dist/utils/link-rules.d.ts +100 -0
- package/dist/utils/link-rules.js +284 -0
- package/dist/utils/link-rules.js.map +1 -0
- package/dist/utils/mock-rules.d.ts +144 -0
- package/dist/utils/mock-rules.js +224 -0
- package/dist/utils/mock-rules.js.map +1 -0
- package/dist/utils/rtl-rules.d.ts +142 -0
- package/dist/utils/rtl-rules.js +296 -0
- package/dist/utils/rtl-rules.js.map +1 -0
- package/dist/utils/seo-rules.d.ts +129 -0
- package/dist/utils/seo-rules.js +726 -0
- package/dist/utils/seo-rules.js.map +1 -0
- package/dist/utils/snapshot-rules.d.ts +33 -0
- package/dist/utils/snapshot-rules.js +111 -0
- package/dist/utils/snapshot-rules.js.map +1 -0
- package/dist/utils/storage-state.d.ts +76 -0
- package/dist/utils/storage-state.js +195 -0
- package/dist/utils/storage-state.js.map +1 -0
- package/dist/utils/style-rules.d.ts +107 -0
- package/dist/utils/style-rules.js +223 -0
- package/dist/utils/style-rules.js.map +1 -0
- package/dist/utils/test-data.d.ts +75 -0
- package/dist/utils/test-data.js +294 -0
- package/dist/utils/test-data.js.map +1 -0
- package/dist/utils/vue-rules.d.ts +72 -0
- package/dist/utils/vue-rules.js +108 -0
- package/dist/utils/vue-rules.js.map +1 -0
- package/package.json +6 -4
package/dist/tools/capture.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import type { BrowserContext, Page } from "playwright";
|
|
2
3
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
import { summariseContext } from "../engine/layers/index.js";
|
|
5
|
+
import { summariseContext, type CapturedContext } from "../engine/layers/index.js";
|
|
6
|
+
import { type InteractionReport } from "../utils/format.js";
|
|
7
|
+
import type { DiffCard, Viewport } from "../types.js";
|
|
8
|
+
import { type ResolvedAuth } from "../utils/storage-state.js";
|
|
5
9
|
export declare const CAPTURE_TOOL_NAME = "framewatch_capture";
|
|
6
10
|
/**
|
|
7
11
|
* Re-exported where it is used: the capture summary's context lines. It lives
|
|
@@ -21,10 +25,10 @@ export declare const captureInteractionSchema: z.ZodEffects<z.ZodObject<{
|
|
|
21
25
|
y: z.ZodOptional<z.ZodNumber>;
|
|
22
26
|
delta_x: z.ZodOptional<z.ZodNumber>;
|
|
23
27
|
delta_y: z.ZodOptional<z.ZodNumber>;
|
|
24
|
-
action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "wait", "navigate"]>;
|
|
28
|
+
action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
|
|
25
29
|
}, "strip", z.ZodTypeAny, {
|
|
26
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
27
30
|
delay_ms: number;
|
|
31
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
28
32
|
value?: string | undefined;
|
|
29
33
|
x?: number | undefined;
|
|
30
34
|
y?: number | undefined;
|
|
@@ -32,17 +36,17 @@ export declare const captureInteractionSchema: z.ZodEffects<z.ZodObject<{
|
|
|
32
36
|
delta_x?: number | undefined;
|
|
33
37
|
delta_y?: number | undefined;
|
|
34
38
|
}, {
|
|
35
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
39
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
36
40
|
value?: string | undefined;
|
|
41
|
+
delay_ms?: number | undefined;
|
|
37
42
|
x?: number | undefined;
|
|
38
43
|
y?: number | undefined;
|
|
39
44
|
selector?: string | undefined;
|
|
40
45
|
delta_x?: number | undefined;
|
|
41
46
|
delta_y?: number | undefined;
|
|
42
|
-
delay_ms?: number | undefined;
|
|
43
47
|
}>, {
|
|
44
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
45
48
|
delay_ms: number;
|
|
49
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
46
50
|
value?: string | undefined;
|
|
47
51
|
x?: number | undefined;
|
|
48
52
|
y?: number | undefined;
|
|
@@ -50,14 +54,14 @@ export declare const captureInteractionSchema: z.ZodEffects<z.ZodObject<{
|
|
|
50
54
|
delta_x?: number | undefined;
|
|
51
55
|
delta_y?: number | undefined;
|
|
52
56
|
}, {
|
|
53
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
57
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
54
58
|
value?: string | undefined;
|
|
59
|
+
delay_ms?: number | undefined;
|
|
55
60
|
x?: number | undefined;
|
|
56
61
|
y?: number | undefined;
|
|
57
62
|
selector?: string | undefined;
|
|
58
63
|
delta_x?: number | undefined;
|
|
59
64
|
delta_y?: number | undefined;
|
|
60
|
-
delay_ms?: number | undefined;
|
|
61
65
|
}>;
|
|
62
66
|
export declare const captureInputShape: {
|
|
63
67
|
url: z.ZodString;
|
|
@@ -85,10 +89,10 @@ export declare const captureInputShape: {
|
|
|
85
89
|
y: z.ZodOptional<z.ZodNumber>;
|
|
86
90
|
delta_x: z.ZodOptional<z.ZodNumber>;
|
|
87
91
|
delta_y: z.ZodOptional<z.ZodNumber>;
|
|
88
|
-
action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "wait", "navigate"]>;
|
|
92
|
+
action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
|
|
89
93
|
}, "strip", z.ZodTypeAny, {
|
|
90
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
91
94
|
delay_ms: number;
|
|
95
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
92
96
|
value?: string | undefined;
|
|
93
97
|
x?: number | undefined;
|
|
94
98
|
y?: number | undefined;
|
|
@@ -96,17 +100,17 @@ export declare const captureInputShape: {
|
|
|
96
100
|
delta_x?: number | undefined;
|
|
97
101
|
delta_y?: number | undefined;
|
|
98
102
|
}, {
|
|
99
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
103
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
100
104
|
value?: string | undefined;
|
|
105
|
+
delay_ms?: number | undefined;
|
|
101
106
|
x?: number | undefined;
|
|
102
107
|
y?: number | undefined;
|
|
103
108
|
selector?: string | undefined;
|
|
104
109
|
delta_x?: number | undefined;
|
|
105
110
|
delta_y?: number | undefined;
|
|
106
|
-
delay_ms?: number | undefined;
|
|
107
111
|
}>, {
|
|
108
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
109
112
|
delay_ms: number;
|
|
113
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
110
114
|
value?: string | undefined;
|
|
111
115
|
x?: number | undefined;
|
|
112
116
|
y?: number | undefined;
|
|
@@ -114,20 +118,21 @@ export declare const captureInputShape: {
|
|
|
114
118
|
delta_x?: number | undefined;
|
|
115
119
|
delta_y?: number | undefined;
|
|
116
120
|
}, {
|
|
117
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
121
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
118
122
|
value?: string | undefined;
|
|
123
|
+
delay_ms?: number | undefined;
|
|
119
124
|
x?: number | undefined;
|
|
120
125
|
y?: number | undefined;
|
|
121
126
|
selector?: string | undefined;
|
|
122
127
|
delta_x?: number | undefined;
|
|
123
128
|
delta_y?: number | undefined;
|
|
124
|
-
delay_ms?: number | undefined;
|
|
125
129
|
}>, "many">>;
|
|
126
130
|
interaction_timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
127
131
|
include_console: z.ZodDefault<z.ZodBoolean>;
|
|
128
132
|
include_network: z.ZodDefault<z.ZodBoolean>;
|
|
129
133
|
include_dom: z.ZodDefault<z.ZodBoolean>;
|
|
130
134
|
include_performance: z.ZodDefault<z.ZodBoolean>;
|
|
135
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
131
136
|
};
|
|
132
137
|
export declare const captureInputSchema: z.ZodObject<{
|
|
133
138
|
url: z.ZodString;
|
|
@@ -155,10 +160,10 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
155
160
|
y: z.ZodOptional<z.ZodNumber>;
|
|
156
161
|
delta_x: z.ZodOptional<z.ZodNumber>;
|
|
157
162
|
delta_y: z.ZodOptional<z.ZodNumber>;
|
|
158
|
-
action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "wait", "navigate"]>;
|
|
163
|
+
action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
|
|
159
164
|
}, "strip", z.ZodTypeAny, {
|
|
160
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
161
165
|
delay_ms: number;
|
|
166
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
162
167
|
value?: string | undefined;
|
|
163
168
|
x?: number | undefined;
|
|
164
169
|
y?: number | undefined;
|
|
@@ -166,17 +171,17 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
166
171
|
delta_x?: number | undefined;
|
|
167
172
|
delta_y?: number | undefined;
|
|
168
173
|
}, {
|
|
169
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
174
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
170
175
|
value?: string | undefined;
|
|
176
|
+
delay_ms?: number | undefined;
|
|
171
177
|
x?: number | undefined;
|
|
172
178
|
y?: number | undefined;
|
|
173
179
|
selector?: string | undefined;
|
|
174
180
|
delta_x?: number | undefined;
|
|
175
181
|
delta_y?: number | undefined;
|
|
176
|
-
delay_ms?: number | undefined;
|
|
177
182
|
}>, {
|
|
178
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
179
183
|
delay_ms: number;
|
|
184
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
180
185
|
value?: string | undefined;
|
|
181
186
|
x?: number | undefined;
|
|
182
187
|
y?: number | undefined;
|
|
@@ -184,20 +189,21 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
184
189
|
delta_x?: number | undefined;
|
|
185
190
|
delta_y?: number | undefined;
|
|
186
191
|
}, {
|
|
187
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
192
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
188
193
|
value?: string | undefined;
|
|
194
|
+
delay_ms?: number | undefined;
|
|
189
195
|
x?: number | undefined;
|
|
190
196
|
y?: number | undefined;
|
|
191
197
|
selector?: string | undefined;
|
|
192
198
|
delta_x?: number | undefined;
|
|
193
199
|
delta_y?: number | undefined;
|
|
194
|
-
delay_ms?: number | undefined;
|
|
195
200
|
}>, "many">>;
|
|
196
201
|
interaction_timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
197
202
|
include_console: z.ZodDefault<z.ZodBoolean>;
|
|
198
203
|
include_network: z.ZodDefault<z.ZodBoolean>;
|
|
199
204
|
include_dom: z.ZodDefault<z.ZodBoolean>;
|
|
200
205
|
include_performance: z.ZodDefault<z.ZodBoolean>;
|
|
206
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
201
207
|
}, "strip", z.ZodTypeAny, {
|
|
202
208
|
url: string;
|
|
203
209
|
wait_for_timeout_ms: number;
|
|
@@ -215,9 +221,10 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
215
221
|
height: number;
|
|
216
222
|
} | undefined;
|
|
217
223
|
wait_for?: string | undefined;
|
|
224
|
+
storage_state?: string | undefined;
|
|
218
225
|
interactions?: {
|
|
219
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
220
226
|
delay_ms: number;
|
|
227
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
221
228
|
value?: string | undefined;
|
|
222
229
|
x?: number | undefined;
|
|
223
230
|
y?: number | undefined;
|
|
@@ -233,19 +240,20 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
233
240
|
} | undefined;
|
|
234
241
|
wait_for?: string | undefined;
|
|
235
242
|
wait_for_timeout_ms?: number | undefined;
|
|
243
|
+
storage_state?: string | undefined;
|
|
236
244
|
duration_ms?: number | undefined;
|
|
237
245
|
sensitivity?: number | undefined;
|
|
238
246
|
max_frames?: number | undefined;
|
|
239
247
|
interval_ms?: number | undefined;
|
|
240
248
|
interactions?: {
|
|
241
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
249
|
+
action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
242
250
|
value?: string | undefined;
|
|
251
|
+
delay_ms?: number | undefined;
|
|
243
252
|
x?: number | undefined;
|
|
244
253
|
y?: number | undefined;
|
|
245
254
|
selector?: string | undefined;
|
|
246
255
|
delta_x?: number | undefined;
|
|
247
256
|
delta_y?: number | undefined;
|
|
248
|
-
delay_ms?: number | undefined;
|
|
249
257
|
}[] | undefined;
|
|
250
258
|
interaction_timeout_ms?: number | undefined;
|
|
251
259
|
include_console?: boolean | undefined;
|
|
@@ -254,7 +262,61 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
254
262
|
include_performance?: boolean | undefined;
|
|
255
263
|
}>;
|
|
256
264
|
export type CaptureInput = z.input<typeof captureInputSchema>;
|
|
257
|
-
type ParsedCaptureInput = z.output<typeof captureInputSchema>;
|
|
265
|
+
export type ParsedCaptureInput = z.output<typeof captureInputSchema>;
|
|
266
|
+
/**
|
|
267
|
+
* Hooks a caller can run inside a capture session.
|
|
268
|
+
*
|
|
269
|
+
* `framewatch_api_mock` uses `prepare` to install its request routes before
|
|
270
|
+
* anything navigates; `framewatch_capture` itself passes none.
|
|
271
|
+
*/
|
|
272
|
+
export interface CaptureHooks {
|
|
273
|
+
/**
|
|
274
|
+
* Run against the page and its context before the layers are attached and
|
|
275
|
+
* before the navigation. Anything that has to be in place for the very first
|
|
276
|
+
* request the page makes belongs here.
|
|
277
|
+
*/
|
|
278
|
+
prepare?: (page: Page, context: BrowserContext) => Promise<void>;
|
|
279
|
+
/**
|
|
280
|
+
* Run once the recording has stopped, while the page is still open. For
|
|
281
|
+
* releasing anything `prepare` installed that would otherwise outlive the
|
|
282
|
+
* call — a route still sleeping out a delay has nothing left to answer.
|
|
283
|
+
* A failure here is swallowed: the frames have already been taken.
|
|
284
|
+
*/
|
|
285
|
+
finish?: (page: Page, context: BrowserContext) => Promise<void>;
|
|
286
|
+
}
|
|
287
|
+
/** Everything one capture session produced, before anything formats it. */
|
|
288
|
+
export interface CaptureRun {
|
|
289
|
+
cards: DiffCard[];
|
|
290
|
+
/** Raw frames the recorder took, before the differ chose between them. */
|
|
291
|
+
total_frames: number;
|
|
292
|
+
duration_ms: number;
|
|
293
|
+
/** URL that was requested. */
|
|
294
|
+
url: string;
|
|
295
|
+
/** URL the page ended on, when it could be read. */
|
|
296
|
+
final_url?: string;
|
|
297
|
+
title?: string;
|
|
298
|
+
/** Frames the recorder had to drop (screenshot failures). */
|
|
299
|
+
dropped: number;
|
|
300
|
+
context: CapturedContext;
|
|
301
|
+
/** Present only when an interaction script was replayed. */
|
|
302
|
+
interactions?: InteractionReport;
|
|
303
|
+
/** The viewport the frames were recorded at. */
|
|
304
|
+
viewport: Viewport;
|
|
305
|
+
/** The saved auth that was applied, if any, and whether the page still showed a login form afterwards. */
|
|
306
|
+
auth: ResolvedAuth | null;
|
|
307
|
+
login_visible: boolean;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Run one capture session: navigate, record, replay the script, diff.
|
|
311
|
+
*
|
|
312
|
+
* Throws on failure rather than returning an error result — each caller has
|
|
313
|
+
* its own wording for what went wrong (see `describeCaptureFailure`).
|
|
314
|
+
*
|
|
315
|
+
* Shared with `framewatch_api_mock`, which needs this exact flow with its
|
|
316
|
+
* routes installed first. One navigation, one recorder, one differ, and one
|
|
317
|
+
* place to fix any of them.
|
|
318
|
+
*/
|
|
319
|
+
export declare function runCapture(input: ParsedCaptureInput, hooks?: CaptureHooks): Promise<CaptureRun>;
|
|
258
320
|
/**
|
|
259
321
|
* The main FrameWatch tool: record a page for `duration_ms` and return only
|
|
260
322
|
* the meaningful visual changes as diff cards (see CLAUDE.md "Diff Cards" and
|
|
@@ -269,6 +331,8 @@ type ParsedCaptureInput = z.output<typeof captureInputSchema>;
|
|
|
269
331
|
* useful message.
|
|
270
332
|
*/
|
|
271
333
|
export declare function capturePage(rawInput: CaptureInput): Promise<CallToolResult>;
|
|
334
|
+
/** The auth line for a run's summary, when there is one (see `describeAuth`). */
|
|
335
|
+
export declare function authLines(run: Pick<CaptureRun, "auth" | "login_visible">): string[];
|
|
272
336
|
/**
|
|
273
337
|
* Turn a Playwright/Node error into a one-line, actionable message. Mirrors
|
|
274
338
|
* `describeFailure` in screenshot.ts: matches on the failing Playwright call
|
package/dist/tools/capture.js
CHANGED
|
@@ -6,6 +6,7 @@ import { applyContext, attachLayers, summariseContext } from "../engine/layers/i
|
|
|
6
6
|
import { CAPTURE_ACTIONS, describeInteraction, executeInteraction, interactionFieldShape, needsTouch, refineInteraction, } from "../engine/interaction.js";
|
|
7
7
|
import { recordFrames } from "../engine/recorder.js";
|
|
8
8
|
import { formatDiffCards } from "../utils/format.js";
|
|
9
|
+
import { describeAuth, loginFormVisible, resolveStorageState, storageStateField } from "../utils/storage-state.js";
|
|
9
10
|
export const CAPTURE_TOOL_NAME = "framewatch_capture";
|
|
10
11
|
/**
|
|
11
12
|
* Re-exported where it is used: the capture summary's context lines. It lives
|
|
@@ -19,7 +20,9 @@ export { summariseContext };
|
|
|
19
20
|
*/
|
|
20
21
|
export const captureInteractionSchema = z
|
|
21
22
|
.object({
|
|
22
|
-
action: z
|
|
23
|
+
action: z
|
|
24
|
+
.enum(CAPTURE_ACTIONS)
|
|
25
|
+
.describe("What to do: click, tap, type, key, scroll, swipe, hover, select, wait or navigate"),
|
|
23
26
|
...interactionFieldShape,
|
|
24
27
|
delay_ms: z.number().int().min(0).default(0).describe("Wait this long (ms) before performing this action"),
|
|
25
28
|
})
|
|
@@ -71,7 +74,8 @@ export const captureInputShape = {
|
|
|
71
74
|
.array(captureInteractionSchema)
|
|
72
75
|
.max(MAX_INTERACTIONS)
|
|
73
76
|
.optional()
|
|
74
|
-
.describe("Sequence of user interactions to replay during the recording"
|
|
77
|
+
.describe("Sequence of user interactions to replay during the recording. Selectors may be CSS or Playwright's " +
|
|
78
|
+
'role form, e.g. role=button[name="Sign in"] — take the role and name from framewatch_snapshot'),
|
|
75
79
|
interaction_timeout_ms: z
|
|
76
80
|
.number()
|
|
77
81
|
.int()
|
|
@@ -94,8 +98,89 @@ export const captureInputShape = {
|
|
|
94
98
|
.boolean()
|
|
95
99
|
.default(false)
|
|
96
100
|
.describe("Attach paint timing, largest contentful paint and layout shifts to the frames they were measured at"),
|
|
101
|
+
storage_state: storageStateField,
|
|
97
102
|
};
|
|
98
103
|
export const captureInputSchema = z.object(captureInputShape);
|
|
104
|
+
/**
|
|
105
|
+
* Run one capture session: navigate, record, replay the script, diff.
|
|
106
|
+
*
|
|
107
|
+
* Throws on failure rather than returning an error result — each caller has
|
|
108
|
+
* its own wording for what went wrong (see `describeCaptureFailure`).
|
|
109
|
+
*
|
|
110
|
+
* Shared with `framewatch_api_mock`, which needs this exact flow with its
|
|
111
|
+
* routes installed first. One navigation, one recorder, one differ, and one
|
|
112
|
+
* place to fix any of them.
|
|
113
|
+
*/
|
|
114
|
+
export async function runCapture(input, hooks = {}) {
|
|
115
|
+
const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };
|
|
116
|
+
const interactions = input.interactions ?? [];
|
|
117
|
+
// Touch has to be decided when the context is created, so read it off the
|
|
118
|
+
// script rather than turning it on for every capture: `hasTouch` puts
|
|
119
|
+
// `ontouchstart` on window, which flips feature detection in most UI
|
|
120
|
+
// libraries and would silently change what a plain capture records.
|
|
121
|
+
const contextOptions = needsTouch(interactions) ? { hasTouch: true } : {};
|
|
122
|
+
const report = interactions.length > 0 ? { total: interactions.length, completed: 0, steps: [] } : undefined;
|
|
123
|
+
const auth = await resolveStorageState(input.storage_state);
|
|
124
|
+
if (auth)
|
|
125
|
+
contextOptions.storageState = auth.state;
|
|
126
|
+
const recording = await withPage({ viewport, contextOptions }, async (page, context) => {
|
|
127
|
+
// Before anything else, including the layers: whatever the caller needs in
|
|
128
|
+
// place for the page's very first request.
|
|
129
|
+
if (hooks.prepare)
|
|
130
|
+
await hooks.prepare(page, context);
|
|
131
|
+
// Layers go on before the navigation: a script that throws while the page
|
|
132
|
+
// loads, the request that never comes back and first paint all happen
|
|
133
|
+
// before frame 0 exists, and they are the most useful things here.
|
|
134
|
+
const layers = await attachLayers(page, {
|
|
135
|
+
console: input.include_console,
|
|
136
|
+
network: input.include_network,
|
|
137
|
+
dom: input.include_dom,
|
|
138
|
+
performance: input.include_performance,
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
// "commit" rather than "load": splash/loading animations must be recorded from the very start.
|
|
142
|
+
await page.goto(input.url, { waitUntil: "commit", timeout: NAVIGATION_TIMEOUT_MS });
|
|
143
|
+
if (input.wait_for) {
|
|
144
|
+
await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms });
|
|
145
|
+
}
|
|
146
|
+
const result = await recordFrames(page, { duration_ms: input.duration_ms, interval_ms: input.interval_ms }, report && ((recorder) => replayScript(page, recorder, interactions, input.interaction_timeout_ms, report)));
|
|
147
|
+
// Drain the layers before anything else can close the page. `collect`
|
|
148
|
+
// reads only what Node already holds, so a page that froze, navigated
|
|
149
|
+
// away or crashed still yields everything it managed to report.
|
|
150
|
+
const collected = layers.collect(result.started_at);
|
|
151
|
+
// The page may have died mid-recording (crash, closed browser). Frames
|
|
152
|
+
// already captured are still worth returning, so never let reading the
|
|
153
|
+
// title or url turn a partial recording into an error.
|
|
154
|
+
// A login form on the page after restoring a session is the expiry signal.
|
|
155
|
+
const loginVisible = auth ? await loginFormVisible(page) : false;
|
|
156
|
+
return { ...result, context: collected, loginVisible, ...(await describePage(page)) };
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
layers.detach();
|
|
160
|
+
if (hooks.finish)
|
|
161
|
+
await hooks.finish(page, context).catch(() => { });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
const { cards, total_frames } = await buildDiffCards(recording.frames, {
|
|
165
|
+
sensitivity: input.sensitivity,
|
|
166
|
+
max_frames: input.max_frames,
|
|
167
|
+
});
|
|
168
|
+
applyContext(cards, recording.context);
|
|
169
|
+
return {
|
|
170
|
+
cards,
|
|
171
|
+
total_frames,
|
|
172
|
+
duration_ms: recording.duration_ms,
|
|
173
|
+
url: input.url,
|
|
174
|
+
final_url: recording.finalUrl,
|
|
175
|
+
title: recording.title,
|
|
176
|
+
dropped: recording.dropped,
|
|
177
|
+
context: recording.context,
|
|
178
|
+
interactions: report,
|
|
179
|
+
viewport,
|
|
180
|
+
auth,
|
|
181
|
+
login_visible: recording.loginVisible,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
99
184
|
/**
|
|
100
185
|
* The main FrameWatch tool: record a page for `duration_ms` and return only
|
|
101
186
|
* the meaningful visual changes as diff cards (see CLAUDE.md "Diff Cards" and
|
|
@@ -116,65 +201,30 @@ export async function capturePage(rawInput) {
|
|
|
116
201
|
return errorResult(`Capture failed: invalid input — ${issues}`);
|
|
117
202
|
}
|
|
118
203
|
const input = parsed.data;
|
|
119
|
-
|
|
120
|
-
const interactions = input.interactions ?? [];
|
|
121
|
-
// Touch has to be decided when the context is created, so read it off the
|
|
122
|
-
// script rather than turning it on for every capture: `hasTouch` puts
|
|
123
|
-
// `ontouchstart` on window, which flips feature detection in most UI
|
|
124
|
-
// libraries and would silently change what a plain capture records.
|
|
125
|
-
const contextOptions = needsTouch(interactions) ? { hasTouch: true } : {};
|
|
126
|
-
const report = interactions.length > 0 ? { total: interactions.length, completed: 0, steps: [] } : undefined;
|
|
204
|
+
let run;
|
|
127
205
|
try {
|
|
128
|
-
|
|
129
|
-
// Layers go on before the navigation: a script that throws while the page
|
|
130
|
-
// loads, the request that never comes back and first paint all happen
|
|
131
|
-
// before frame 0 exists, and they are the most useful things here.
|
|
132
|
-
const layers = await attachLayers(page, {
|
|
133
|
-
console: input.include_console,
|
|
134
|
-
network: input.include_network,
|
|
135
|
-
dom: input.include_dom,
|
|
136
|
-
performance: input.include_performance,
|
|
137
|
-
});
|
|
138
|
-
try {
|
|
139
|
-
// "commit" rather than "load": splash/loading animations must be recorded from the very start.
|
|
140
|
-
await page.goto(input.url, { waitUntil: "commit", timeout: NAVIGATION_TIMEOUT_MS });
|
|
141
|
-
if (input.wait_for) {
|
|
142
|
-
await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms });
|
|
143
|
-
}
|
|
144
|
-
const result = await recordFrames(page, { duration_ms: input.duration_ms, interval_ms: input.interval_ms }, report && ((recorder) => replayScript(page, recorder, interactions, input.interaction_timeout_ms, report)));
|
|
145
|
-
// Drain the layers before anything else can close the page. `collect`
|
|
146
|
-
// reads only what Node already holds, so a page that froze, navigated
|
|
147
|
-
// away or crashed still yields everything it managed to report.
|
|
148
|
-
const context = layers.collect(result.started_at);
|
|
149
|
-
// The page may have died mid-recording (crash, closed browser). Frames
|
|
150
|
-
// already captured are still worth returning, so never let reading the
|
|
151
|
-
// title or url turn a partial recording into an error.
|
|
152
|
-
return { ...result, context, ...(await describePage(page)) };
|
|
153
|
-
}
|
|
154
|
-
finally {
|
|
155
|
-
layers.detach();
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
const { cards, total_frames } = await buildDiffCards(recording.frames, {
|
|
159
|
-
sensitivity: input.sensitivity,
|
|
160
|
-
max_frames: input.max_frames,
|
|
161
|
-
});
|
|
162
|
-
applyContext(cards, recording.context);
|
|
163
|
-
return formatDiffCards({
|
|
164
|
-
cards,
|
|
165
|
-
total_frames,
|
|
166
|
-
duration_ms: recording.duration_ms,
|
|
167
|
-
url: input.url,
|
|
168
|
-
final_url: recording.finalUrl,
|
|
169
|
-
title: recording.title,
|
|
170
|
-
dropped: recording.dropped,
|
|
171
|
-
interactions: report,
|
|
172
|
-
notes: summariseContext(recording.context, cards.length),
|
|
173
|
-
});
|
|
206
|
+
run = await runCapture(input);
|
|
174
207
|
}
|
|
175
208
|
catch (error) {
|
|
176
209
|
return errorResult(describeCaptureFailure(input, error));
|
|
177
210
|
}
|
|
211
|
+
return formatDiffCards({
|
|
212
|
+
cards: run.cards,
|
|
213
|
+
total_frames: run.total_frames,
|
|
214
|
+
duration_ms: run.duration_ms,
|
|
215
|
+
url: run.url,
|
|
216
|
+
final_url: run.final_url,
|
|
217
|
+
title: run.title,
|
|
218
|
+
dropped: run.dropped,
|
|
219
|
+
interactions: run.interactions,
|
|
220
|
+
viewport: run.viewport,
|
|
221
|
+
notes: [...(summariseContext(run.context, run.cards.length) ?? []), ...authLines(run)],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/** The auth line for a run's summary, when there is one (see `describeAuth`). */
|
|
225
|
+
export function authLines(run) {
|
|
226
|
+
const line = run.auth ? describeAuth(run.auth, run.login_visible) : null;
|
|
227
|
+
return line ? [line] : [];
|
|
178
228
|
}
|
|
179
229
|
/**
|
|
180
230
|
* Replay the interaction script while the recorder runs, forcing a frame after
|
|
@@ -189,8 +239,9 @@ export async function capturePage(rawInput) {
|
|
|
189
239
|
async function replayScript(page, recorder, interactions, timeoutMs, report) {
|
|
190
240
|
for (let i = 0; i < interactions.length; i++) {
|
|
191
241
|
const step = interactions[i];
|
|
242
|
+
let outcome;
|
|
192
243
|
try {
|
|
193
|
-
await executeInteraction(page, step, { timeout_ms: timeoutMs });
|
|
244
|
+
outcome = await executeInteraction(page, step, { timeout_ms: timeoutMs });
|
|
194
245
|
}
|
|
195
246
|
catch (error) {
|
|
196
247
|
report.error = error instanceof Error ? error.message : String(error);
|
|
@@ -199,7 +250,7 @@ async function replayScript(page, recorder, interactions, timeoutMs, report) {
|
|
|
199
250
|
return;
|
|
200
251
|
}
|
|
201
252
|
report.completed++;
|
|
202
|
-
report.steps.push(describeInteraction(step));
|
|
253
|
+
report.steps.push(describeInteraction(step) + (outcome.note ?? ""));
|
|
203
254
|
await recorder.captureNow("interaction");
|
|
204
255
|
}
|
|
205
256
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/tools/capture.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { Page } from \"playwright\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n CAPTURE_INTERVAL_MS,\n DEFAULT_CAPTURE_DURATION_MS,\n DEFAULT_MAX_FRAMES,\n DEFAULT_SENSITIVITY,\n DEFAULT_VIEWPORT,\n MAX_CAPTURE_DURATION_MS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n MAX_FRAMES_CAP,\n MAX_INTERACTIONS,\n MIN_CAPTURE_DURATION_MS,\n NAVIGATION_TIMEOUT_MS,\n PAGE_INFO_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\nimport { buildDiffCards } from \"../engine/differ.js\";\nimport { applyContext, attachLayers, summariseContext } from \"../engine/layers/index.js\";\nimport {\n CAPTURE_ACTIONS,\n describeInteraction,\n executeInteraction,\n interactionFieldShape,\n needsTouch,\n refineInteraction,\n type Interaction,\n} from \"../engine/interaction.js\";\nimport { recordFrames, type FrameRecorder } from \"../engine/recorder.js\";\nimport { formatDiffCards, type InteractionReport } from \"../utils/format.js\";\n\nexport const CAPTURE_TOOL_NAME = \"framewatch_capture\";\n\n/**\n * Re-exported where it is used: the capture summary's context lines. It lives\n * with the layers themselves now that `framewatch_interact` reports them too.\n */\nexport { summariseContext };\n\n/**\n * One step of a replayable interaction script. `delay_ms` is a wait *before*\n * the action, so a `wait` step is just a delay with nothing after it, and the\n * delays of successive steps accumulate over the recording.\n */\nexport const captureInteractionSchema = z\n .object({\n action: z.enum(CAPTURE_ACTIONS).describe(\"What to do: click, tap, type, scroll, swipe, wait or navigate\"),\n ...interactionFieldShape,\n delay_ms: z.number().int().min(0).default(0).describe(\"Wait this long (ms) before performing this action\"),\n })\n .superRefine(refineInteraction);\n\nexport const captureInputShape = {\n url: z.string().url().describe(\"URL to capture, e.g. http://localhost:3000 (http, https and file URLs are accepted)\"),\n duration_ms: z\n .number()\n .int()\n .min(MIN_CAPTURE_DURATION_MS)\n .max(MAX_CAPTURE_DURATION_MS)\n .default(DEFAULT_CAPTURE_DURATION_MS)\n .describe(\"How long to record (ms)\"),\n sensitivity: z\n .number()\n .min(0)\n .max(1)\n .default(DEFAULT_SENSITIVITY)\n .describe(\"Frame change threshold (0 = keep all, 1 = keep none). 0.06 means ~6% of grid cells must change.\"),\n max_frames: z\n .number()\n .int()\n .min(1)\n .max(MAX_FRAMES_CAP)\n .default(DEFAULT_MAX_FRAMES)\n .describe(\"Maximum diff cards to return\"),\n interval_ms: z\n .number()\n .int()\n .min(16)\n .max(2000)\n .default(CAPTURE_INTERVAL_MS)\n .describe(\"Raw frame capture interval (ms) — 100 = 10fps\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH).default(DEFAULT_VIEWPORT.width),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT).default(DEFAULT_VIEWPORT.height),\n })\n .optional()\n .describe(\"Viewport size (defaults to 1280x720)\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) before recording starts\"),\n wait_for_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for `wait_for` to appear (must be > 0)\"),\n interactions: z\n .array(captureInteractionSchema)\n .max(MAX_INTERACTIONS)\n .optional()\n .describe(\"Sequence of user interactions to replay during the recording\"),\n interaction_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) an interaction step may spend waiting for its target element\"),\n include_console: z\n .boolean()\n .default(true)\n .describe(\"Attach console logs, uncaught errors and unhandled rejections to the frames they happened between\"),\n include_network: z\n .boolean()\n .default(false)\n .describe(\"Attach network requests (method, url, status, duration) to the frames they settled between\"),\n include_dom: z\n .boolean()\n .default(false)\n .describe(\"Attach a summary of the DOM mutations between frames — which elements were added, removed or restyled\"),\n include_performance: z\n .boolean()\n .default(false)\n .describe(\"Attach paint timing, largest contentful paint and layout shifts to the frames they were measured at\"),\n};\n\nexport const captureInputSchema = z.object(captureInputShape);\nexport type CaptureInput = z.input<typeof captureInputSchema>;\ntype ParsedCaptureInput = z.output<typeof captureInputSchema>;\n\n/**\n * The main FrameWatch tool: record a page for `duration_ms` and return only\n * the meaningful visual changes as diff cards (see CLAUDE.md \"Diff Cards\" and\n * \"MCP Response Format\").\n *\n * Flow: navigate (waiting only for the navigation to commit, so loading and\n * splash animations are recorded from their first frame) → optionally wait\n * for `wait_for` → record raw frames every `interval_ms`, replaying\n * `interactions` as the recording runs → smart-diff them into at most\n * `max_frames` cards → format. All failures — including invalid input — are\n * reported as `isError` results rather than thrown so the MCP client sees a\n * useful message.\n */\nexport async function capturePage(rawInput: CaptureInput): Promise<CallToolResult> {\n const parsed = captureInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Capture failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n const interactions = input.interactions ?? [];\n // Touch has to be decided when the context is created, so read it off the\n // script rather than turning it on for every capture: `hasTouch` puts\n // `ontouchstart` on window, which flips feature detection in most UI\n // libraries and would silently change what a plain capture records.\n const contextOptions = needsTouch(interactions) ? { hasTouch: true } : {};\n\n const report: InteractionReport | undefined =\n interactions.length > 0 ? { total: interactions.length, completed: 0, steps: [] } : undefined;\n\n try {\n const recording = await withPage({ viewport, contextOptions }, async (page) => {\n // Layers go on before the navigation: a script that throws while the page\n // loads, the request that never comes back and first paint all happen\n // before frame 0 exists, and they are the most useful things here.\n const layers = await attachLayers(page, {\n console: input.include_console,\n network: input.include_network,\n dom: input.include_dom,\n performance: input.include_performance,\n });\n try {\n // \"commit\" rather than \"load\": splash/loading animations must be recorded from the very start.\n await page.goto(input.url, { waitUntil: \"commit\", timeout: NAVIGATION_TIMEOUT_MS });\n if (input.wait_for) {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms });\n }\n const result = await recordFrames(\n page,\n { duration_ms: input.duration_ms, interval_ms: input.interval_ms },\n report && ((recorder) => replayScript(page, recorder, interactions, input.interaction_timeout_ms, report)),\n );\n // Drain the layers before anything else can close the page. `collect`\n // reads only what Node already holds, so a page that froze, navigated\n // away or crashed still yields everything it managed to report.\n const context = layers.collect(result.started_at);\n // The page may have died mid-recording (crash, closed browser). Frames\n // already captured are still worth returning, so never let reading the\n // title or url turn a partial recording into an error.\n return { ...result, context, ...(await describePage(page)) };\n } finally {\n layers.detach();\n }\n });\n\n const { cards, total_frames } = await buildDiffCards(recording.frames, {\n sensitivity: input.sensitivity,\n max_frames: input.max_frames,\n });\n applyContext(cards, recording.context);\n\n return formatDiffCards({\n cards,\n total_frames,\n duration_ms: recording.duration_ms,\n url: input.url,\n final_url: recording.finalUrl,\n title: recording.title,\n dropped: recording.dropped,\n interactions: report,\n notes: summariseContext(recording.context, cards.length),\n });\n } catch (error) {\n return errorResult(describeCaptureFailure(input, error));\n }\n}\n\n/**\n * Replay the interaction script while the recorder runs, forcing a frame after\n * every step so the differ always keeps the result of each action.\n *\n * A failing step ends the script but never the capture: the frames recorded so\n * far are the most useful thing FrameWatch can hand back (they show the state\n * the page was actually in when the selector did not match), so the failure is\n * recorded in `report` and a forced \"error\" frame is captured instead of\n * throwing. The recording then plays out the rest of its duration.\n */\nasync function replayScript(\n page: Page,\n recorder: FrameRecorder,\n interactions: Interaction[],\n timeoutMs: number,\n report: InteractionReport,\n): Promise<void> {\n for (let i = 0; i < interactions.length; i++) {\n const step = interactions[i];\n try {\n await executeInteraction(page, step, { timeout_ms: timeoutMs });\n } catch (error) {\n report.error = error instanceof Error ? error.message : String(error);\n report.failed_index = i + 1;\n await recorder.captureNow(\"error\");\n return;\n }\n report.completed++;\n report.steps.push(describeInteraction(step));\n await recorder.captureNow(\"interaction\");\n }\n}\n\n/**\n * Title and final url of a page. Both are cosmetic, so neither is allowed to\n * fail or delay the result: the page may have died during the recording, and\n * `page.title()` blocks for Playwright's full default timeout while the main\n * thread is busy — exactly the case a developer points this tool at.\n */\nasync function describePage(page: Page): Promise<{ title?: string; finalUrl?: string }> {\n if (page.isClosed()) return {};\n const finalUrl = safely(() => page.url());\n // Settle the losing side too: a page.title() that outlives the race would\n // otherwise reject unobserved when the context is closed underneath it.\n const title = page.title().catch(() => undefined);\n let timer: NodeJS.Timeout | undefined;\n const giveUp = new Promise<undefined>((resolve) => {\n timer = setTimeout(() => resolve(undefined), PAGE_INFO_TIMEOUT_MS);\n });\n try {\n return { title: await Promise.race([title, giveUp]), finalUrl };\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction safely<T>(read: () => T): T | undefined {\n try {\n return read();\n } catch {\n return undefined;\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * Turn a Playwright/Node error into a one-line, actionable message. Mirrors\n * `describeFailure` in screenshot.ts: matches on the failing Playwright call\n * (the message prefix) rather than on substrings of user-supplied selectors,\n * so a navigation failure is never blamed on an element.\n */\nexport function describeCaptureFailure(\n input: Pick<ParsedCaptureInput, \"url\" | \"wait_for\" | \"wait_for_timeout_ms\">,\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const prefix = `Capture of ${input.url} failed:`;\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n `${prefix} Playwright's Chromium browser is not installed. ` +\n `Run \\`npx playwright install chromium\\` and try again. (${firstLine})`\n );\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `${prefix} selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms}ms.`;\n }\n return `${prefix} ${firstLine}`;\n}\n\nexport function registerCaptureTool(server: McpServer): void {\n server.registerTool(\n CAPTURE_TOOL_NAME,\n {\n title: \"Capture\",\n description:\n \"Record a web page for a few seconds and return only the meaningful visual changes as diff cards: \" +\n \"a full-frame PNG plus a crop of the changed region and its position for each kept frame. \" +\n \"Use it to check animations, splash/loading screens, transitions and anything that changes over time. \" +\n \"Lower `sensitivity` to keep more frames; `max_frames` caps the output. \" +\n \"Each frame can also carry the context for the interval that produced it: console output and uncaught \" +\n \"errors (on by default), plus network requests, DOM mutations and paint/layout-shift timing via \" +\n \"`include_network`, `include_dom` and `include_performance` — that is how you find out *why* a frame \" +\n \"looks wrong, not just that it does.\",\n inputSchema: captureInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => capturePage(args),\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/tools/capture.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { BrowserContext, BrowserContextOptions, Page } from \"playwright\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n CAPTURE_INTERVAL_MS,\n DEFAULT_CAPTURE_DURATION_MS,\n DEFAULT_MAX_FRAMES,\n DEFAULT_SENSITIVITY,\n DEFAULT_VIEWPORT,\n MAX_CAPTURE_DURATION_MS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n MAX_FRAMES_CAP,\n MAX_INTERACTIONS,\n MIN_CAPTURE_DURATION_MS,\n NAVIGATION_TIMEOUT_MS,\n PAGE_INFO_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\nimport { buildDiffCards } from \"../engine/differ.js\";\nimport { applyContext, attachLayers, summariseContext, type CapturedContext } from \"../engine/layers/index.js\";\nimport {\n CAPTURE_ACTIONS,\n describeInteraction,\n executeInteraction,\n interactionFieldShape,\n needsTouch,\n refineInteraction,\n type Interaction,\n} from \"../engine/interaction.js\";\nimport { recordFrames, type FrameRecorder } from \"../engine/recorder.js\";\nimport { formatDiffCards, type InteractionReport } from \"../utils/format.js\";\nimport type { DiffCard, Viewport } from \"../types.js\";\nimport { describeAuth, loginFormVisible, resolveStorageState, storageStateField, type ResolvedAuth } from \"../utils/storage-state.js\";\n\nexport const CAPTURE_TOOL_NAME = \"framewatch_capture\";\n\n/**\n * Re-exported where it is used: the capture summary's context lines. It lives\n * with the layers themselves now that `framewatch_interact` reports them too.\n */\nexport { summariseContext };\n\n/**\n * One step of a replayable interaction script. `delay_ms` is a wait *before*\n * the action, so a `wait` step is just a delay with nothing after it, and the\n * delays of successive steps accumulate over the recording.\n */\nexport const captureInteractionSchema = z\n .object({\n action: z\n .enum(CAPTURE_ACTIONS)\n .describe(\"What to do: click, tap, type, key, scroll, swipe, hover, select, wait or navigate\"),\n ...interactionFieldShape,\n delay_ms: z.number().int().min(0).default(0).describe(\"Wait this long (ms) before performing this action\"),\n })\n .superRefine(refineInteraction);\n\nexport const captureInputShape = {\n url: z.string().url().describe(\"URL to capture, e.g. http://localhost:3000 (http, https and file URLs are accepted)\"),\n duration_ms: z\n .number()\n .int()\n .min(MIN_CAPTURE_DURATION_MS)\n .max(MAX_CAPTURE_DURATION_MS)\n .default(DEFAULT_CAPTURE_DURATION_MS)\n .describe(\"How long to record (ms)\"),\n sensitivity: z\n .number()\n .min(0)\n .max(1)\n .default(DEFAULT_SENSITIVITY)\n .describe(\"Frame change threshold (0 = keep all, 1 = keep none). 0.06 means ~6% of grid cells must change.\"),\n max_frames: z\n .number()\n .int()\n .min(1)\n .max(MAX_FRAMES_CAP)\n .default(DEFAULT_MAX_FRAMES)\n .describe(\"Maximum diff cards to return\"),\n interval_ms: z\n .number()\n .int()\n .min(16)\n .max(2000)\n .default(CAPTURE_INTERVAL_MS)\n .describe(\"Raw frame capture interval (ms) — 100 = 10fps\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH).default(DEFAULT_VIEWPORT.width),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT).default(DEFAULT_VIEWPORT.height),\n })\n .optional()\n .describe(\"Viewport size (defaults to 1280x720)\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) before recording starts\"),\n wait_for_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for `wait_for` to appear (must be > 0)\"),\n interactions: z\n .array(captureInteractionSchema)\n .max(MAX_INTERACTIONS)\n .optional()\n .describe(\n \"Sequence of user interactions to replay during the recording. Selectors may be CSS or Playwright's \" +\n 'role form, e.g. role=button[name=\"Sign in\"] — take the role and name from framewatch_snapshot',\n ),\n interaction_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) an interaction step may spend waiting for its target element\"),\n include_console: z\n .boolean()\n .default(true)\n .describe(\"Attach console logs, uncaught errors and unhandled rejections to the frames they happened between\"),\n include_network: z\n .boolean()\n .default(false)\n .describe(\"Attach network requests (method, url, status, duration) to the frames they settled between\"),\n include_dom: z\n .boolean()\n .default(false)\n .describe(\"Attach a summary of the DOM mutations between frames — which elements were added, removed or restyled\"),\n include_performance: z\n .boolean()\n .default(false)\n .describe(\"Attach paint timing, largest contentful paint and layout shifts to the frames they were measured at\"),\n storage_state: storageStateField,\n};\n\nexport const captureInputSchema = z.object(captureInputShape);\nexport type CaptureInput = z.input<typeof captureInputSchema>;\nexport type ParsedCaptureInput = z.output<typeof captureInputSchema>;\n\n/**\n * Hooks a caller can run inside a capture session.\n *\n * `framewatch_api_mock` uses `prepare` to install its request routes before\n * anything navigates; `framewatch_capture` itself passes none.\n */\nexport interface CaptureHooks {\n /**\n * Run against the page and its context before the layers are attached and\n * before the navigation. Anything that has to be in place for the very first\n * request the page makes belongs here.\n */\n prepare?: (page: Page, context: BrowserContext) => Promise<void>;\n /**\n * Run once the recording has stopped, while the page is still open. For\n * releasing anything `prepare` installed that would otherwise outlive the\n * call — a route still sleeping out a delay has nothing left to answer.\n * A failure here is swallowed: the frames have already been taken.\n */\n finish?: (page: Page, context: BrowserContext) => Promise<void>;\n}\n\n/** Everything one capture session produced, before anything formats it. */\nexport interface CaptureRun {\n cards: DiffCard[];\n /** Raw frames the recorder took, before the differ chose between them. */\n total_frames: number;\n duration_ms: number;\n /** URL that was requested. */\n url: string;\n /** URL the page ended on, when it could be read. */\n final_url?: string;\n title?: string;\n /** Frames the recorder had to drop (screenshot failures). */\n dropped: number;\n context: CapturedContext;\n /** Present only when an interaction script was replayed. */\n interactions?: InteractionReport;\n /** The viewport the frames were recorded at. */\n viewport: Viewport;\n /** The saved auth that was applied, if any, and whether the page still showed a login form afterwards. */\n auth: ResolvedAuth | null;\n login_visible: boolean;\n}\n\n/**\n * Run one capture session: navigate, record, replay the script, diff.\n *\n * Throws on failure rather than returning an error result — each caller has\n * its own wording for what went wrong (see `describeCaptureFailure`).\n *\n * Shared with `framewatch_api_mock`, which needs this exact flow with its\n * routes installed first. One navigation, one recorder, one differ, and one\n * place to fix any of them.\n */\nexport async function runCapture(input: ParsedCaptureInput, hooks: CaptureHooks = {}): Promise<CaptureRun> {\n const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n const interactions = input.interactions ?? [];\n // Touch has to be decided when the context is created, so read it off the\n // script rather than turning it on for every capture: `hasTouch` puts\n // `ontouchstart` on window, which flips feature detection in most UI\n // libraries and would silently change what a plain capture records.\n const contextOptions: BrowserContextOptions = needsTouch(interactions) ? { hasTouch: true } : {};\n\n const report: InteractionReport | undefined =\n interactions.length > 0 ? { total: interactions.length, completed: 0, steps: [] } : undefined;\n\n const auth = await resolveStorageState(input.storage_state);\n if (auth) contextOptions.storageState = auth.state;\n\n const recording = await withPage({ viewport, contextOptions }, async (page, context) => {\n // Before anything else, including the layers: whatever the caller needs in\n // place for the page's very first request.\n if (hooks.prepare) await hooks.prepare(page, context);\n\n // Layers go on before the navigation: a script that throws while the page\n // loads, the request that never comes back and first paint all happen\n // before frame 0 exists, and they are the most useful things here.\n const layers = await attachLayers(page, {\n console: input.include_console,\n network: input.include_network,\n dom: input.include_dom,\n performance: input.include_performance,\n });\n try {\n // \"commit\" rather than \"load\": splash/loading animations must be recorded from the very start.\n await page.goto(input.url, { waitUntil: \"commit\", timeout: NAVIGATION_TIMEOUT_MS });\n if (input.wait_for) {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms });\n }\n const result = await recordFrames(\n page,\n { duration_ms: input.duration_ms, interval_ms: input.interval_ms },\n report && ((recorder) => replayScript(page, recorder, interactions, input.interaction_timeout_ms, report)),\n );\n // Drain the layers before anything else can close the page. `collect`\n // reads only what Node already holds, so a page that froze, navigated\n // away or crashed still yields everything it managed to report.\n const collected = layers.collect(result.started_at);\n // The page may have died mid-recording (crash, closed browser). Frames\n // already captured are still worth returning, so never let reading the\n // title or url turn a partial recording into an error.\n // A login form on the page after restoring a session is the expiry signal.\n const loginVisible = auth ? await loginFormVisible(page) : false;\n return { ...result, context: collected, loginVisible, ...(await describePage(page)) };\n } finally {\n layers.detach();\n if (hooks.finish) await hooks.finish(page, context).catch(() => {});\n }\n });\n\n const { cards, total_frames } = await buildDiffCards(recording.frames, {\n sensitivity: input.sensitivity,\n max_frames: input.max_frames,\n });\n applyContext(cards, recording.context);\n\n return {\n cards,\n total_frames,\n duration_ms: recording.duration_ms,\n url: input.url,\n final_url: recording.finalUrl,\n title: recording.title,\n dropped: recording.dropped,\n context: recording.context,\n interactions: report,\n viewport,\n auth,\n login_visible: recording.loginVisible,\n };\n}\n\n/**\n * The main FrameWatch tool: record a page for `duration_ms` and return only\n * the meaningful visual changes as diff cards (see CLAUDE.md \"Diff Cards\" and\n * \"MCP Response Format\").\n *\n * Flow: navigate (waiting only for the navigation to commit, so loading and\n * splash animations are recorded from their first frame) → optionally wait\n * for `wait_for` → record raw frames every `interval_ms`, replaying\n * `interactions` as the recording runs → smart-diff them into at most\n * `max_frames` cards → format. All failures — including invalid input — are\n * reported as `isError` results rather than thrown so the MCP client sees a\n * useful message.\n */\nexport async function capturePage(rawInput: CaptureInput): Promise<CallToolResult> {\n const parsed = captureInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Capture failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n let run: CaptureRun;\n try {\n run = await runCapture(input);\n } catch (error) {\n return errorResult(describeCaptureFailure(input, error));\n }\n\n return formatDiffCards({\n cards: run.cards,\n total_frames: run.total_frames,\n duration_ms: run.duration_ms,\n url: run.url,\n final_url: run.final_url,\n title: run.title,\n dropped: run.dropped,\n interactions: run.interactions,\n viewport: run.viewport,\n notes: [...(summariseContext(run.context, run.cards.length) ?? []), ...authLines(run)],\n });\n}\n\n/** The auth line for a run's summary, when there is one (see `describeAuth`). */\nexport function authLines(run: Pick<CaptureRun, \"auth\" | \"login_visible\">): string[] {\n const line = run.auth ? describeAuth(run.auth, run.login_visible) : null;\n return line ? [line] : [];\n}\n\n/**\n * Replay the interaction script while the recorder runs, forcing a frame after\n * every step so the differ always keeps the result of each action.\n *\n * A failing step ends the script but never the capture: the frames recorded so\n * far are the most useful thing FrameWatch can hand back (they show the state\n * the page was actually in when the selector did not match), so the failure is\n * recorded in `report` and a forced \"error\" frame is captured instead of\n * throwing. The recording then plays out the rest of its duration.\n */\nasync function replayScript(\n page: Page,\n recorder: FrameRecorder,\n interactions: Interaction[],\n timeoutMs: number,\n report: InteractionReport,\n): Promise<void> {\n for (let i = 0; i < interactions.length; i++) {\n const step = interactions[i];\n let outcome;\n try {\n outcome = await executeInteraction(page, step, { timeout_ms: timeoutMs });\n } catch (error) {\n report.error = error instanceof Error ? error.message : String(error);\n report.failed_index = i + 1;\n await recorder.captureNow(\"error\");\n return;\n }\n report.completed++;\n report.steps.push(describeInteraction(step) + (outcome.note ?? \"\"));\n await recorder.captureNow(\"interaction\");\n }\n}\n\n/**\n * Title and final url of a page. Both are cosmetic, so neither is allowed to\n * fail or delay the result: the page may have died during the recording, and\n * `page.title()` blocks for Playwright's full default timeout while the main\n * thread is busy — exactly the case a developer points this tool at.\n */\nasync function describePage(page: Page): Promise<{ title?: string; finalUrl?: string }> {\n if (page.isClosed()) return {};\n const finalUrl = safely(() => page.url());\n // Settle the losing side too: a page.title() that outlives the race would\n // otherwise reject unobserved when the context is closed underneath it.\n const title = page.title().catch(() => undefined);\n let timer: NodeJS.Timeout | undefined;\n const giveUp = new Promise<undefined>((resolve) => {\n timer = setTimeout(() => resolve(undefined), PAGE_INFO_TIMEOUT_MS);\n });\n try {\n return { title: await Promise.race([title, giveUp]), finalUrl };\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction safely<T>(read: () => T): T | undefined {\n try {\n return read();\n } catch {\n return undefined;\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * Turn a Playwright/Node error into a one-line, actionable message. Mirrors\n * `describeFailure` in screenshot.ts: matches on the failing Playwright call\n * (the message prefix) rather than on substrings of user-supplied selectors,\n * so a navigation failure is never blamed on an element.\n */\nexport function describeCaptureFailure(\n input: Pick<ParsedCaptureInput, \"url\" | \"wait_for\" | \"wait_for_timeout_ms\">,\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const prefix = `Capture of ${input.url} failed:`;\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n `${prefix} Playwright's Chromium browser is not installed. ` +\n `Run \\`npx playwright install chromium\\` and try again. (${firstLine})`\n );\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `${prefix} selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms}ms.`;\n }\n return `${prefix} ${firstLine}`;\n}\n\nexport function registerCaptureTool(server: McpServer): void {\n server.registerTool(\n CAPTURE_TOOL_NAME,\n {\n title: \"Capture\",\n description:\n \"Record a web page for a few seconds and return only the meaningful visual changes as diff cards: \" +\n \"a full-frame PNG plus a crop of the changed region and its position for each kept frame. \" +\n \"Use it to check animations, splash/loading screens, transitions and anything that changes over time. \" +\n \"Lower `sensitivity` to keep more frames; `max_frames` caps the output. \" +\n \"Each frame can also carry the context for the interval that produced it: console output and uncaught \" +\n \"errors (on by default), plus network requests, DOM mutations and paint/layout-shift timing via \" +\n \"`include_network`, `include_dom` and `include_performance` — that is how you find out *why* a frame \" +\n \"looks wrong, not just that it does.\",\n inputSchema: captureInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => capturePage(args),\n );\n}\n"]}
|