framewatch-mcp-server 0.2.0 → 0.2.1
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 +90 -37
- package/dist/engine/inspect.js.map +1 -1
- package/dist/engine/interaction.d.ts +1 -1
- package/dist/engine/interaction.js +1 -1
- package/dist/engine/interaction.js.map +1 -1
- package/dist/engine/vue.d.ts +1 -1
- package/dist/engine/vue.js +2 -2
- package/dist/engine/vue.js.map +1 -1
- package/dist/tools/capture.d.ts +1 -2
- package/dist/tools/capture.js +1 -2
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/interact.js +2 -2
- package/dist/tools/interact.js.map +1 -1
- package/dist/utils/format.d.ts +2 -2
- package/dist/utils/format.js +2 -2
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/snapshot-rules.d.ts +3 -1
- package/dist/utils/snapshot-rules.js +9 -3
- package/dist/utils/snapshot-rules.js.map +1 -1
- package/dist/utils/storage-state.d.ts +2 -2
- package/dist/utils/storage-state.js.map +1 -1
- package/dist/utils/style-rules.js +13 -1
- package/dist/utils/style-rules.js.map +1 -1
- package/package.json +2 -2
|
@@ -27,12 +27,18 @@ const INTERACTIVE_ROLES = new Set([
|
|
|
27
27
|
"tab",
|
|
28
28
|
"treeitem",
|
|
29
29
|
]);
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* `[ref=e8]` on a page's first document; `[ref=f1e8]` once the page has
|
|
32
|
+
* navigated, Playwright prefixing the frame ordinal. Both are refs.
|
|
33
|
+
*/
|
|
34
|
+
const REF_PATTERN = /\[ref=((?:f\d+)?e\d+)\]/;
|
|
31
35
|
const LINE_PATTERN = /^(\s*)- (\S+)(.*)$/;
|
|
32
|
-
/** A bare ref as the caller writes it: `e8
|
|
36
|
+
/** A bare ref as the caller writes it: `e8`, or `f1e8` after a navigation. */
|
|
33
37
|
export function isRef(value) {
|
|
34
|
-
return
|
|
38
|
+
return REF_SHAPE.test(value);
|
|
35
39
|
}
|
|
40
|
+
/** The shape of a ref, for input validation as well as `isRef`. */
|
|
41
|
+
export const REF_SHAPE = /^(?:f\d+)?e\d+$/;
|
|
36
42
|
/** The Playwright selector that resolves a snapshot ref. */
|
|
37
43
|
export function refSelector(ref) {
|
|
38
44
|
return `aria-ref=${ref}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot-rules.js","sourceRoot":"","sources":["../../src/utils/snapshot-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,kGAAkG;AAClG,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IAChC,QAAQ;IACR,MAAM;IACN,SAAS;IACT,WAAW;IACX,UAAU;IACV,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,YAAY;IACZ,UAAU;IACV,SAAS;IACT,QAAQ;IACR,UAAU;IACV,kBAAkB;IAClB,eAAe;IACf,KAAK;IACL,UAAU;CACX,CAAC,CAAC;AAEH,MAAM,WAAW,GAAG,
|
|
1
|
+
{"version":3,"file":"snapshot-rules.js","sourceRoot":"","sources":["../../src/utils/snapshot-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `framewatch_snapshot` does with the tree Playwright hands it.\n *\n * Playwright's AI-mode aria snapshot is YAML-ish text, one element per line,\n * indented by depth, each carrying a `[ref=eN]` that `aria-ref=eN` resolves\n * back to the element. Everything here is text in, text out — no browser —\n * so the counting, the filtering and the truncation can be tested in\n * milliseconds against a tree written by hand.\n */\n\n/** Roles a person can act on. A line with one of these, or a pointer cursor, is \"interactive\". */\nconst INTERACTIVE_ROLES = new Set([\n \"button\",\n \"link\",\n \"textbox\",\n \"searchbox\",\n \"checkbox\",\n \"radio\",\n \"switch\",\n \"slider\",\n \"spinbutton\",\n \"combobox\",\n \"listbox\",\n \"option\",\n \"menuitem\",\n \"menuitemcheckbox\",\n \"menuitemradio\",\n \"tab\",\n \"treeitem\",\n]);\n\n/**\n * `[ref=e8]` on a page's first document; `[ref=f1e8]` once the page has\n * navigated, Playwright prefixing the frame ordinal. Both are refs.\n */\nconst REF_PATTERN = /\\[ref=((?:f\\d+)?e\\d+)\\]/;\nconst LINE_PATTERN = /^(\\s*)- (\\S+)(.*)$/;\n\n/** A bare ref as the caller writes it: `e8`, or `f1e8` after a navigation. */\nexport function isRef(value: string): boolean {\n return REF_SHAPE.test(value);\n}\n\n/** The shape of a ref, for input validation as well as `isRef`. */\nexport const REF_SHAPE = /^(?:f\\d+)?e\\d+$/;\n\n/** The Playwright selector that resolves a snapshot ref. */\nexport function refSelector(ref: string): string {\n return `aria-ref=${ref}`;\n}\n\nexport interface SnapshotCounts {\n /** Lines carrying a ref — every element the snapshot can name. */\n elements: number;\n /** Of those, the ones a person can act on. */\n interactive: number;\n}\n\nexport function countSnapshot(tree: string): SnapshotCounts {\n let elements = 0;\n let interactive = 0;\n for (const line of tree.split(\"\\n\")) {\n if (!REF_PATTERN.test(line)) continue;\n elements++;\n if (isInteractiveLine(line)) interactive++;\n }\n return { elements, interactive };\n}\n\n/**\n * The interactive lines only, flattened to one level and stripped of the\n * trailing colon a parent line carries. A link's `/url` child, which is the\n * only child worth keeping, is folded onto the link's own line.\n */\nexport function interactiveOnly(tree: string): string {\n const lines = tree.split(\"\\n\");\n const out: string[] = [];\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (!REF_PATTERN.test(line) || !isInteractiveLine(line)) continue;\n let text = line.trim().replace(/:$/, \"\");\n const url = childUrl(lines, i);\n if (url !== undefined) text += ` → ${url}`;\n out.push(text);\n }\n return out.join(\"\\n\");\n}\n\nexport interface TruncatedSnapshot {\n text: string;\n /** Whole lines dropped from the end to fit. */\n cut_lines: number;\n}\n\n/** Cut the tree to at most `maxChars`, on a line boundary, and say how many lines went. */\nexport function truncateSnapshot(tree: string, maxChars: number): TruncatedSnapshot {\n if (tree.length <= maxChars) return { text: tree, cut_lines: 0 };\n const lines = tree.split(\"\\n\");\n const kept: string[] = [];\n let length = 0;\n for (const line of lines) {\n const next = length + line.length + (kept.length > 0 ? 1 : 0);\n if (next > maxChars) break;\n kept.push(line);\n length = next;\n }\n return { text: kept.join(\"\\n\"), cut_lines: lines.length - kept.length };\n}\n\nfunction isInteractiveLine(line: string): boolean {\n const match = LINE_PATTERN.exec(line);\n if (!match) return false;\n const role = match[2];\n if (INTERACTIVE_ROLES.has(role)) return true;\n return line.includes(\"[cursor=pointer]\");\n}\n\n/** The `/url` line directly under `index`, when the element has one. */\nfunction childUrl(lines: string[], index: number): string | undefined {\n const own = lines[index].search(/\\S/);\n for (let j = index + 1; j < lines.length; j++) {\n const line = lines[j];\n const indent = line.search(/\\S/);\n if (indent === -1 || indent <= own) break;\n const url = /^\\s*- \\/url: (.*)$/.exec(line);\n if (url) return url[1].trim();\n }\n return undefined;\n}\n"]}
|
|
@@ -7,8 +7,8 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
|
7
7
|
* This is exactly what Playwright's `context.storageState()` returns and what
|
|
8
8
|
* `browser.newContext({ storageState })` accepts, which is the whole point:
|
|
9
9
|
* `framewatch_save_auth` runs a login once, writes this to disk, and every
|
|
10
|
-
* other tool loads it instead of replaying the flow (see
|
|
11
|
-
*
|
|
10
|
+
* other tool loads it instead of replaying the flow (see the README,
|
|
11
|
+
* framewatch_save_auth).
|
|
12
12
|
*
|
|
13
13
|
* The type is spelled out rather than imported because Playwright exports it
|
|
14
14
|
* only as the return type of a method, and the tools need to name it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-state.js","sourceRoot":"","sources":["../../src/utils/storage-state.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { z } from \"zod\";\nimport type { BrowserContext, Page } from \"playwright\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { DEFAULT_AUTH_STATE_PATH } from \"../constants.js\";\n\n/**\n * Saved browser state — cookies plus per-origin localStorage.\n *\n * This is exactly what Playwright's `context.storageState()` returns and what\n * `browser.newContext({ storageState })` accepts, which is the whole point:\n * `framewatch_save_auth` runs a login once, writes this to disk, and every\n * other tool loads it instead of replaying the flow (see CLAUDE.md\n * \"Auth persistence\").\n *\n * The type is spelled out rather than imported because Playwright exports it\n * only as the return type of a method, and the tools need to name it.\n */\nexport type StorageState = Awaited<ReturnType<BrowserContext[\"storageState\"]>>;\n\n/**\n * Read a state file written by `framewatch_save_auth`.\n *\n * Every failure — no file, unreadable file, not JSON, JSON of the wrong shape\n * — comes back as one line naming the path and the tool that produces a good\n * one. A stale or missing auth file is the single most likely thing to go\n * wrong in an authenticated capture, and \"ENOENT: no such file or directory,\n * open '…'\" is not what the reader needs to see.\n */\nexport async function readStorageState(path: string): Promise<StorageState> {\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n const reason =\n code === \"ENOENT\"\n ? \"not found\"\n : code === \"EISDIR\"\n ? \"is a directory, not a file\"\n : code === \"EACCES\"\n ? \"could not be read (permission denied)\"\n : `could not be read (${firstLine(error)})`;\n throw new Error(`${prefix(path)} ${reason}. ${RECREATE}`);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n throw new Error(`${prefix(path)} is not valid JSON (${firstLine(error)}). ${RECREATE}`);\n }\n\n if (!isStorageState(parsed)) {\n throw new Error(\n `${prefix(path)} is not a browser state file — it has no \\`cookies\\` and \\`origins\\` arrays. ${RECREATE}`,\n );\n }\n return parsed;\n}\n\n/** The word that turns automatic pickup off for one call. */\nexport const NO_STORAGE_STATE = \"none\";\n\n/** Where the saved auth lives unless `FRAMEWATCH_AUTH_STATE` says otherwise. */\nexport function defaultAuthPath(env: NodeJS.ProcessEnv = process.env, cwd: string = process.cwd()): string {\n const override = env.FRAMEWATCH_AUTH_STATE;\n return override && override.trim() !== \"\" ? override : join(cwd, DEFAULT_AUTH_STATE_PATH);\n}\n\n/** A state that is about to be applied to a page, and where it came from. */\nexport interface ResolvedAuth {\n path: string;\n state: StorageState;\n /** Picked up from the default location without being asked for. */\n auto: boolean;\n /** The file's modification time, when it could be read. */\n saved_at?: Date;\n}\n\n/**\n * The auth to apply for one call.\n *\n * An explicit path is read as it always was, and a missing one still fails\n * loudly. With nothing asked for, the default file is used when it exists —\n * the agent then cannot forget to pass it, which is the whole point — and\n * `\"none\"` opens the page signed out. A default file that cannot be read is an\n * error rather than a silent sign-out, because \"the page showed the login\n * form\" would be blamed on the app.\n */\nexport async function resolveStorageState(\n requested: string | undefined,\n env: NodeJS.ProcessEnv = process.env,\n cwd: string = process.cwd(),\n): Promise<ResolvedAuth | null> {\n if (requested === NO_STORAGE_STATE) return null;\n if (requested !== undefined && requested.trim() !== \"\") {\n return { path: requested, state: await readStorageState(requested), auto: false, ...(await savedAt(requested)) };\n }\n const path = defaultAuthPath(env, cwd);\n let exists: boolean;\n try {\n exists = (await stat(path)).isFile();\n } catch {\n exists = false;\n }\n if (!exists) return null;\n try {\n return { path, state: await readStorageState(path), auto: true, ...(await savedAt(path)) };\n } catch (error) {\n throw new Error(`${firstLine(error)} It was picked up automatically; pass storage_state: \"${NO_STORAGE_STATE}\" to open the page without it.`);\n }\n}\n\nasync function savedAt(path: string): Promise<{ saved_at?: Date }> {\n try {\n return { saved_at: (await stat(path)).mtime };\n } catch {\n return {};\n }\n}\n\n/**\n * The line a tool prints about the auth it applied, or null when there is\n * nothing to say (an explicit file that worked). A login form on the page\n * after restoring a session is the expiry signal, and it is named as such\n * whether the file was automatic or not.\n */\nexport function describeAuth(auth: ResolvedAuth, loginVisible: boolean): string | null {\n const when = auth.saved_at ? ` (saved ${ago(auth.saved_at)})` : \"\";\n if (loginVisible) {\n return `Auth: ${auth.path}${when} did not sign you in — this page shows a login form. Run framewatch_save_auth again to refresh it.`;\n }\n if (!auth.auto) return null;\n return `Auth: using ${auth.path}${when} — pass storage_state: \"${NO_STORAGE_STATE}\" to open the page signed out.`;\n}\n\n/** Whether the page is showing a login form: a visible password field. */\nexport async function loginFormVisible(page: Page): Promise<boolean> {\n try {\n return await page.evaluate(() => {\n const doc = (globalThis as any).document;\n if (!doc) return false;\n const fields = doc.querySelectorAll('input[type=\"password\"]');\n for (let i = 0; i < fields.length; i++) {\n const rect = fields[i].getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) return true;\n }\n return false;\n });\n } catch {\n return false;\n }\n}\n\n/** The text block for `describeAuth`, when there is one. */\nexport function authNote(auth: ResolvedAuth | null, loginVisible: boolean): { type: \"text\"; text: string } | null {\n if (!auth) return null;\n const text = describeAuth(auth, loginVisible);\n return text ? { type: \"text\", text } : null;\n}\n\n/** A tool result with the auth line appended, when there is one. Error results are left alone. */\nexport function withAuthNote(result: CallToolResult, auth: ResolvedAuth | null, loginVisible = false): CallToolResult {\n const note = authNote(auth, loginVisible);\n if (!note || result.isError) return result;\n return { ...result, content: [...result.content, note] };\n}\n\nfunction ago(date: Date): string {\n const seconds = Math.max(0, Math.round((Date.now() - date.getTime()) / 1000));\n if (seconds < 60) return \"just now\";\n const minutes = Math.round(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.round(minutes / 60);\n if (hours < 48) return `${hours}h ago`;\n return `${Math.round(hours / 24)}d ago`;\n}\n\n/** Write a state file, creating the directories on the way to it. */\nexport async function writeStorageState(state: StorageState, path: string): Promise<void> {\n const parent = dirname(path);\n if (parent) await mkdir(parent, { recursive: true });\n await writeFile(path, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n}\n\n/** One phrase describing what a state actually carries, for the tool's summary. */\nexport function storageStateSummary(state: StorageState): string {\n const cookies = state.cookies.length;\n const origins = state.origins.length;\n const keys = state.origins.reduce((total, origin) => total + origin.localStorage.length, 0);\n if (cookies === 0 && keys === 0) return \"no cookies and nothing in storage\";\n return `${count(cookies, \"cookie\")}, ${count(origins, \"origin\")} with ${count(keys, \"stored key\")}`;\n}\n\nfunction count(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`;\n}\n\nconst RECREATE = \"Run framewatch_save_auth to create it.\";\n\nfunction prefix(path: string): string {\n return `Auth state file \"${path}\"`;\n}\n\nfunction firstLine(error: unknown): string {\n return (error instanceof Error ? error.message : String(error)).split(\"\\n\")[0];\n}\n\nfunction isStorageState(value: unknown): value is StorageState {\n if (typeof value !== \"object\" || value === null) return false;\n const { cookies, origins } = value as { cookies?: unknown; origins?: unknown };\n return Array.isArray(cookies) && Array.isArray(origins);\n}\n\n/**\n * The `storage_state` input every page-opening tool accepts. One definition so\n * the wording (and therefore what an agent believes it does) is the same in\n * all of them.\n */\nexport const storageStateField = z\n .string()\n .optional()\n .describe(\n \"Path to an auth state file saved by framewatch_save_auth. The page opens with those cookies and \" +\n \"storage already in place, so a login/gate does not have to be replayed. Omit it and the default file \" +\n \"(.framewatch/auth.json) is used automatically when it exists; pass \\\"none\\\" to open the page signed out. \" +\n \"If the saved session has expired the result says so — re-run framewatch_save_auth then.\",\n );\n"]}
|
|
1
|
+
{"version":3,"file":"storage-state.js","sourceRoot":"","sources":["../../src/utils/storage-state.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { z } from \"zod\";\nimport type { BrowserContext, Page } from \"playwright\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { DEFAULT_AUTH_STATE_PATH } from \"../constants.js\";\n\n/**\n * Saved browser state — cookies plus per-origin localStorage.\n *\n * This is exactly what Playwright's `context.storageState()` returns and what\n * `browser.newContext({ storageState })` accepts, which is the whole point:\n * `framewatch_save_auth` runs a login once, writes this to disk, and every\n * other tool loads it instead of replaying the flow (see the README,\n * framewatch_save_auth).\n *\n * The type is spelled out rather than imported because Playwright exports it\n * only as the return type of a method, and the tools need to name it.\n */\nexport type StorageState = Awaited<ReturnType<BrowserContext[\"storageState\"]>>;\n\n/**\n * Read a state file written by `framewatch_save_auth`.\n *\n * Every failure — no file, unreadable file, not JSON, JSON of the wrong shape\n * — comes back as one line naming the path and the tool that produces a good\n * one. A stale or missing auth file is the single most likely thing to go\n * wrong in an authenticated capture, and \"ENOENT: no such file or directory,\n * open '…'\" is not what the reader needs to see.\n */\nexport async function readStorageState(path: string): Promise<StorageState> {\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n const reason =\n code === \"ENOENT\"\n ? \"not found\"\n : code === \"EISDIR\"\n ? \"is a directory, not a file\"\n : code === \"EACCES\"\n ? \"could not be read (permission denied)\"\n : `could not be read (${firstLine(error)})`;\n throw new Error(`${prefix(path)} ${reason}. ${RECREATE}`);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n throw new Error(`${prefix(path)} is not valid JSON (${firstLine(error)}). ${RECREATE}`);\n }\n\n if (!isStorageState(parsed)) {\n throw new Error(\n `${prefix(path)} is not a browser state file — it has no \\`cookies\\` and \\`origins\\` arrays. ${RECREATE}`,\n );\n }\n return parsed;\n}\n\n/** The word that turns automatic pickup off for one call. */\nexport const NO_STORAGE_STATE = \"none\";\n\n/** Where the saved auth lives unless `FRAMEWATCH_AUTH_STATE` says otherwise. */\nexport function defaultAuthPath(env: NodeJS.ProcessEnv = process.env, cwd: string = process.cwd()): string {\n const override = env.FRAMEWATCH_AUTH_STATE;\n return override && override.trim() !== \"\" ? override : join(cwd, DEFAULT_AUTH_STATE_PATH);\n}\n\n/** A state that is about to be applied to a page, and where it came from. */\nexport interface ResolvedAuth {\n path: string;\n state: StorageState;\n /** Picked up from the default location without being asked for. */\n auto: boolean;\n /** The file's modification time, when it could be read. */\n saved_at?: Date;\n}\n\n/**\n * The auth to apply for one call.\n *\n * An explicit path is read as it always was, and a missing one still fails\n * loudly. With nothing asked for, the default file is used when it exists —\n * the agent then cannot forget to pass it, which is the whole point — and\n * `\"none\"` opens the page signed out. A default file that cannot be read is an\n * error rather than a silent sign-out, because \"the page showed the login\n * form\" would be blamed on the app.\n */\nexport async function resolveStorageState(\n requested: string | undefined,\n env: NodeJS.ProcessEnv = process.env,\n cwd: string = process.cwd(),\n): Promise<ResolvedAuth | null> {\n if (requested === NO_STORAGE_STATE) return null;\n if (requested !== undefined && requested.trim() !== \"\") {\n return { path: requested, state: await readStorageState(requested), auto: false, ...(await savedAt(requested)) };\n }\n const path = defaultAuthPath(env, cwd);\n let exists: boolean;\n try {\n exists = (await stat(path)).isFile();\n } catch {\n exists = false;\n }\n if (!exists) return null;\n try {\n return { path, state: await readStorageState(path), auto: true, ...(await savedAt(path)) };\n } catch (error) {\n throw new Error(`${firstLine(error)} It was picked up automatically; pass storage_state: \"${NO_STORAGE_STATE}\" to open the page without it.`);\n }\n}\n\nasync function savedAt(path: string): Promise<{ saved_at?: Date }> {\n try {\n return { saved_at: (await stat(path)).mtime };\n } catch {\n return {};\n }\n}\n\n/**\n * The line a tool prints about the auth it applied, or null when there is\n * nothing to say (an explicit file that worked). A login form on the page\n * after restoring a session is the expiry signal, and it is named as such\n * whether the file was automatic or not.\n */\nexport function describeAuth(auth: ResolvedAuth, loginVisible: boolean): string | null {\n const when = auth.saved_at ? ` (saved ${ago(auth.saved_at)})` : \"\";\n if (loginVisible) {\n return `Auth: ${auth.path}${when} did not sign you in — this page shows a login form. Run framewatch_save_auth again to refresh it.`;\n }\n if (!auth.auto) return null;\n return `Auth: using ${auth.path}${when} — pass storage_state: \"${NO_STORAGE_STATE}\" to open the page signed out.`;\n}\n\n/** Whether the page is showing a login form: a visible password field. */\nexport async function loginFormVisible(page: Page): Promise<boolean> {\n try {\n return await page.evaluate(() => {\n const doc = (globalThis as any).document;\n if (!doc) return false;\n const fields = doc.querySelectorAll('input[type=\"password\"]');\n for (let i = 0; i < fields.length; i++) {\n const rect = fields[i].getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) return true;\n }\n return false;\n });\n } catch {\n return false;\n }\n}\n\n/** The text block for `describeAuth`, when there is one. */\nexport function authNote(auth: ResolvedAuth | null, loginVisible: boolean): { type: \"text\"; text: string } | null {\n if (!auth) return null;\n const text = describeAuth(auth, loginVisible);\n return text ? { type: \"text\", text } : null;\n}\n\n/** A tool result with the auth line appended, when there is one. Error results are left alone. */\nexport function withAuthNote(result: CallToolResult, auth: ResolvedAuth | null, loginVisible = false): CallToolResult {\n const note = authNote(auth, loginVisible);\n if (!note || result.isError) return result;\n return { ...result, content: [...result.content, note] };\n}\n\nfunction ago(date: Date): string {\n const seconds = Math.max(0, Math.round((Date.now() - date.getTime()) / 1000));\n if (seconds < 60) return \"just now\";\n const minutes = Math.round(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.round(minutes / 60);\n if (hours < 48) return `${hours}h ago`;\n return `${Math.round(hours / 24)}d ago`;\n}\n\n/** Write a state file, creating the directories on the way to it. */\nexport async function writeStorageState(state: StorageState, path: string): Promise<void> {\n const parent = dirname(path);\n if (parent) await mkdir(parent, { recursive: true });\n await writeFile(path, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n}\n\n/** One phrase describing what a state actually carries, for the tool's summary. */\nexport function storageStateSummary(state: StorageState): string {\n const cookies = state.cookies.length;\n const origins = state.origins.length;\n const keys = state.origins.reduce((total, origin) => total + origin.localStorage.length, 0);\n if (cookies === 0 && keys === 0) return \"no cookies and nothing in storage\";\n return `${count(cookies, \"cookie\")}, ${count(origins, \"origin\")} with ${count(keys, \"stored key\")}`;\n}\n\nfunction count(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`;\n}\n\nconst RECREATE = \"Run framewatch_save_auth to create it.\";\n\nfunction prefix(path: string): string {\n return `Auth state file \"${path}\"`;\n}\n\nfunction firstLine(error: unknown): string {\n return (error instanceof Error ? error.message : String(error)).split(\"\\n\")[0];\n}\n\nfunction isStorageState(value: unknown): value is StorageState {\n if (typeof value !== \"object\" || value === null) return false;\n const { cookies, origins } = value as { cookies?: unknown; origins?: unknown };\n return Array.isArray(cookies) && Array.isArray(origins);\n}\n\n/**\n * The `storage_state` input every page-opening tool accepts. One definition so\n * the wording (and therefore what an agent believes it does) is the same in\n * all of them.\n */\nexport const storageStateField = z\n .string()\n .optional()\n .describe(\n \"Path to an auth state file saved by framewatch_save_auth. The page opens with those cookies and \" +\n \"storage already in place, so a login/gate does not have to be replayed. Omit it and the default file \" +\n \"(.framewatch/auth.json) is used automatically when it exists; pass \\\"none\\\" to open the page signed out. \" +\n \"If the saved session has expired the result says so — re-run framewatch_save_auth then.\",\n );\n"]}
|
|
@@ -115,6 +115,17 @@ export function describeAlignment(box, parent, previous) {
|
|
|
115
115
|
parts.push(`${Math.round(left)}px from parent's left, ${Math.round(right)}px from parent's right`);
|
|
116
116
|
}
|
|
117
117
|
if (previous) {
|
|
118
|
+
const sideGap = box.x - (previous.x + previous.width);
|
|
119
|
+
if (sideGap >= -ALIGN_TOLERANCE_PX) {
|
|
120
|
+
// Side by side (a row of buttons): the gap is horizontal and what can
|
|
121
|
+
// be off is the vertical alignment.
|
|
122
|
+
const drop = box.y - previous.y;
|
|
123
|
+
const vertical = Math.abs(drop) <= ALIGN_TOLERANCE_PX
|
|
124
|
+
? "top-aligned with it"
|
|
125
|
+
: `${Math.round(Math.abs(drop))}px ${drop > 0 ? "lower" : "higher"} than it`;
|
|
126
|
+
parts.push(`${Math.round(Math.max(0, sideGap))}px right of previous sibling, ${vertical}`);
|
|
127
|
+
return parts.join("; ");
|
|
128
|
+
}
|
|
118
129
|
const gap = box.y - (previous.y + previous.height);
|
|
119
130
|
const vertical = gap >= 0 ? `${Math.round(gap)}px below previous sibling` : `overlaps previous sibling by ${Math.round(-gap)}px`;
|
|
120
131
|
const shift = box.x - previous.x;
|
|
@@ -200,7 +211,8 @@ function describeBorder(border) {
|
|
|
200
211
|
if (width === 0 || match[2] === "none")
|
|
201
212
|
return "none";
|
|
202
213
|
const colour = parseColour(match[3]);
|
|
203
|
-
|
|
214
|
+
const shown = colour ? (colour.a === 0 ? "transparent" : toHex(colour)) : match[3];
|
|
215
|
+
return `${match[1]}px ${match[2]} ${shown}`;
|
|
204
216
|
}
|
|
205
217
|
export function formatInventory(inventory) {
|
|
206
218
|
const lines = [`Design inventory — ${inventory.elements} elements, ${inventory.text_elements} with text`];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style-rules.js","sourceRoot":"","sources":["../../src/utils/style-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BoundingBox } from \"../types.js\";\n\n/**\n * What `framewatch_inspect` says about a measurement.\n *\n * One element's computed style and geometry in, lines of wording out. Nothing\n * here touches a browser: the colour maths (WCAG contrast), the alignment\n * descriptions and the formatting are all decided on plain numbers, so every\n * verdict can be tested in milliseconds and the browser tests only have to\n * prove the numbers are real.\n */\n\nexport interface Rgba {\n r: number;\n g: number;\n b: number;\n /** 0..1 */\n a: number;\n}\n\n/** A colour as `getComputedStyle` prints it (`rgb()` / `rgba()`), or as a hex literal, or `transparent`. */\nexport function parseColour(css: string): Rgba | null {\n const value = css.trim().toLowerCase();\n if (value === \"transparent\") return { r: 0, g: 0, b: 0, a: 0 };\n\n const fn = /^rgba?\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*(?:,\\s*([\\d.]+)\\s*)?\\)$/.exec(value);\n if (fn) {\n return {\n r: Math.round(Number(fn[1])),\n g: Math.round(Number(fn[2])),\n b: Math.round(Number(fn[3])),\n a: fn[4] === undefined ? 1 : Number(fn[4]),\n };\n }\n // The space-separated modern syntax, which Chromium uses for colours outside sRGB.\n const modern = /^rgba?\\(\\s*([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s*(?:\\/\\s*([\\d.]+%?)\\s*)?\\)$/.exec(value);\n if (modern) {\n const alpha = modern[4] === undefined ? 1 : modern[4].endsWith(\"%\") ? Number(modern[4].slice(0, -1)) / 100 : Number(modern[4]);\n return { r: Math.round(Number(modern[1])), g: Math.round(Number(modern[2])), b: Math.round(Number(modern[3])), a: alpha };\n }\n\n const hex = /^#([0-9a-f]{3,8})$/.exec(value);\n if (hex) {\n const digits = hex[1];\n if (digits.length === 3 || digits.length === 4) {\n const [r, g, b, a] = digits.split(\"\").map((d) => parseInt(d + d, 16));\n return { r, g, b, a: digits.length === 4 ? a / 255 : 1 };\n }\n if (digits.length === 6 || digits.length === 8) {\n const r = parseInt(digits.slice(0, 2), 16);\n const g = parseInt(digits.slice(2, 4), 16);\n const b = parseInt(digits.slice(4, 6), 16);\n const a = digits.length === 8 ? parseInt(digits.slice(6, 8), 16) / 255 : 1;\n return { r, g, b, a };\n }\n }\n return null;\n}\n\n/** `top` painted over `bottom`, the way the browser composites a translucent background. */\nexport function compositeOver(top: Rgba, bottom: Rgba): Rgba {\n if (top.a >= 1) return { ...top };\n if (top.a <= 0) return { ...bottom };\n const a = top.a + bottom.a * (1 - top.a);\n const mix = (t: number, b: number): number => Math.round((t * top.a + b * bottom.a * (1 - top.a)) / a);\n return { r: mix(top.r, bottom.r), g: mix(top.g, bottom.g), b: mix(top.b, bottom.b), a };\n}\n\nexport function toHex(colour: Rgba): string {\n const part = (n: number): string => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, \"0\");\n const rgb = `#${part(colour.r)}${part(colour.g)}${part(colour.b)}`;\n return colour.a >= 1 ? rgb : `${rgb}${part(colour.a * 255)}`;\n}\n\n/** WCAG 2 relative luminance of an opaque colour. */\nexport function relativeLuminance(colour: Rgba): number {\n const channel = (n: number): number => {\n const s = n / 255;\n return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n };\n return 0.2126 * channel(colour.r) + 0.7152 * channel(colour.g) + 0.0722 * channel(colour.b);\n}\n\n/** WCAG 2 contrast ratio, 1..21, whichever way round the colours are given. */\nexport function contrastRatio(a: Rgba, b: Rgba): number {\n const la = relativeLuminance(a);\n const lb = relativeLuminance(b);\n const [light, dark] = la >= lb ? [la, lb] : [lb, la];\n return (light + 0.05) / (dark + 0.05);\n}\n\n/** WCAG's \"large text\": 24px, or 18.66px (14pt) when bold. */\nexport function isLargeText(fontSizePx: number, fontWeight: number): boolean {\n if (fontSizePx >= 24) return true;\n return fontWeight >= 700 && fontSizePx >= 18.66;\n}\n\nexport type ContrastVerdict = \"AAA\" | \"AA\" | \"fail\";\n\nexport function contrastVerdict(ratio: number, large: boolean): ContrastVerdict {\n const aaa = large ? 4.5 : 7;\n const aa = large ? 3 : 4.5;\n if (ratio >= aaa) return \"AAA\";\n if (ratio >= aa) return \"AA\";\n return \"fail\";\n}\n\nexport function describeContrast(ratio: number, large: boolean): string {\n const verdict = contrastVerdict(ratio, large);\n const size = large ? \"large text\" : \"normal text\";\n const shown = `contrast ${ratio.toFixed(2)}:1`;\n if (verdict === \"fail\") return `${shown} — fails AA for ${size} (needs ${large ? \"3\" : \"4.5\"}:1)`;\n return `${shown} — passes ${verdict} for ${size}`;\n}\n\n/** Positions within a pixel of each other count as the same: layout rounds, and so does the reader. */\nconst ALIGN_TOLERANCE_PX = 1;\n\n/**\n * Where a box sits relative to its parent, and to the sibling before it.\n * Horizontal first (flush, centred, or the inset from each side), then the\n * vertical relationship to the previous sibling.\n */\nexport function describeAlignment(box: BoundingBox, parent: BoundingBox, previous?: BoundingBox): string {\n const parts: string[] = [];\n\n const left = box.x - parent.x;\n const right = parent.x + parent.width - (box.x + box.width);\n if (Math.abs(left) <= ALIGN_TOLERANCE_PX && Math.abs(right) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"fills parent's width\");\n } else if (Math.abs(left - right) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"horizontally centred in parent\");\n } else if (Math.abs(left) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"left edge flush with parent\");\n } else if (Math.abs(right) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"right edge flush with parent\");\n } else {\n parts.push(`${Math.round(left)}px from parent's left, ${Math.round(right)}px from parent's right`);\n }\n\n if (previous) {\n const gap = box.y - (previous.y + previous.height);\n const vertical =\n gap >= 0 ? `${Math.round(gap)}px below previous sibling` : `overlaps previous sibling by ${Math.round(-gap)}px`;\n const shift = box.x - previous.x;\n const horizontal =\n Math.abs(shift) <= ALIGN_TOLERANCE_PX\n ? \"left-aligned with it\"\n : `${Math.round(Math.abs(shift))}px ${shift > 0 ? \"right\" : \"left\"} of its left edge`;\n parts.push(`${vertical}, ${horizontal}`);\n }\n\n return parts.join(\"; \");\n}\n\n/** One element, as measured inside the page. Colours are as `getComputedStyle` printed them. */\nexport interface ElementMeasurement {\n /** What the caller asked for: a ref (`e8`) or a selector. */\n target: string;\n tag: string;\n id?: string;\n classes: string[];\n role?: string;\n /** Accessible name, when the element has one. */\n name?: string;\n /** Own visible text, trimmed and elided. */\n text?: string;\n /** Viewport pixels. */\n box: BoundingBox;\n visible: boolean;\n in_viewport: \"fully\" | \"partially\" | \"none\";\n /** An ancestor with `overflow` other than visible cuts part of it off. */\n clipped: boolean;\n /** Its own content is wider or taller than the box it was given. */\n text_overflows: boolean;\n display: string;\n position: string;\n opacity: number;\n font: { family: string; size: number; weight: number; line_height: number | null; style: string };\n color: string;\n /** Effective background: its own, composited over its ancestors' until opaque. */\n background: string;\n /** top, right, bottom, left — pixels. */\n padding: [number, number, number, number];\n margin: [number, number, number, number];\n gap: number | null;\n /** Shorthand as computed, e.g. `1px solid rgb(0, 0, 0)` or `0px none rgb(0, 0, 0)`. */\n border: string;\n radius: string;\n parent?: { tag: string; box: BoundingBox };\n previous?: { tag: string; box: BoundingBox };\n}\n\n/** The lines `framewatch_inspect` prints for one element. */\nexport function formatInspection(m: ElementMeasurement, index: number): string[] {\n const lines: string[] = [];\n\n const identity = [m.target, m.role ?? m.tag, m.name !== undefined ? `\"${m.name}\"` : null].filter(Boolean).join(\" \");\n lines.push(`${index}. ${identity} — <${describeElement(m)}>`);\n\n const { box } = m;\n const centre = `${Math.round(box.x + box.width / 2)},${Math.round(box.y + box.height / 2)}`;\n const state: string[] = [];\n state.push(m.visible ? \"visible\" : `not visible${m.opacity === 0 ? \" (opacity 0)\" : \"\"}`);\n state.push(\n m.in_viewport === \"fully\"\n ? \"fully in viewport\"\n : m.in_viewport === \"partially\"\n ? \"partly outside the viewport\"\n : \"outside the viewport\",\n );\n if (m.clipped) state.push(\"clipped by an ancestor\");\n if (m.text_overflows) state.push(\"text overflows its box\");\n lines.push(` box: ${box.x},${box.y} ${box.width}x${box.height} (viewport px; centre ${centre}) — ${state.join(\", \")}`);\n\n lines.push(` text: ${describeText(m)}`);\n\n const spacing = [`padding ${sides(m.padding)}`, `margin ${sides(m.margin)}`];\n if (m.gap !== null) spacing.push(`gap ${m.gap}`);\n lines.push(` spacing: ${spacing.join(\"; \")}`);\n\n lines.push(` border: ${describeBorder(m.border)}; radius ${m.radius}`);\n\n const layout = [m.display];\n if (m.position !== \"static\") layout.push(`position ${m.position}`);\n if (m.parent) layout.push(describeAlignment(m.box, m.parent.box, m.previous?.box));\n lines.push(` layout: ${layout.join(\"; \")}`);\n\n return lines;\n}\n\nfunction describeElement(m: ElementMeasurement): string {\n let out = m.tag;\n if (m.id) out += `#${m.id}`;\n else if (m.classes.length > 0) out += `.${m.classes[0]}`;\n return out;\n}\n\nfunction describeText(m: ElementMeasurement): string {\n const font = m.font;\n const size = font.line_height !== null ? `${font.size}px/${font.line_height}px` : `${font.size}px`;\n const parts = [`${size} ${font.family} ${font.weight}${font.style !== \"normal\" ? ` ${font.style}` : \"\"}`];\n\n const fg = parseColour(m.color);\n const bg = parseColour(m.background);\n // A translucent text colour renders as a blend with what is behind it; that\n // blend is what the eye sees and what the contrast is rated on, so it is the\n // one printed, with the declared value beside it.\n const rendered = fg && bg && fg.a < 1 && bg.a >= 1 ? compositeOver(fg, bg) : fg;\n const shownFg = rendered ? (fg && fg.a < 1 && rendered !== fg ? `${toHex(rendered)} (${toHex(fg)} as declared)` : toHex(rendered)) : m.color;\n parts.push(`${shownFg} on ${bg ? toHex(bg) : m.background}`);\n\n const hasText = (m.text ?? \"\").length > 0 || (m.name ?? \"\").length > 0;\n if (hasText && rendered && bg && bg.a >= 1) {\n parts.push(describeContrast(contrastRatio(rendered, bg), isLargeText(font.size, font.weight)));\n }\n return parts.join(\" — \");\n}\n\n/** `8 16 8 16` → `8 16`, `8 8 8 8` → `8`, the way a stylesheet would write it. */\nfunction sides([top, right, bottom, left]: [number, number, number, number]): string {\n if (top === right && right === bottom && bottom === left) return String(top);\n if (top === bottom && right === left) return `${top} ${right}`;\n return `${top} ${right} ${bottom} ${left}`;\n}\n\nfunction describeBorder(border: string): string {\n const match = /^([\\d.]+)px\\s+(\\w+)\\s+(.+)$/.exec(border.trim());\n if (!match) return border || \"none\";\n const width = Number(match[1]);\n if (width === 0 || match[2] === \"none\") return \"none\";\n const colour = parseColour(match[3]);\n return `${match[1]}px ${match[2]} ${colour ? toHex(colour) : match[3]}`;\n}\n\n/* ── Page inventory ───────────────────────────────────────────────────── */\n\nexport interface Tally {\n value: string;\n count: number;\n}\n\n/** What the page is built from — every value in use, with how often. Most-used first. */\nexport interface PageInventory {\n elements: number;\n /** Elements with visible text of their own. */\n text_elements: number;\n fonts: Tally[];\n font_sizes: Tally[];\n font_weights: Tally[];\n text_colours: Tally[];\n backgrounds: Tally[];\n /** Non-zero padding, margin and gap values. */\n spacing: Tally[];\n radii: Tally[];\n}\n\nexport function formatInventory(inventory: PageInventory): string[] {\n const lines = [`Design inventory — ${inventory.elements} elements, ${inventory.text_elements} with text`];\n const row = (label: string, tallies: Tally[]): void => {\n if (tallies.length === 0) {\n lines.push(` ${label}: none`);\n return;\n }\n lines.push(` ${label} (${tallies.length}): ${tallies.map((t) => `${t.value} ×${t.count}`).join(\", \")}`);\n };\n row(\"fonts\", inventory.fonts);\n row(\"font sizes\", inventory.font_sizes);\n row(\"font weights\", inventory.font_weights);\n row(\"text colours\", inventory.text_colours);\n row(\"backgrounds\", inventory.backgrounds);\n row(\"spacing\", inventory.spacing);\n row(\"radii\", inventory.radii);\n return lines;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"style-rules.js","sourceRoot":"","sources":["../../src/utils/style-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BoundingBox } from \"../types.js\";\n\n/**\n * What `framewatch_inspect` says about a measurement.\n *\n * One element's computed style and geometry in, lines of wording out. Nothing\n * here touches a browser: the colour maths (WCAG contrast), the alignment\n * descriptions and the formatting are all decided on plain numbers, so every\n * verdict can be tested in milliseconds and the browser tests only have to\n * prove the numbers are real.\n */\n\nexport interface Rgba {\n r: number;\n g: number;\n b: number;\n /** 0..1 */\n a: number;\n}\n\n/** A colour as `getComputedStyle` prints it (`rgb()` / `rgba()`), or as a hex literal, or `transparent`. */\nexport function parseColour(css: string): Rgba | null {\n const value = css.trim().toLowerCase();\n if (value === \"transparent\") return { r: 0, g: 0, b: 0, a: 0 };\n\n const fn = /^rgba?\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*(?:,\\s*([\\d.]+)\\s*)?\\)$/.exec(value);\n if (fn) {\n return {\n r: Math.round(Number(fn[1])),\n g: Math.round(Number(fn[2])),\n b: Math.round(Number(fn[3])),\n a: fn[4] === undefined ? 1 : Number(fn[4]),\n };\n }\n // The space-separated modern syntax, which Chromium uses for colours outside sRGB.\n const modern = /^rgba?\\(\\s*([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s*(?:\\/\\s*([\\d.]+%?)\\s*)?\\)$/.exec(value);\n if (modern) {\n const alpha = modern[4] === undefined ? 1 : modern[4].endsWith(\"%\") ? Number(modern[4].slice(0, -1)) / 100 : Number(modern[4]);\n return { r: Math.round(Number(modern[1])), g: Math.round(Number(modern[2])), b: Math.round(Number(modern[3])), a: alpha };\n }\n\n const hex = /^#([0-9a-f]{3,8})$/.exec(value);\n if (hex) {\n const digits = hex[1];\n if (digits.length === 3 || digits.length === 4) {\n const [r, g, b, a] = digits.split(\"\").map((d) => parseInt(d + d, 16));\n return { r, g, b, a: digits.length === 4 ? a / 255 : 1 };\n }\n if (digits.length === 6 || digits.length === 8) {\n const r = parseInt(digits.slice(0, 2), 16);\n const g = parseInt(digits.slice(2, 4), 16);\n const b = parseInt(digits.slice(4, 6), 16);\n const a = digits.length === 8 ? parseInt(digits.slice(6, 8), 16) / 255 : 1;\n return { r, g, b, a };\n }\n }\n return null;\n}\n\n/** `top` painted over `bottom`, the way the browser composites a translucent background. */\nexport function compositeOver(top: Rgba, bottom: Rgba): Rgba {\n if (top.a >= 1) return { ...top };\n if (top.a <= 0) return { ...bottom };\n const a = top.a + bottom.a * (1 - top.a);\n const mix = (t: number, b: number): number => Math.round((t * top.a + b * bottom.a * (1 - top.a)) / a);\n return { r: mix(top.r, bottom.r), g: mix(top.g, bottom.g), b: mix(top.b, bottom.b), a };\n}\n\nexport function toHex(colour: Rgba): string {\n const part = (n: number): string => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, \"0\");\n const rgb = `#${part(colour.r)}${part(colour.g)}${part(colour.b)}`;\n return colour.a >= 1 ? rgb : `${rgb}${part(colour.a * 255)}`;\n}\n\n/** WCAG 2 relative luminance of an opaque colour. */\nexport function relativeLuminance(colour: Rgba): number {\n const channel = (n: number): number => {\n const s = n / 255;\n return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n };\n return 0.2126 * channel(colour.r) + 0.7152 * channel(colour.g) + 0.0722 * channel(colour.b);\n}\n\n/** WCAG 2 contrast ratio, 1..21, whichever way round the colours are given. */\nexport function contrastRatio(a: Rgba, b: Rgba): number {\n const la = relativeLuminance(a);\n const lb = relativeLuminance(b);\n const [light, dark] = la >= lb ? [la, lb] : [lb, la];\n return (light + 0.05) / (dark + 0.05);\n}\n\n/** WCAG's \"large text\": 24px, or 18.66px (14pt) when bold. */\nexport function isLargeText(fontSizePx: number, fontWeight: number): boolean {\n if (fontSizePx >= 24) return true;\n return fontWeight >= 700 && fontSizePx >= 18.66;\n}\n\nexport type ContrastVerdict = \"AAA\" | \"AA\" | \"fail\";\n\nexport function contrastVerdict(ratio: number, large: boolean): ContrastVerdict {\n const aaa = large ? 4.5 : 7;\n const aa = large ? 3 : 4.5;\n if (ratio >= aaa) return \"AAA\";\n if (ratio >= aa) return \"AA\";\n return \"fail\";\n}\n\nexport function describeContrast(ratio: number, large: boolean): string {\n const verdict = contrastVerdict(ratio, large);\n const size = large ? \"large text\" : \"normal text\";\n const shown = `contrast ${ratio.toFixed(2)}:1`;\n if (verdict === \"fail\") return `${shown} — fails AA for ${size} (needs ${large ? \"3\" : \"4.5\"}:1)`;\n return `${shown} — passes ${verdict} for ${size}`;\n}\n\n/** Positions within a pixel of each other count as the same: layout rounds, and so does the reader. */\nconst ALIGN_TOLERANCE_PX = 1;\n\n/**\n * Where a box sits relative to its parent, and to the sibling before it.\n * Horizontal first (flush, centred, or the inset from each side), then the\n * vertical relationship to the previous sibling.\n */\nexport function describeAlignment(box: BoundingBox, parent: BoundingBox, previous?: BoundingBox): string {\n const parts: string[] = [];\n\n const left = box.x - parent.x;\n const right = parent.x + parent.width - (box.x + box.width);\n if (Math.abs(left) <= ALIGN_TOLERANCE_PX && Math.abs(right) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"fills parent's width\");\n } else if (Math.abs(left - right) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"horizontally centred in parent\");\n } else if (Math.abs(left) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"left edge flush with parent\");\n } else if (Math.abs(right) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"right edge flush with parent\");\n } else {\n parts.push(`${Math.round(left)}px from parent's left, ${Math.round(right)}px from parent's right`);\n }\n\n if (previous) {\n const sideGap = box.x - (previous.x + previous.width);\n if (sideGap >= -ALIGN_TOLERANCE_PX) {\n // Side by side (a row of buttons): the gap is horizontal and what can\n // be off is the vertical alignment.\n const drop = box.y - previous.y;\n const vertical =\n Math.abs(drop) <= ALIGN_TOLERANCE_PX\n ? \"top-aligned with it\"\n : `${Math.round(Math.abs(drop))}px ${drop > 0 ? \"lower\" : \"higher\"} than it`;\n parts.push(`${Math.round(Math.max(0, sideGap))}px right of previous sibling, ${vertical}`);\n return parts.join(\"; \");\n }\n const gap = box.y - (previous.y + previous.height);\n const vertical =\n gap >= 0 ? `${Math.round(gap)}px below previous sibling` : `overlaps previous sibling by ${Math.round(-gap)}px`;\n const shift = box.x - previous.x;\n const horizontal =\n Math.abs(shift) <= ALIGN_TOLERANCE_PX\n ? \"left-aligned with it\"\n : `${Math.round(Math.abs(shift))}px ${shift > 0 ? \"right\" : \"left\"} of its left edge`;\n parts.push(`${vertical}, ${horizontal}`);\n }\n\n return parts.join(\"; \");\n}\n\n/** One element, as measured inside the page. Colours are as `getComputedStyle` printed them. */\nexport interface ElementMeasurement {\n /** What the caller asked for: a ref (`e8`) or a selector. */\n target: string;\n tag: string;\n id?: string;\n classes: string[];\n role?: string;\n /** Accessible name, when the element has one. */\n name?: string;\n /** Own visible text, trimmed and elided. */\n text?: string;\n /** Viewport pixels. */\n box: BoundingBox;\n visible: boolean;\n in_viewport: \"fully\" | \"partially\" | \"none\";\n /** An ancestor with `overflow` other than visible cuts part of it off. */\n clipped: boolean;\n /** Its own content is wider or taller than the box it was given. */\n text_overflows: boolean;\n display: string;\n position: string;\n opacity: number;\n font: { family: string; size: number; weight: number; line_height: number | null; style: string };\n color: string;\n /** Effective background: its own, composited over its ancestors' until opaque. */\n background: string;\n /** top, right, bottom, left — pixels. */\n padding: [number, number, number, number];\n margin: [number, number, number, number];\n gap: number | null;\n /** Shorthand as computed, e.g. `1px solid rgb(0, 0, 0)` or `0px none rgb(0, 0, 0)`. */\n border: string;\n radius: string;\n parent?: { tag: string; box: BoundingBox };\n previous?: { tag: string; box: BoundingBox };\n}\n\n/** The lines `framewatch_inspect` prints for one element. */\nexport function formatInspection(m: ElementMeasurement, index: number): string[] {\n const lines: string[] = [];\n\n const identity = [m.target, m.role ?? m.tag, m.name !== undefined ? `\"${m.name}\"` : null].filter(Boolean).join(\" \");\n lines.push(`${index}. ${identity} — <${describeElement(m)}>`);\n\n const { box } = m;\n const centre = `${Math.round(box.x + box.width / 2)},${Math.round(box.y + box.height / 2)}`;\n const state: string[] = [];\n state.push(m.visible ? \"visible\" : `not visible${m.opacity === 0 ? \" (opacity 0)\" : \"\"}`);\n state.push(\n m.in_viewport === \"fully\"\n ? \"fully in viewport\"\n : m.in_viewport === \"partially\"\n ? \"partly outside the viewport\"\n : \"outside the viewport\",\n );\n if (m.clipped) state.push(\"clipped by an ancestor\");\n if (m.text_overflows) state.push(\"text overflows its box\");\n lines.push(` box: ${box.x},${box.y} ${box.width}x${box.height} (viewport px; centre ${centre}) — ${state.join(\", \")}`);\n\n lines.push(` text: ${describeText(m)}`);\n\n const spacing = [`padding ${sides(m.padding)}`, `margin ${sides(m.margin)}`];\n if (m.gap !== null) spacing.push(`gap ${m.gap}`);\n lines.push(` spacing: ${spacing.join(\"; \")}`);\n\n lines.push(` border: ${describeBorder(m.border)}; radius ${m.radius}`);\n\n const layout = [m.display];\n if (m.position !== \"static\") layout.push(`position ${m.position}`);\n if (m.parent) layout.push(describeAlignment(m.box, m.parent.box, m.previous?.box));\n lines.push(` layout: ${layout.join(\"; \")}`);\n\n return lines;\n}\n\nfunction describeElement(m: ElementMeasurement): string {\n let out = m.tag;\n if (m.id) out += `#${m.id}`;\n else if (m.classes.length > 0) out += `.${m.classes[0]}`;\n return out;\n}\n\nfunction describeText(m: ElementMeasurement): string {\n const font = m.font;\n const size = font.line_height !== null ? `${font.size}px/${font.line_height}px` : `${font.size}px`;\n const parts = [`${size} ${font.family} ${font.weight}${font.style !== \"normal\" ? ` ${font.style}` : \"\"}`];\n\n const fg = parseColour(m.color);\n const bg = parseColour(m.background);\n // A translucent text colour renders as a blend with what is behind it; that\n // blend is what the eye sees and what the contrast is rated on, so it is the\n // one printed, with the declared value beside it.\n const rendered = fg && bg && fg.a < 1 && bg.a >= 1 ? compositeOver(fg, bg) : fg;\n const shownFg = rendered ? (fg && fg.a < 1 && rendered !== fg ? `${toHex(rendered)} (${toHex(fg)} as declared)` : toHex(rendered)) : m.color;\n parts.push(`${shownFg} on ${bg ? toHex(bg) : m.background}`);\n\n const hasText = (m.text ?? \"\").length > 0 || (m.name ?? \"\").length > 0;\n if (hasText && rendered && bg && bg.a >= 1) {\n parts.push(describeContrast(contrastRatio(rendered, bg), isLargeText(font.size, font.weight)));\n }\n return parts.join(\" — \");\n}\n\n/** `8 16 8 16` → `8 16`, `8 8 8 8` → `8`, the way a stylesheet would write it. */\nfunction sides([top, right, bottom, left]: [number, number, number, number]): string {\n if (top === right && right === bottom && bottom === left) return String(top);\n if (top === bottom && right === left) return `${top} ${right}`;\n return `${top} ${right} ${bottom} ${left}`;\n}\n\nfunction describeBorder(border: string): string {\n const match = /^([\\d.]+)px\\s+(\\w+)\\s+(.+)$/.exec(border.trim());\n if (!match) return border || \"none\";\n const width = Number(match[1]);\n if (width === 0 || match[2] === \"none\") return \"none\";\n const colour = parseColour(match[3]);\n const shown = colour ? (colour.a === 0 ? \"transparent\" : toHex(colour)) : match[3];\n return `${match[1]}px ${match[2]} ${shown}`;\n}\n\n/* ── Page inventory ───────────────────────────────────────────────────── */\n\nexport interface Tally {\n value: string;\n count: number;\n}\n\n/** What the page is built from — every value in use, with how often. Most-used first. */\nexport interface PageInventory {\n elements: number;\n /** Elements with visible text of their own. */\n text_elements: number;\n fonts: Tally[];\n font_sizes: Tally[];\n font_weights: Tally[];\n text_colours: Tally[];\n backgrounds: Tally[];\n /** Non-zero padding, margin and gap values. */\n spacing: Tally[];\n radii: Tally[];\n}\n\nexport function formatInventory(inventory: PageInventory): string[] {\n const lines = [`Design inventory — ${inventory.elements} elements, ${inventory.text_elements} with text`];\n const row = (label: string, tallies: Tally[]): void => {\n if (tallies.length === 0) {\n lines.push(` ${label}: none`);\n return;\n }\n lines.push(` ${label} (${tallies.length}): ${tallies.map((t) => `${t.value} ×${t.count}`).join(\", \")}`);\n };\n row(\"fonts\", inventory.fonts);\n row(\"font sizes\", inventory.font_sizes);\n row(\"font weights\", inventory.font_weights);\n row(\"text colours\", inventory.text_colours);\n row(\"backgrounds\", inventory.backgrounds);\n row(\"spacing\", inventory.spacing);\n row(\"radii\", inventory.radii);\n return lines;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framewatch-mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "MCP server for smart visual testing of web apps — frame capture, diffing, and interaction replay",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "kekoDev (https://github.com/kekoDev)",
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://framewatch.keko.dev",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/kekoDev/framewatch.git"
|