playwright-e2e-mcp 0.6.0 → 0.7.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.
Files changed (77) hide show
  1. package/README.md +40 -8
  2. package/dist/cli/ci.js +3 -0
  3. package/dist/cli/ci.js.map +1 -1
  4. package/dist/cli/guard.d.ts +11 -0
  5. package/dist/cli/guard.js +80 -0
  6. package/dist/cli/guard.js.map +1 -0
  7. package/dist/cli/upload.d.ts +14 -0
  8. package/dist/cli/upload.js +81 -0
  9. package/dist/cli/upload.js.map +1 -0
  10. package/dist/http.d.ts +1 -1
  11. package/dist/http.js +1 -1
  12. package/dist/index.js +22 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/server.d.ts +2 -2
  15. package/dist/server.js +74 -8
  16. package/dist/server.js.map +1 -1
  17. package/dist/tools/analyze-history.d.ts +15 -0
  18. package/dist/tools/analyze-history.js +44 -9
  19. package/dist/tools/analyze-history.js.map +1 -1
  20. package/dist/tools/audit-accessibility.d.ts +3 -0
  21. package/dist/tools/audit-performance.d.ts +3 -0
  22. package/dist/tools/audit-performance.js +1 -1
  23. package/dist/tools/audit-performance.js.map +1 -1
  24. package/dist/tools/check-test-change.d.ts +44 -0
  25. package/dist/tools/check-test-change.js +132 -0
  26. package/dist/tools/check-test-change.js.map +1 -0
  27. package/dist/tools/compare-visual-state.d.ts +3 -0
  28. package/dist/tools/generate-e2e-test.js +1 -1
  29. package/dist/tools/generate-e2e-test.js.map +1 -1
  30. package/dist/tools/get-run-status.js +8 -2
  31. package/dist/tools/get-run-status.js.map +1 -1
  32. package/dist/tools/inspect-page.d.ts +3 -0
  33. package/dist/tools/inspect-page.js +9 -5
  34. package/dist/tools/inspect-page.js.map +1 -1
  35. package/dist/tools/probe-script.js +158 -10
  36. package/dist/tools/probe-script.js.map +1 -1
  37. package/dist/tools/quarantine.d.ts +61 -0
  38. package/dist/tools/quarantine.js +133 -0
  39. package/dist/tools/quarantine.js.map +1 -0
  40. package/dist/tools/run-test.d.ts +10 -1
  41. package/dist/tools/run-test.js +36 -5
  42. package/dist/tools/run-test.js.map +1 -1
  43. package/dist/tools/save-login.d.ts +142 -0
  44. package/dist/tools/save-login.js +264 -0
  45. package/dist/tools/save-login.js.map +1 -0
  46. package/dist/tools/shared.d.ts +30 -1
  47. package/dist/tools/shared.js +51 -27
  48. package/dist/tools/shared.js.map +1 -1
  49. package/dist/tools/structured.d.ts +16 -1
  50. package/dist/tools/structured.js +14 -1
  51. package/dist/tools/structured.js.map +1 -1
  52. package/dist/tools/suggest-fix.js +1 -1
  53. package/dist/tools/suggest-fix.js.map +1 -1
  54. package/dist/tools/validate-selector.d.ts +6 -0
  55. package/dist/tools/validate-selector.js +28 -5
  56. package/dist/tools/validate-selector.js.map +1 -1
  57. package/dist/types/index.d.ts +1 -1
  58. package/dist/types/index.js.map +1 -1
  59. package/dist/utils/locator-expr.d.ts +3 -1
  60. package/dist/utils/locator-expr.js +25 -7
  61. package/dist/utils/locator-expr.js.map +1 -1
  62. package/dist/utils/pro-client.d.ts +29 -0
  63. package/dist/utils/pro-client.js +114 -0
  64. package/dist/utils/pro-client.js.map +1 -0
  65. package/dist/utils/quarantine.d.ts +64 -0
  66. package/dist/utils/quarantine.js +106 -0
  67. package/dist/utils/quarantine.js.map +1 -0
  68. package/dist/utils/test-change-guard.d.ts +69 -0
  69. package/dist/utils/test-change-guard.js +391 -0
  70. package/dist/utils/test-change-guard.js.map +1 -0
  71. package/dist/utils/totp.d.ts +26 -0
  72. package/dist/utils/totp.js +84 -0
  73. package/dist/utils/totp.js.map +1 -0
  74. package/package.json +1 -1
  75. package/skill/playwright-e2e-mcp/SKILL.md +46 -7
  76. package/skill/playwright-e2e-mcp/references/setup.md +7 -1
  77. package/skill/playwright-e2e-mcp/references/tools.md +43 -1
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Tool — save-login: log in once and save the session as a Playwright
3
+ * storageState file that tests and the page tools reuse. Before logging in
4
+ * again it checks whether the saved session still works, so expired logins
5
+ * refresh themselves and valid ones are left alone.
6
+ *
7
+ * Passwords and one-time codes never pass through the chat: fill values
8
+ * may say {{env:E2E_PASSWORD}} (read from the server's environment) and
9
+ * {{totp}} (an authenticator code generated from a secret in an
10
+ * environment variable at the moment it is typed).
11
+ */
12
+ import { readFile, stat } from 'node:fs/promises';
13
+ import path from 'node:path';
14
+ import { z } from 'zod';
15
+ import { PlaywrightMcpError } from '../types/index.js';
16
+ import { relativeToRoot, sanitizeUserPath, toNativePath, writeFileInsideRoot } from '../utils/path-utils.js';
17
+ import { parseLocator } from '../utils/locator-expr.js';
18
+ import { parseTotpSecret } from '../utils/totp.js';
19
+ import { assertUrlAllowed } from '../utils/url-policy.js';
20
+ import { assertHttpUrl, guard, pageActionSchema, resolvePageState, resolveProjectRoot, runBrowserScript, toolError, toolText, } from './shared.js';
21
+ const saveLoginInput = z.object({
22
+ url: z.string().describe('Login page URL (http/https)'),
23
+ actions: z
24
+ .array(pageActionSchema)
25
+ .max(20)
26
+ .optional()
27
+ .describe("Login steps. In fill values write {{env:E2E_PASSWORD}} for a secret from the environment and {{totp}} for an authenticator code, never the secret itself. End with a 'wait' on something only signed-in users see"),
28
+ verify: z
29
+ .object({
30
+ url: z.string().optional().describe('Page to open to check the session, e.g. /account (default: where login ends)'),
31
+ visible: z.string().optional().describe("Locator only a signed-in user sees, e.g. getByRole('button', { name: 'Sign out' })"),
32
+ notUrl: z.string().optional().describe("The session is bad if the page lands on a URL containing this, e.g. '/login'"),
33
+ })
34
+ .optional()
35
+ .describe('How to tell the session works; also used to decide whether a saved session is still valid'),
36
+ name: z
37
+ .string()
38
+ .regex(/^[\w.-]{1,40}$/)
39
+ .optional()
40
+ .describe("Session name, e.g. 'admin' (default 'user'); saved to playwright/.auth/<name>.json"),
41
+ path: z.string().optional().describe('Where to save the storageState JSON inside the project (overrides name)'),
42
+ totpSecretEnv: z
43
+ .string()
44
+ .optional()
45
+ .describe('Environment variable holding the authenticator secret (base32 or otpauth:// URI), for {{totp}}'),
46
+ reuse: z.boolean().optional().describe('Keep a saved session that still passes verify instead of logging in again (default true)'),
47
+ headers: z.record(z.string(), z.string()).optional().describe('Extra HTTP headers'),
48
+ viewport: z.object({ width: z.number().int().min(200).max(4_000), height: z.number().int().min(200).max(4_000) }).optional(),
49
+ actionTimeoutMs: z.number().int().min(1_000).max(120_000).optional().describe('How long each step waits for its element (default 10000)'),
50
+ projectRoot: z.string().optional().describe('Project directory (default: server root)'),
51
+ timeoutMs: z.number().int().min(5_000).max(300_000).optional().describe('Overall limit in ms (default 60000)'),
52
+ });
53
+ export const saveLoginSchema = saveLoginInput;
54
+ const ENV_TOKEN_RE = /\{\{\s*env:([A-Za-z0-9_]+)\s*\}\}/g;
55
+ const TOTP_TOKEN_RE = /\{\{\s*totp\s*\}\}/;
56
+ /**
57
+ * Environment variables a login may read. A page the agent visits could
58
+ * ask it to type {{env:AWS_SECRET_ACCESS_KEY}} into a form, so only names
59
+ * that are plainly test logins are allowed, plus any the user lists.
60
+ */
61
+ export function loginEnvAllowed(name) {
62
+ if (/^(E2E|TEST|PW|PLAYWRIGHT|LOGIN)_[A-Z0-9_]+$/.test(name) && name !== 'PW_MCP_HTTP_TOKEN')
63
+ return true;
64
+ const extra = (process.env.PW_MCP_LOGIN_ENV ?? '')
65
+ .split(',')
66
+ .map((entry) => entry.trim())
67
+ .filter(Boolean);
68
+ return extra.includes(name);
69
+ }
70
+ /** The environment values the fill steps ask for, checked and read. */
71
+ export function resolveLoginValues(actions) {
72
+ const names = new Set();
73
+ for (const action of actions ?? []) {
74
+ if (action.type !== 'fill' || !action.value)
75
+ continue;
76
+ for (const match of action.value.matchAll(ENV_TOKEN_RE))
77
+ names.add(match[1]);
78
+ }
79
+ const values = {};
80
+ const missing = [];
81
+ for (const name of names) {
82
+ if (!loginEnvAllowed(name)) {
83
+ throw new PlaywrightMcpError(`{{env:${name}}} is not an allowed login variable`, 'INVALID_INPUT', {
84
+ hint: 'Name login secrets E2E_*, TEST_*, PW_*, PLAYWRIGHT_* or LOGIN_* (e.g. E2E_PASSWORD), or list the name in PW_MCP_LOGIN_ENV.',
85
+ });
86
+ }
87
+ const value = process.env[name];
88
+ if (value === undefined || value === '')
89
+ missing.push(name);
90
+ else
91
+ values[name] = value;
92
+ }
93
+ if (missing.length > 0) {
94
+ throw new PlaywrightMcpError(`Not set in the server's environment: ${missing.join(', ')}`, 'INVALID_INPUT', {
95
+ hint: 'Set them where the MCP server starts (the env block of your MCP client config, or the shell), then restart the server.',
96
+ });
97
+ }
98
+ return values;
99
+ }
100
+ /** A literal typed into a password field: worth moving into an env variable. */
101
+ function literalPasswordStep(actions) {
102
+ return (actions ?? []).some((a) => a.type === 'fill' && /pass(word|code)?|pin\b/i.test(a.locator ?? '') && !!a.value && !a.value.includes('{{'));
103
+ }
104
+ function expiryText(scan) {
105
+ if (scan.firstExpiry === null)
106
+ return scan.cookies > 0 ? 'session cookies only (they end when the browser closes)' : 'no cookies';
107
+ const seconds = scan.firstExpiry - Date.now() / 1000;
108
+ const when = new Date(scan.firstExpiry * 1000).toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
109
+ if (seconds <= 0)
110
+ return `a cookie already expired (${when})`;
111
+ const hours = seconds / 3600;
112
+ const span = hours < 48 ? `${Math.max(1, Math.round(hours))} h` : `${Math.round(hours / 24)} days`;
113
+ return `first cookie expires in ${span} (${when})`;
114
+ }
115
+ /** Is the session file covered by .gitignore? A rough check that catches the common miss. */
116
+ async function ignoredByGit(root, relative) {
117
+ const text = await readFile(toNativePath(path.posix.join(root, '.gitignore')), 'utf8').catch(() => '');
118
+ const lines = text.split(/\r?\n/).map((line) => line.trim().replace(/^\//, '').replace(/\/$/, ''));
119
+ const parts = relative.split('/');
120
+ for (let i = 1; i <= parts.length; i += 1) {
121
+ const prefix = parts.slice(0, i).join('/');
122
+ if (lines.includes(prefix) || lines.includes(`${prefix}/*`) || lines.includes(`${prefix}/**`))
123
+ return true;
124
+ }
125
+ return lines.some((line) => line === '*.auth.json' || line === '.auth' || (line.endsWith('.json') && relative.endsWith(line)));
126
+ }
127
+ function scanLines(scan) {
128
+ return [
129
+ `**Session:** ${scan.cookies} cookie${scan.cookies === 1 ? '' : 's'}, ${scan.origins} origin${scan.origins === 1 ? '' : 's'} with local storage · ${expiryText(scan)}`,
130
+ ];
131
+ }
132
+ export const saveLoginTool = {
133
+ name: 'save-login',
134
+ description: "Log in once and save the session as a storageState file (default playwright/.auth/<name>.json) for tests and the page tools. If a saved session still passes verify it is reused; expired ones log in again. Secrets stay out of chat: fill values use {{env:E2E_PASSWORD}} and {{totp}} (authenticator code from totpSecretEnv).",
135
+ inputSchema: saveLoginSchema,
136
+ handler: async (args, ctx) => guard('save-login', async () => {
137
+ if (ctx.restricted) {
138
+ throw new PlaywrightMcpError('save-login is not available on this server', 'INVALID_PATH', {
139
+ hint: 'It writes a session file into the project. Run the server locally.',
140
+ });
141
+ }
142
+ const url = assertHttpUrl(args.url);
143
+ await assertUrlAllowed(url);
144
+ const root = await resolveProjectRoot(args.projectRoot, ctx);
145
+ const timeoutMs = args.timeoutMs ?? 60_000;
146
+ const relativeTarget = args.path?.trim() || `playwright/.auth/${args.name ?? 'user'}.json`;
147
+ const target = sanitizeUserPath(relativeTarget, root);
148
+ if (!target.endsWith('.json')) {
149
+ throw new PlaywrightMcpError(`path must end in .json: ${relativeTarget}`, 'INVALID_PATH');
150
+ }
151
+ const shown = relativeToRoot(root, target);
152
+ let verifyUrl;
153
+ if (args.verify?.url) {
154
+ verifyUrl = new URL(args.verify.url, url).toString();
155
+ assertHttpUrl(verifyUrl);
156
+ await assertUrlAllowed(verifyUrl);
157
+ }
158
+ const visible = args.verify?.visible ? parseLocator(args.verify.visible) : undefined;
159
+ const loginVerify = args.verify ? { url: verifyUrl, visible, notUrl: args.verify.notUrl } : undefined;
160
+ const hasCheck = !!(visible || args.verify?.notUrl);
161
+ // A fresh login never starts from a session (PW_MCP_STORAGE_STATE included).
162
+ const { storageState: _default, ...base } = await resolvePageState({ headers: args.headers, viewport: args.viewport, actionTimeoutMs: args.actionTimeoutMs, actions: args.actions }, root, url, ctx);
163
+ const common = (state) => ({
164
+ ...state,
165
+ projectRoot: root,
166
+ loginVerify,
167
+ gotoTimeout: Math.min(30_000, Math.max(5_000, timeoutMs - 5_000)),
168
+ waitTimeout: 10_000,
169
+ startedAt: Date.now(),
170
+ });
171
+ // 1. A saved session that still works is kept.
172
+ const existing = await stat(toNativePath(target)).catch(() => null);
173
+ if (existing?.isFile() && args.reuse !== false && hasCheck) {
174
+ const check = await runBrowserScript({
175
+ ...common({ headers: base.headers, viewport: base.viewport }),
176
+ loginVerify: { ...loginVerify, url: undefined },
177
+ mode: 'login',
178
+ url: verifyUrl ?? url,
179
+ storageState: toNativePath(target),
180
+ loginCheckOnly: true,
181
+ }, { timeoutMs, signal: ctx.signal });
182
+ const scan = check.ok ? check.data?.login : undefined;
183
+ if (scan?.verified === true) {
184
+ return toolText([
185
+ `## ✅ STILL VALID — \`${shown}\``,
186
+ '',
187
+ `The saved session passed the check at ${verifyUrl ?? url}, so no new login was needed.`,
188
+ '',
189
+ ...scanLines(scan),
190
+ '',
191
+ `Use it with \`storageState: '${shown}'\` in the page tools, or \`use: { storageState: '${shown}' }\` in playwright.config.`,
192
+ ].join('\n'), { status: 'valid', valid: true, path: shown, url: verifyUrl ?? url, ...(scan.firstExpiry ? { expiresAt: new Date(scan.firstExpiry * 1000).toISOString() } : {}) });
193
+ }
194
+ }
195
+ if (!args.actions?.length) {
196
+ throw new PlaywrightMcpError('Pass the login steps in actions', 'INVALID_INPUT', {
197
+ hint: existing?.isFile()
198
+ ? `The saved session \`${shown}\` ${hasCheck ? 'failed the check' : 'cannot be checked without verify.visible or verify.notUrl'}, so a new login is needed.`
199
+ : "E.g. fill getByLabel('Email') with {{env:E2E_USER}}, fill getByLabel('Password') with {{env:E2E_PASSWORD}}, click getByRole('button', { name: 'Sign in' }).",
200
+ });
201
+ }
202
+ // 2. Log in, check, save.
203
+ const env = {};
204
+ const values = resolveLoginValues(args.actions);
205
+ if (Object.keys(values).length > 0)
206
+ env.PW_MCP_FILL_VALUES = JSON.stringify(values);
207
+ const wantsTotp = args.actions.some((a) => a.type === 'fill' && TOTP_TOKEN_RE.test(a.value ?? ''));
208
+ if (wantsTotp) {
209
+ const name = args.totpSecretEnv;
210
+ if (!name)
211
+ throw new PlaywrightMcpError('{{totp}} needs totpSecretEnv', 'INVALID_INPUT', { hint: 'Name the environment variable that holds the authenticator secret, e.g. E2E_TOTP_SECRET.' });
212
+ if (!loginEnvAllowed(name)) {
213
+ throw new PlaywrightMcpError(`${name} is not an allowed login variable`, 'INVALID_INPUT', {
214
+ hint: 'Name it E2E_*, TEST_*, PW_*, PLAYWRIGHT_* or LOGIN_* (e.g. E2E_TOTP_SECRET), or list it in PW_MCP_LOGIN_ENV.',
215
+ });
216
+ }
217
+ const secret = process.env[name];
218
+ if (!secret)
219
+ throw new PlaywrightMcpError(`${name} is not set in the server's environment`, 'INVALID_INPUT');
220
+ const totp = parseTotpSecret(secret);
221
+ env.PW_MCP_TOTP = JSON.stringify({ key: totp.key.toString('hex'), digits: totp.digits, period: totp.period, algorithm: totp.algorithm });
222
+ }
223
+ const outcome = await runBrowserScript({ ...common(base), mode: 'login', url }, { timeoutMs, signal: ctx.signal, env });
224
+ if (!outcome.ok) {
225
+ const done = outcome.actionsDone?.length ? `\nSteps done: ${outcome.actionsDone.join(' → ')}` : '';
226
+ return toolError(outcome.kind ?? 'UNKNOWN', `Login on ${url} failed: ${outcome.error ?? 'unknown error'}${done}`, outcome.hint, outcome.stderrTail);
227
+ }
228
+ const scan = outcome.data?.login;
229
+ if (!scan)
230
+ return toolError('UNKNOWN', 'The browser returned no session.', undefined, outcome.stderrTail);
231
+ if (scan.verified === false) {
232
+ return toolText([
233
+ `## ❌ LOGIN FAILED — nothing saved`,
234
+ '',
235
+ `The steps ran, but the check failed: ${scan.verifyNote || 'the session did not work'}. Ended on ${outcome.data?.finalUrl ?? url}.`,
236
+ '',
237
+ '**How to fix:** check the credentials in the environment, add a `wait` step for the page after sign-in, or run **inspect-page** with the same actions to see where the login stops (an MFA prompt, a consent screen, an error message).',
238
+ ].join('\n'), { ok: false, status: 'failed', valid: false, path: shown, url });
239
+ }
240
+ await writeFileInsideRoot(target, `${JSON.stringify(scan.state, null, 2)}\n`, root);
241
+ const lines = [`## ✅ SAVED — \`${shown}\``, ''];
242
+ lines.push(scan.verified === true
243
+ ? `Logged in and checked: the session works${verifyUrl ? ` on ${verifyUrl}` : ''}.`
244
+ : 'Logged in. No check was given, so the session was saved without one; add `verify` so expired sessions are caught next time.', '', ...scanLines(scan));
245
+ if (scan.cookies === 0 && scan.origins === 0) {
246
+ lines.push('', '> ⚠️ The session is empty: no cookies or local storage were set. The login may not have finished; end the steps with a `wait` on the signed-in page.');
247
+ }
248
+ if (literalPasswordStep(args.actions)) {
249
+ lines.push('', '> 🔐 A password was typed as plain text, so it is now in this chat. Put it in an environment variable (E2E_PASSWORD) and write `{{env:E2E_PASSWORD}}` instead.');
250
+ }
251
+ if (!(await ignoredByGit(root, shown))) {
252
+ lines.push('', `> ⚠️ \`${shown}\` holds live session cookies and is not in .gitignore. Add \`${path.posix.dirname(shown)}/\` to .gitignore so it is never committed.`);
253
+ }
254
+ lines.push('', `**Use it:** \`storageState: '${shown}'\` in inspect-page, validate-selector and the audits, or \`use: { storageState: '${shown}' }\` in playwright.config. Call save-login again before a run: it keeps the session while it works and logs in again when it expires.`);
255
+ return toolText(lines.join('\n'), {
256
+ status: 'saved',
257
+ valid: true,
258
+ path: shown,
259
+ url,
260
+ ...(scan.firstExpiry ? { expiresAt: new Date(scan.firstExpiry * 1000).toISOString() } : {}),
261
+ });
262
+ }),
263
+ };
264
+ //# sourceMappingURL=save-login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-login.js","sourceRoot":"","sources":["../../src/tools/save-login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7G,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,aAAa,EACb,KAAK,EACL,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,SAAS,EACT,QAAQ,GACT,MAAM,aAAa,CAAC;AAGrB,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,6BAA6B,CAAC;IACvD,OAAO,EAAE,CAAC;SACP,KAAK,CAAC,gBAAgB,CAAC;SACvB,GAAG,CAAC,EAAE,CAAC;SACP,QAAQ,EAAE;SACV,QAAQ,CACP,mNAAmN,CACpN;IACH,MAAM,EAAE,CAAC;SACN,MAAM,CAAC;QACN,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8EAA8E,CAAC;QACnH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oFAAoF,CAAC;QAC7H,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8EAA8E,CAAC;KACvH,CAAC;SACD,QAAQ,EAAE;SACV,QAAQ,CAAC,2FAA2F,CAAC;IACxG,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,KAAK,CAAC,gBAAgB,CAAC;SACvB,QAAQ,EAAE;SACV,QAAQ,CAAC,oFAAoF,CAAC;IACjG,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yEAAyE,CAAC;IAC/G,aAAa,EAAE,CAAC;SACb,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,gGAAgG,CAAC;IAC7G,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0FAA0F,CAAC;IAClI,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oBAAoB,CAAC;IACnF,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC5H,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0DAA0D,CAAC;IACzI,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0CAA0C,CAAC;IACvF,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;CAC/G,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,eAAe,GAAG,cAAc,CAAC;AAE9C,MAAM,YAAY,GAAG,oCAAoC,CAAC;AAC1D,MAAM,aAAa,GAAG,oBAAoB,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,6CAA6C,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,mBAAmB;QAAE,OAAO,IAAI,CAAC;IAC1G,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE,CAAC;SAC/C,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,kBAAkB,CAAC,OAAkC;IACnE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,MAAM,IAAI,OAAO,IAAI,EAAE,EAAE,CAAC;QACnC,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK;YAAE,SAAS;QACtD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,CAAC;IACD,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,kBAAkB,CAAC,SAAS,IAAI,qCAAqC,EAAE,eAAe,EAAE;gBAChG,IAAI,EAAE,4HAA4H;aACnI,CAAC,CAAC;QACL,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;YACvD,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IAC5B,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,kBAAkB,CAAC,wCAAwC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,eAAe,EAAE;YAC1G,IAAI,EAAE,wHAAwH;SAC/H,CAAC,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,gFAAgF;AAChF,SAAS,mBAAmB,CAAC,OAAkC;IAC7D,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CACzB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CACpH,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,IAAe;IACjC,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,yDAAyD,CAAC,CAAC,CAAC,YAAY,CAAC;IAClI,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;IACrD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC;IACrG,IAAI,OAAO,IAAI,CAAC;QAAE,OAAO,6BAA6B,IAAI,GAAG,CAAC;IAC9D,MAAM,KAAK,GAAG,OAAO,GAAG,IAAI,CAAC;IAC7B,MAAM,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC;IACnG,OAAO,2BAA2B,IAAI,KAAK,IAAI,GAAG,CAAC;AACrD,CAAC;AAED,6FAA6F;AAC7F,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,QAAgB;IACxD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IACvG,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;IACnG,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,MAAM,IAAI,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IAC7G,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,aAAa,IAAI,IAAI,KAAK,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjI,CAAC;AAED,SAAS,SAAS,CAAC,IAAe;IAChC,OAAO;QACL,gBAAgB,IAAI,CAAC,OAAO,UAAU,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,OAAO,UAAU,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,yBAAyB,UAAU,CAAC,IAAI,CAAC,EAAE;KACvK,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,IAAI,EAAE,YAAY;IAClB,WAAW,EACT,mUAAmU;IACrU,WAAW,EAAE,eAAe;IAC5B,OAAO,EAAE,KAAK,EAAE,IAAoB,EAAE,GAAgB,EAAyB,EAAE,CAC/E,KAAK,CAAC,YAAY,EAAE,KAAK,IAAI,EAAE;QAC7B,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;YACnB,MAAM,IAAI,kBAAkB,CAAC,4CAA4C,EAAE,cAAc,EAAE;gBACzF,IAAI,EAAE,oEAAoE;aAC3E,CAAC,CAAC;QACL,CAAC;QACD,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,kBAAkB,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC;QAE3C,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,oBAAoB,IAAI,CAAC,IAAI,IAAI,MAAM,OAAO,CAAC;QAC3F,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;QACtD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,kBAAkB,CAAC,2BAA2B,cAAc,EAAE,EAAE,cAAc,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAE3C,IAAI,SAA6B,CAAC;QAClC,IAAI,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC;YACrB,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;YACrD,aAAa,CAAC,SAAS,CAAC,CAAC;YACzB,MAAM,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACpC,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACrF,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACtG,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAEpD,6EAA6E;QAC7E,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,gBAAgB,CAChE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,EAChH,IAAI,EACJ,GAAG,EACH,GAAG,CACJ,CAAC;QACF,MAAM,MAAM,GAAG,CAAC,KAAwB,EAA6C,EAAE,CAAC,CAAC;YACvF,GAAG,KAAK;YACR,WAAW,EAAE,IAAI;YACjB,WAAW;YACX,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,CAAC,CAAC;YACjE,WAAW,EAAE,MAAM;YACnB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC,CAAC;QAEH,+CAA+C;QAC/C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACpE,IAAI,QAAQ,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC3D,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAClC;gBACE,GAAG,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC7D,WAAW,EAAE,EAAE,GAAG,WAAW,EAAE,GAAG,EAAE,SAAS,EAAE;gBAC/C,IAAI,EAAE,OAAO;gBACb,GAAG,EAAE,SAAS,IAAI,GAAG;gBACrB,YAAY,EAAE,YAAY,CAAC,MAAM,CAAC;gBAClC,cAAc,EAAE,IAAI;aACrB,EACD,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAClC,CAAC;YACF,MAAM,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;YACtD,IAAI,IAAI,EAAE,QAAQ,KAAK,IAAI,EAAE,CAAC;gBAC5B,OAAO,QAAQ,CACb;oBACE,wBAAwB,KAAK,IAAI;oBACjC,EAAE;oBACF,yCAAyC,SAAS,IAAI,GAAG,+BAA+B;oBACxF,EAAE;oBACF,GAAG,SAAS,CAAC,IAAI,CAAC;oBAClB,EAAE;oBACF,gCAAgC,KAAK,qDAAqD,KAAK,6BAA6B;iBAC7H,CAAC,IAAI,CAAC,IAAI,CAAC,EACZ,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAClK,CAAC;YACJ,CAAC;QACH,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;YAC1B,MAAM,IAAI,kBAAkB,CAAC,iCAAiC,EAAE,eAAe,EAAE;gBAC/E,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE;oBACtB,CAAC,CAAC,uBAAuB,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,2DAA2D,6BAA6B;oBAC5J,CAAC,CAAC,6JAA6J;aAClK,CAAC,CAAC;QACL,CAAC;QAED,0BAA0B;QAC1B,MAAM,GAAG,GAA2B,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAChD,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,kBAAkB,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACpF,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;QACnG,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC;YAChC,IAAI,CAAC,IAAI;gBAAE,MAAM,IAAI,kBAAkB,CAAC,8BAA8B,EAAE,eAAe,EAAE,EAAE,IAAI,EAAE,0FAA0F,EAAE,CAAC,CAAC;YAC/L,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3B,MAAM,IAAI,kBAAkB,CAAC,GAAG,IAAI,mCAAmC,EAAE,eAAe,EAAE;oBACxF,IAAI,EAAE,8GAA8G;iBACrH,CAAC,CAAC;YACL,CAAC;YACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,kBAAkB,CAAC,GAAG,IAAI,yCAAyC,EAAE,eAAe,CAAC,CAAC;YAC7G,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;YACrC,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAC3I,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QACxH,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;YAChB,MAAM,IAAI,GAAG,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,iBAAiB,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnG,OAAO,SAAS,CAAC,OAAO,CAAC,IAAI,IAAI,SAAS,EAAE,YAAY,GAAG,YAAY,OAAO,CAAC,KAAK,IAAI,eAAe,GAAG,IAAI,EAAE,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;QACtJ,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;QACjC,IAAI,CAAC,IAAI;YAAE,OAAO,SAAS,CAAC,SAAS,EAAE,kCAAkC,EAAE,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;QAE1G,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;YAC5B,OAAO,QAAQ,CACb;gBACE,mCAAmC;gBACnC,EAAE;gBACF,wCAAwC,IAAI,CAAC,UAAU,IAAI,0BAA0B,cAAc,OAAO,CAAC,IAAI,EAAE,QAAQ,IAAI,GAAG,GAAG;gBACnI,EAAE;gBACF,yOAAyO;aAC1O,CAAC,IAAI,CAAC,IAAI,CAAC,EACZ,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAChE,CAAC;QACJ,CAAC;QAED,MAAM,mBAAmB,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAEpF,MAAM,KAAK,GAAG,CAAC,kBAAkB,KAAK,IAAI,EAAE,EAAE,CAAC,CAAC;QAChD,KAAK,CAAC,IAAI,CACR,IAAI,CAAC,QAAQ,KAAK,IAAI;YACpB,CAAC,CAAC,2CAA2C,SAAS,CAAC,CAAC,CAAC,OAAO,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG;YACnF,CAAC,CAAC,6HAA6H,EACjI,EAAE,EACF,GAAG,SAAS,CAAC,IAAI,CAAC,CACnB,CAAC;QACF,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;YAC7C,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,sJAAsJ,CAAC,CAAC;QACzK,CAAC;QACD,IAAI,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACtC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,gKAAgK,CAAC,CAAC;QACnL,CAAC;QACD,IAAI,CAAC,CAAC,MAAM,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;YACvC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,UAAU,KAAK,iEAAiE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;QACzK,CAAC;QACD,KAAK,CAAC,IAAI,CACR,EAAE,EACF,gCAAgC,KAAK,qFAAqF,KAAK,wIAAwI,CACxQ,CAAC;QACF,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YAChC,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,IAAI;YACX,IAAI,EAAE,KAAK;YACX,GAAG;YACH,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5F,CAAC,CAAC;IACL,CAAC,CAAC;CACL,CAAC"}
@@ -113,6 +113,7 @@ export declare const pageStateShape: {
113
113
  url: z.ZodOptional<z.ZodString>;
114
114
  ms: z.ZodOptional<z.ZodNumber>;
115
115
  }, z.core.$strip>>>;
116
+ actionTimeoutMs: z.ZodOptional<z.ZodNumber>;
116
117
  routes: z.ZodOptional<z.ZodArray<z.ZodObject<{
117
118
  url: z.ZodString;
118
119
  method: z.ZodOptional<z.ZodString>;
@@ -154,6 +155,7 @@ export declare const pageStateShape: {
154
155
  }>>;
155
156
  };
156
157
  export interface PageStateInput {
158
+ actionTimeoutMs?: number;
157
159
  storageState?: string;
158
160
  headers?: Record<string, string>;
159
161
  actions?: Array<z.infer<typeof pageActionSchema>>;
@@ -186,6 +188,8 @@ export interface ResolvedRouteMock {
186
188
  delayMs?: number;
187
189
  }
188
190
  export interface ResolvedPageState {
191
+ /** Per-action wait in ms (the probe's default is 10000). */
192
+ actionTimeout?: number;
189
193
  storageState?: string;
190
194
  headers?: Record<string, string>;
191
195
  actions?: Array<{
@@ -227,7 +231,7 @@ export declare function describeRouteHits(state: ResolvedPageState, hits: number
227
231
  /** A hint when an unauthenticated page landed on a login screen. */
228
232
  export declare function loginHint(finalUrl: string | undefined, state: ResolvedPageState): string | undefined;
229
233
  export interface BrowserScriptConfig extends Omit<ResolvedPageState, never> {
230
- mode: 'inspect' | 'validate' | 'screenshot' | 'heal' | 'a11y' | 'perf';
234
+ mode: 'inspect' | 'validate' | 'screenshot' | 'heal' | 'a11y' | 'perf' | 'login';
231
235
  projectRoot: string;
232
236
  /** Page to open; omitted when rendering a snapshot. */
233
237
  url?: string;
@@ -264,6 +268,13 @@ export interface BrowserScriptConfig extends Omit<ResolvedPageState, never> {
264
268
  mobile?: boolean;
265
269
  /** perf: ms to keep observing after load so late shifts and LCP land. */
266
270
  settleMs?: number;
271
+ /** login: how to tell the session works, and whether to only check it. */
272
+ loginVerify?: {
273
+ url?: string;
274
+ visible?: LocatorCall[];
275
+ notUrl?: string;
276
+ };
277
+ loginCheckOnly?: boolean;
267
278
  startedAt: number;
268
279
  }
269
280
  /** Network emulation in Chrome DevTools terms (throughput in bytes/s). */
@@ -299,6 +310,23 @@ export interface BrowserScriptData {
299
310
  ariaTruncated?: boolean;
300
311
  /** Requests each network mock in `routes` handled, by index. */
301
312
  routeHits?: number[];
313
+ /** HTTP status of the page's main document, when it was fetched over the network. */
314
+ httpStatus?: number;
315
+ /** mode 'login': the session check and what the session holds. */
316
+ login?: LoginScan;
317
+ }
318
+ export interface LoginScan {
319
+ /** null when no check was asked for. */
320
+ verified: boolean | null;
321
+ verifyNote?: string;
322
+ /** The storageState to save; absent when checking only or the check failed. */
323
+ state?: unknown;
324
+ cookies: number;
325
+ sessionCookies: number;
326
+ origins: number;
327
+ /** Earliest cookie expiry, in Unix seconds. */
328
+ firstExpiry: number | null;
329
+ expired: number;
302
330
  }
303
331
  export interface PerformanceResource {
304
332
  url: string;
@@ -380,6 +408,7 @@ export interface BrowserScriptOutcome {
380
408
  export declare function runBrowserScript(config: BrowserScriptConfig, options: {
381
409
  timeoutMs?: number;
382
410
  signal?: AbortSignal;
411
+ env?: Record<string, string>;
383
412
  }): Promise<BrowserScriptOutcome>;
384
413
  /** Accept only http(s) URLs for page-facing tools. */
385
414
  export declare function assertHttpUrl(url: string): string;
@@ -294,7 +294,18 @@ export const routeMockSchema = z.object({
294
294
  /** Inputs shared by every tool that opens a page. */
295
295
  export const pageStateShape = {
296
296
  ...sessionShape,
297
- actions: z.array(pageActionSchema).max(20).optional().describe('Steps run after load to reach a state (modal, step 3)'),
297
+ actions: z
298
+ .array(pageActionSchema)
299
+ .max(20)
300
+ .optional()
301
+ .describe("Steps run in order after load to reach a state (modal, step 3). One call = one fresh browser, so include the login steps; end with a 'wait' on what the state shows"),
302
+ actionTimeoutMs: z
303
+ .number()
304
+ .int()
305
+ .min(1_000)
306
+ .max(120_000)
307
+ .optional()
308
+ .describe('How long each action waits for its element (default 10000); keep timeoutMs above the total'),
298
309
  routes: z
299
310
  .array(routeMockSchema)
300
311
  .max(20)
@@ -365,7 +376,7 @@ async function resolveHar(har, root, ctx) {
365
376
  function assertHeaders(headers, what) {
366
377
  for (const [name, value] of Object.entries(headers)) {
367
378
  if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name) || /[\r\n]/.test(value)) {
368
- throw new PlaywrightMcpError(`Invalid ${what} "${name}"`, 'INVALID_PATH', {
379
+ throw new PlaywrightMcpError(`Invalid ${what} "${name}"`, 'INVALID_INPUT', {
369
380
  hint: 'Header names must be tokens and values must be single-line.',
370
381
  });
371
382
  }
@@ -377,13 +388,13 @@ export function resolveRouteMocks(routes) {
377
388
  const step = `routes[${i}] (${route.url})`;
378
389
  const method = route.method?.trim().toUpperCase();
379
390
  if (method !== undefined && !/^[A-Z]+$/.test(method)) {
380
- throw new PlaywrightMcpError(`${step}: "${route.method}" is not an HTTP method`, 'INVALID_PATH');
391
+ throw new PlaywrightMcpError(`${step}: "${route.method}" is not an HTTP method`, 'INVALID_INPUT');
381
392
  }
382
393
  if (route.json !== undefined && route.body !== undefined) {
383
- throw new PlaywrightMcpError(`${step}: pass json or body, not both`, 'INVALID_PATH');
394
+ throw new PlaywrightMcpError(`${step}: pass json or body, not both`, 'INVALID_INPUT');
384
395
  }
385
396
  if (route.abort && (route.json !== undefined || route.body !== undefined || route.status !== undefined)) {
386
- throw new PlaywrightMcpError(`${step}: abort cannot be combined with a response`, 'INVALID_PATH');
397
+ throw new PlaywrightMcpError(`${step}: abort cannot be combined with a response`, 'INVALID_INPUT');
387
398
  }
388
399
  if (route.headers)
389
400
  assertHeaders(route.headers, `${step} header`);
@@ -392,7 +403,7 @@ export function resolveRouteMocks(routes) {
392
403
  if (route.json !== undefined) {
393
404
  body = JSON.stringify(route.json);
394
405
  if (body === undefined || body.length > MAX_MOCK_BODY) {
395
- throw new PlaywrightMcpError(`${step}: json must be serialisable and under ${MAX_MOCK_BODY} characters`, 'INVALID_PATH');
406
+ throw new PlaywrightMcpError(`${step}: json must be serialisable and under ${MAX_MOCK_BODY} characters`, 'INVALID_INPUT');
396
407
  }
397
408
  contentType ??= 'application/json';
398
409
  }
@@ -461,12 +472,12 @@ export async function resolvePageState(input, root, entryUrl, ctx) {
461
472
  out.har = await resolveHar(input.har, root, ctx);
462
473
  if (input.browser && input.browser !== 'chromium') {
463
474
  if (serverlessChromiumEnabled()) {
464
- throw new PlaywrightMcpError(`${input.browser} is not available on the hosted endpoint`, 'INVALID_PATH', {
475
+ throw new PlaywrightMcpError(`${input.browser} is not available on the hosted endpoint`, 'INVALID_INPUT', {
465
476
  hint: "The hosted endpoint bundles Chromium only. Use 'chromium', or run the server locally for Firefox and WebKit.",
466
477
  });
467
478
  }
468
479
  if (input.browser === 'webkit' && blockPrivateUrls()) {
469
- throw new PlaywrightMcpError('webkit is not available while private addresses are blocked', 'INVALID_PATH', {
480
+ throw new PlaywrightMcpError('webkit is not available while private addresses are blocked', 'INVALID_INPUT', {
470
481
  hint: "WebKit cannot route every connection through the address guard. Use 'chromium' or 'firefox'.",
471
482
  });
472
483
  }
@@ -475,7 +486,7 @@ export async function resolvePageState(input, root, entryUrl, ctx) {
475
486
  if (input.device) {
476
487
  // Checked against the project's own Playwright device list in the browser child.
477
488
  if (!/^[\w .()+-]+$/.test(input.device)) {
478
- throw new PlaywrightMcpError(`device "${input.device}" is not a Playwright device name`, 'INVALID_PATH', {
489
+ throw new PlaywrightMcpError(`device "${input.device}" is not a Playwright device name`, 'INVALID_INPUT', {
479
490
  hint: "Use a name from Playwright's device list, e.g. 'iPhone 15' or 'Pixel 7'.",
480
491
  });
481
492
  }
@@ -483,12 +494,12 @@ export async function resolvePageState(input, root, entryUrl, ctx) {
483
494
  }
484
495
  if (input.channel) {
485
496
  if ((input.browser ?? 'chromium') !== 'chromium') {
486
- throw new PlaywrightMcpError(`channel ${input.channel} needs the chromium engine`, 'INVALID_PATH', {
497
+ throw new PlaywrightMcpError(`channel ${input.channel} needs the chromium engine`, 'INVALID_INPUT', {
487
498
  hint: 'Leave browser unset (or chromium) when you pick a channel.',
488
499
  });
489
500
  }
490
501
  if (serverlessChromiumEnabled()) {
491
- throw new PlaywrightMcpError(`channel ${input.channel} is not available on the hosted endpoint`, 'INVALID_PATH', {
502
+ throw new PlaywrightMcpError(`channel ${input.channel} is not available on the hosted endpoint`, 'INVALID_INPUT', {
492
503
  hint: 'The hosted endpoint bundles one Chromium build. Run the server locally to test in Chrome or Edge.',
493
504
  });
494
505
  }
@@ -496,6 +507,8 @@ export async function resolvePageState(input, root, entryUrl, ctx) {
496
507
  }
497
508
  if (input.routes && input.routes.length > 0)
498
509
  out.routes = resolveRouteMocks(input.routes);
510
+ if (input.actionTimeoutMs !== undefined)
511
+ out.actionTimeout = input.actionTimeoutMs;
499
512
  if (input.actions && input.actions.length > 0) {
500
513
  out.actions = [];
501
514
  let current = entryUrl;
@@ -503,13 +516,13 @@ export async function resolvePageState(input, root, entryUrl, ctx) {
503
516
  const step = `actions[${i}] (${action.type})`;
504
517
  if (action.type === 'goto') {
505
518
  if (!action.url)
506
- throw new PlaywrightMcpError(`${step} needs a url`, 'INVALID_PATH');
519
+ throw new PlaywrightMcpError(`${step} needs a url`, 'INVALID_INPUT');
507
520
  let next;
508
521
  try {
509
522
  next = new URL(action.url, current).toString();
510
523
  }
511
524
  catch {
512
- throw new PlaywrightMcpError(`${step}: "${action.url}" is not a valid URL`, 'INVALID_PATH');
525
+ throw new PlaywrightMcpError(`${step}: "${action.url}" is not a valid URL`, 'INVALID_INPUT');
513
526
  }
514
527
  assertHttpUrl(next);
515
528
  await assertUrlAllowed(next);
@@ -518,10 +531,10 @@ export async function resolvePageState(input, root, entryUrl, ctx) {
518
531
  continue;
519
532
  }
520
533
  if (NEEDS_LOCATOR.has(action.type) && !action.locator) {
521
- throw new PlaywrightMcpError(`${step} needs a locator`, 'INVALID_PATH');
534
+ throw new PlaywrightMcpError(`${step} needs a locator`, 'INVALID_INPUT');
522
535
  }
523
536
  if ((action.type === 'press' || action.type === 'select') && action.value === undefined) {
524
- throw new PlaywrightMcpError(`${step} needs a value`, 'INVALID_PATH');
537
+ throw new PlaywrightMcpError(`${step} needs a value`, 'INVALID_INPUT');
525
538
  }
526
539
  const locator = action.locator ? parseLocator(action.locator) : undefined;
527
540
  out.actions.push({
@@ -613,7 +626,7 @@ export async function runBrowserScript(config, options) {
613
626
  try {
614
627
  const outcome = await runProcess(process.execPath, [scriptPath, configPath], {
615
628
  cwd: config.projectRoot,
616
- env: childEnv({ FORCE_COLOR: '0', ...serverless?.env }),
629
+ env: childEnv({ FORCE_COLOR: '0', ...serverless?.env, ...options.env }),
617
630
  timeoutMs: options.timeoutMs ?? 45_000,
618
631
  signal: options.signal,
619
632
  });
@@ -628,6 +641,26 @@ export async function runBrowserScript(config, options) {
628
641
  if (outcome.aborted) {
629
642
  return { ok: false, kind: 'CLIENT_DISCONNECT', error: 'Inspection aborted (client disconnected).' };
630
643
  }
644
+ // Checked before the child's output: killing it closes the browser, and the
645
+ // "browser has been closed" error it prints then is not a crash.
646
+ if (outcome.timedOut) {
647
+ const done = extractJsonFromText(outcome.stdout);
648
+ let steps;
649
+ try {
650
+ steps = done ? JSON.parse(done).actionsDone : undefined;
651
+ }
652
+ catch {
653
+ /* no usable output */
654
+ }
655
+ return {
656
+ ok: false,
657
+ kind: 'TIMEOUT',
658
+ error: `The whole call exceeded timeoutMs (${options.timeoutMs ?? 45_000}ms) and was stopped.`,
659
+ hint: 'Raise timeoutMs. It covers launch, navigation and every action, so keep it above the sum of the waits (each action waits up to actionTimeoutMs, default 10000).',
660
+ ...(steps?.length ? { actionsDone: steps } : {}),
661
+ stderrTail: tailText(outcome.stderr),
662
+ };
663
+ }
631
664
  const json = extractJsonFromText(outcome.stdout);
632
665
  if (json !== null) {
633
666
  try {
@@ -647,15 +680,6 @@ export async function runBrowserScript(config, options) {
647
680
  /* fall through to generic handling */
648
681
  }
649
682
  }
650
- if (outcome.timedOut) {
651
- return {
652
- ok: false,
653
- kind: 'TIMEOUT',
654
- error: `Page inspection exceeded ${options.timeoutMs ?? 45_000}ms and was killed.`,
655
- hint: 'The page may be blocking on network requests. Raise timeoutMs or use waitUntil: "domcontentloaded".',
656
- stderrTail: tailText(outcome.stderr),
657
- };
658
- }
659
683
  return {
660
684
  ok: false,
661
685
  kind: 'UNKNOWN',
@@ -681,12 +705,12 @@ export function assertHttpUrl(url) {
681
705
  parsed = new URL(trimmed);
682
706
  }
683
707
  catch {
684
- throw new PlaywrightMcpError(`"${url}" is not a valid URL`, 'INVALID_PATH', {
708
+ throw new PlaywrightMcpError(`"${url}" is not a valid URL`, 'INVALID_INPUT', {
685
709
  hint: 'Use a full URL, e.g. http://localhost:3000/login',
686
710
  });
687
711
  }
688
712
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
689
- throw new PlaywrightMcpError(`Unsupported URL scheme "${parsed.protocol}"`, 'INVALID_PATH', {
713
+ throw new PlaywrightMcpError(`Unsupported URL scheme "${parsed.protocol}"`, 'INVALID_INPUT', {
690
714
  hint: 'Only http:// and https:// URLs are supported.',
691
715
  });
692
716
  }