framewatch-mcp-server 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +895 -15
- package/dist/constants.d.ts +274 -0
- package/dist/constants.js +279 -0
- package/dist/constants.js.map +1 -1
- package/dist/engine/browser.d.ts +20 -4
- package/dist/engine/browser.js +26 -9
- package/dist/engine/browser.js.map +1 -1
- package/dist/engine/clicks.d.ts +221 -0
- package/dist/engine/clicks.js +801 -0
- package/dist/engine/clicks.js.map +1 -0
- package/dist/engine/forms.d.ts +137 -0
- package/dist/engine/forms.js +474 -0
- package/dist/engine/forms.js.map +1 -0
- package/dist/engine/hmr.d.ts +41 -0
- package/dist/engine/hmr.js +91 -0
- package/dist/engine/hmr.js.map +1 -0
- package/dist/engine/inspect.d.ts +31 -0
- package/dist/engine/inspect.js +383 -0
- package/dist/engine/inspect.js.map +1 -0
- package/dist/engine/interaction.d.ts +12 -7
- package/dist/engine/interaction.js +110 -18
- package/dist/engine/interaction.js.map +1 -1
- package/dist/engine/links.d.ts +134 -0
- package/dist/engine/links.js +384 -0
- package/dist/engine/links.js.map +1 -0
- package/dist/engine/mocks.d.ts +53 -0
- package/dist/engine/mocks.js +148 -0
- package/dist/engine/mocks.js.map +1 -0
- package/dist/engine/rtl.d.ts +129 -0
- package/dist/engine/rtl.js +540 -0
- package/dist/engine/rtl.js.map +1 -0
- package/dist/engine/seo.d.ts +189 -0
- package/dist/engine/seo.js +398 -0
- package/dist/engine/seo.js.map +1 -0
- package/dist/engine/snapshot.d.ts +29 -0
- package/dist/engine/snapshot.js +10 -0
- package/dist/engine/snapshot.js.map +1 -0
- package/dist/engine/vue.d.ts +54 -0
- package/dist/engine/vue.js +419 -0
- package/dist/engine/vue.js.map +1 -0
- package/dist/index.js +45 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/accessibility.d.ts +4 -0
- package/dist/tools/accessibility.js +9 -2
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/api-mock.d.ts +405 -0
- package/dist/tools/api-mock.js +186 -0
- package/dist/tools/api-mock.js.map +1 -0
- package/dist/tools/capture.d.ts +90 -26
- package/dist/tools/capture.js +109 -58
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/compare.d.ts +4 -0
- package/dist/tools/compare.js +16 -5
- package/dist/tools/compare.js.map +1 -1
- package/dist/tools/dead-clicks.d.ts +128 -0
- package/dist/tools/dead-clicks.js +570 -0
- package/dist/tools/dead-clicks.js.map +1 -0
- package/dist/tools/form-test.d.ts +112 -0
- package/dist/tools/form-test.js +477 -0
- package/dist/tools/form-test.js.map +1 -0
- package/dist/tools/index.d.ts +17 -1
- package/dist/tools/index.js +45 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inspect.d.ts +78 -0
- package/dist/tools/inspect.js +136 -0
- package/dist/tools/inspect.js.map +1 -0
- package/dist/tools/interact.d.ts +37 -18
- package/dist/tools/interact.js +113 -13
- package/dist/tools/interact.js.map +1 -1
- package/dist/tools/links.d.ts +129 -0
- package/dist/tools/links.js +640 -0
- package/dist/tools/links.js.map +1 -0
- package/dist/tools/responsive.d.ts +10 -6
- package/dist/tools/responsive.js +21 -4
- package/dist/tools/responsive.js.map +1 -1
- package/dist/tools/rtl.d.ts +241 -0
- package/dist/tools/rtl.js +410 -0
- package/dist/tools/rtl.js.map +1 -0
- package/dist/tools/save-auth.d.ts +263 -0
- package/dist/tools/save-auth.js +253 -0
- package/dist/tools/save-auth.js.map +1 -0
- package/dist/tools/screenshot.d.ts +4 -0
- package/dist/tools/screenshot.js +15 -4
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/seo.d.ts +113 -0
- package/dist/tools/seo.js +281 -0
- package/dist/tools/seo.js.map +1 -0
- package/dist/tools/snapshot.d.ts +122 -0
- package/dist/tools/snapshot.js +183 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/tools/wait-for.d.ts +107 -0
- package/dist/tools/wait-for.js +167 -0
- package/dist/tools/wait-for.js.map +1 -0
- package/dist/utils/arabic-text.d.ts +14 -0
- package/dist/utils/arabic-text.js +193 -0
- package/dist/utils/arabic-text.js.map +1 -0
- package/dist/utils/budget.d.ts +41 -0
- package/dist/utils/budget.js +182 -0
- package/dist/utils/budget.js.map +1 -0
- package/dist/utils/format.d.ts +11 -1
- package/dist/utils/format.js +27 -4
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/highlight.d.ts +69 -0
- package/dist/utils/highlight.js +181 -0
- package/dist/utils/highlight.js.map +1 -0
- package/dist/utils/link-rules.d.ts +100 -0
- package/dist/utils/link-rules.js +284 -0
- package/dist/utils/link-rules.js.map +1 -0
- package/dist/utils/mock-rules.d.ts +144 -0
- package/dist/utils/mock-rules.js +224 -0
- package/dist/utils/mock-rules.js.map +1 -0
- package/dist/utils/rtl-rules.d.ts +142 -0
- package/dist/utils/rtl-rules.js +296 -0
- package/dist/utils/rtl-rules.js.map +1 -0
- package/dist/utils/seo-rules.d.ts +129 -0
- package/dist/utils/seo-rules.js +726 -0
- package/dist/utils/seo-rules.js.map +1 -0
- package/dist/utils/snapshot-rules.d.ts +33 -0
- package/dist/utils/snapshot-rules.js +111 -0
- package/dist/utils/snapshot-rules.js.map +1 -0
- package/dist/utils/storage-state.d.ts +76 -0
- package/dist/utils/storage-state.js +195 -0
- package/dist/utils/storage-state.js.map +1 -0
- package/dist/utils/style-rules.d.ts +107 -0
- package/dist/utils/style-rules.js +223 -0
- package/dist/utils/style-rules.js.map +1 -0
- package/dist/utils/test-data.d.ts +75 -0
- package/dist/utils/test-data.js +294 -0
- package/dist/utils/test-data.js.map +1 -0
- package/dist/utils/vue-rules.d.ts +72 -0
- package/dist/utils/vue-rules.js +108 -0
- package/dist/utils/vue-rules.js.map +1 -0
- package/package.json +6 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-rules.js","sourceRoot":"","sources":["../../src/utils/seo-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n MAX_SEO_TEXT_LENGTH,\n SEO_CLS_GOOD,\n SEO_CLS_POOR,\n SEO_DESCRIPTION_MAX,\n SEO_DESCRIPTION_MIN,\n SEO_DOM_NODES_POOR,\n SEO_DOM_NODES_WARN,\n SEO_LCP_GOOD_MS,\n SEO_LCP_POOR_MS,\n SEO_OG_IMAGE_IDEAL,\n SEO_OG_IMAGE_MIN,\n SEO_TITLE_MAX,\n SEO_TITLE_MIN,\n SEO_TTFB_GOOD_MS,\n SEO_TTFB_POOR_MS,\n} from \"../constants.js\";\nimport type { ImageProbe, JsonLdBlock, PageSeo, PerfMetrics, RobotsFetch } from \"../engine/seo.js\";\n\n/**\n * What counts as an SEO problem.\n *\n * Everything here is pure: it takes what `engine/seo.ts` read off the page and\n * returns findings. Nothing in this file opens a browser, which is the point —\n * \"is this canonical wrong?\", \"does this robots.txt block that path?\" and\n * \"which properties is this Product missing?\" are all judgements with edge\n * cases, and judgements need unit tests that run in milliseconds.\n *\n * The type import above is type-only, so there is no runtime dependency on the\n * engine and no cycle: the engine reads, this judges, the tool prints.\n */\n\n/* ── Findings ─────────────────────────────────────────────────────────── */\n\nexport const SEO_AREAS = [\"indexing\", \"content\", \"headings\", \"social\", \"images\", \"structured_data\", \"performance\"] as const;\nexport type SeoArea = (typeof SEO_AREAS)[number];\n\n/**\n * `problem` will cost traffic, `warning` should be fixed, `info` is a\n * measurement with no verdict attached, `pass` is a check that ran and was\n * happy. Passes are reported rather than dropped for the same reason the\n * accessibility tool counts the rules that passed: without them, a short\n * report is indistinguishable from an audit that never ran.\n */\nexport type SeoLevel = \"problem\" | \"warning\" | \"info\" | \"pass\";\n\nexport interface SeoFinding {\n area: SeoArea;\n level: SeoLevel;\n /** What was checked — \"Title\", \"og:image\", \"robots.txt\". */\n label: string;\n /** What was found, including the value itself where it is short enough to be worth seeing. */\n detail: string;\n /** What to do about it. Only on problems and warnings. */\n fix?: string;\n}\n\nexport interface SeoReport {\n findings: SeoFinding[];\n problems: number;\n warnings: number;\n passes: number;\n}\n\nexport interface SeoResponseInfo {\n status: number;\n headers: Record<string, string>;\n}\n\nexport interface SeoAuditInput {\n /** The URL the caller asked for, which is not necessarily the one that answered. */\n requested_url: string;\n page: PageSeo;\n response?: SeoResponseInfo;\n robots?: RobotsFetch;\n robots_user_agent: string;\n og_image?: ImageProbe;\n performance?: PerfMetrics;\n}\n\n/* ── The audit ────────────────────────────────────────────────────────── */\n\n/**\n * Judge a page. The order of the checks is the order they are printed in, and\n * it runs from \"will this page be in the index at all\" down to \"how fast is\n * it\" — a `noindex` makes every other finding academic, so it goes first.\n */\nexport function judgeSeo(input: SeoAuditInput): SeoReport {\n const findings: SeoFinding[] = [];\n const add = (area: SeoArea, level: SeoLevel, label: string, detail: string, fix?: string): void => {\n findings.push({ area, level, label, detail, ...(fix ? { fix } : {}) });\n };\n\n auditIndexing(input, add);\n auditContent(input, add);\n auditHeadings(input, add);\n auditSocial(input, add);\n auditImages(input, add);\n auditStructuredData(input, add);\n auditPerformance(input, add);\n\n return {\n findings,\n problems: findings.filter((f) => f.level === \"problem\").length,\n warnings: findings.filter((f) => f.level === \"warning\").length,\n passes: findings.filter((f) => f.level === \"pass\").length,\n };\n}\n\ntype Add = (area: SeoArea, level: SeoLevel, label: string, detail: string, fix?: string) => void;\n\n/** Can this page be crawled and indexed, and under which address? */\nfunction auditIndexing(input: SeoAuditInput, add: Add): void {\n const { page, response } = input;\n\n if (response && response.status >= 400) {\n add(\n \"indexing\",\n \"problem\",\n \"HTTP status\",\n `the page answered ${response.status} — an error page is not indexed, whatever is rendered on it`,\n \"Serve a 200 for pages that should be in the index.\",\n );\n } else if (response && response.status >= 300) {\n add(\"indexing\", \"info\", \"HTTP status\", `${response.status}`);\n }\n\n if (!sameAddress(input.requested_url, page.url)) {\n add(\n \"indexing\",\n \"info\",\n \"Final URL\",\n `${input.requested_url} redirected to ${page.url} — everything below describes the page that answered`,\n );\n }\n\n // The single most expensive one-line mistake there is: a staging noindex\n // that shipped. Checked before anything else about the page's content.\n const robotsMeta = [...metaValues(page, \"robots\"), ...metaValues(page, \"googlebot\")].join(\", \");\n const headerRobots = response ? (response.headers[\"x-robots-tag\"] ?? \"\") : \"\";\n const directives = `${robotsMeta} ${headerRobots}`.toLowerCase();\n if (/\\bnoindex\\b|\\bnone\\b/.test(directives)) {\n const source = /\\bnoindex\\b|\\bnone\\b/.test(headerRobots.toLowerCase()) ? \"X-Robots-Tag header\" : \"<meta name=robots>\";\n add(\n \"indexing\",\n \"problem\",\n \"noindex\",\n `this page tells search engines not to index it (${source}: \"${(headerRobots || robotsMeta).trim()}\")`,\n \"Remove the noindex if this page is meant to be found — it is the usual reason a live site has no search presence.\",\n );\n } else if (/\\bnofollow\\b/.test(directives)) {\n add(\"indexing\", \"warning\", \"nofollow\", `robots directives say \"${(robotsMeta || headerRobots).trim()}\" — links on this page pass no signal`);\n } else {\n add(\"indexing\", \"pass\", \"Robots directives\", robotsMeta ? `\"${robotsMeta}\" — indexable` : \"none — indexable by default\");\n }\n\n auditRobotsTxt(input, add);\n auditCanonical(input, add);\n\n if (page.lang) {\n add(\"indexing\", \"pass\", \"Language\", `<html lang=\"${page.lang}\">${page.dir ? ` dir=\"${page.dir}\"` : \"\"}`);\n } else {\n add(\n \"indexing\",\n \"warning\",\n \"Language\",\n \"<html> has no lang attribute\",\n 'Add lang (e.g. <html lang=\"en\"> or lang=\"ar\" dir=\"rtl\") — it drives translation offers, hyphenation and screen-reader pronunciation.',\n );\n }\n\n const viewport = metaValues(page, \"viewport\")[0];\n if (viewport) {\n add(\"indexing\", \"pass\", \"Viewport\", `\"${viewport}\"`);\n } else {\n add(\n \"indexing\",\n \"warning\",\n \"Viewport\",\n \"no <meta name=viewport>\",\n 'Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> — indexing is mobile-first, and without it a phone renders the page at 980px wide.',\n );\n }\n\n if (!page.charset) {\n add(\n \"indexing\",\n \"warning\",\n \"Charset\",\n \"the document declares no character encoding\",\n 'Add <meta charset=\"utf-8\"> as the first thing in <head>.',\n );\n }\n\n const alternates = page.links.alternate ?? [];\n const hreflangs = alternates.filter((link) => link.hreflang);\n if (hreflangs.length > 0) {\n const missingDefault = !hreflangs.some((link) => link.hreflang?.toLowerCase() === \"x-default\");\n const detail = `${hreflangs.length} alternate${hreflangs.length === 1 ? \"\" : \"s\"}: ${hreflangs\n .map((link) => link.hreflang)\n .join(\", \")}`;\n if (missingDefault) {\n add(\"indexing\", \"info\", \"hreflang\", `${detail} — no x-default, so unmatched locales fall back to nothing in particular`);\n } else {\n add(\"indexing\", \"pass\", \"hreflang\", detail);\n }\n }\n}\n\nfunction auditRobotsTxt(input: SeoAuditInput, add: Add): void {\n const robots = input.robots;\n if (!robots) return;\n\n if (robots.error !== undefined) {\n add(\"indexing\", \"info\", \"robots.txt\", `not checked — ${robots.error}`);\n return;\n }\n if (robots.text === undefined) {\n add(\n \"indexing\",\n \"pass\",\n \"robots.txt\",\n `${robots.url} answered ${robots.status ?? \"no content\"} — no rules, so everything is crawlable`,\n );\n return;\n }\n\n const verdict = evaluateRobots(robots.text, input.page.url, input.robots_user_agent);\n if (!verdict.allowed) {\n add(\n \"indexing\",\n \"problem\",\n \"robots.txt\",\n `blocked for ${input.robots_user_agent} by \"${verdict.rule}\"${verdict.group ? ` in the \"${verdict.group}\" group` : \"\"} — ` +\n \"the page will not be crawled, and nothing on it (title, description, structured data) will ever be read\",\n `Remove or narrow that rule in ${robots.url}.`,\n );\n } else {\n add(\"indexing\", \"pass\", \"robots.txt\", `${input.robots_user_agent} may crawl this path — ${verdict.reason}`);\n }\n if (verdict.sitemaps.length > 0) {\n add(\"indexing\", \"info\", \"Sitemap\", verdict.sitemaps.join(\", \"));\n }\n}\n\nfunction auditCanonical(input: SeoAuditInput, add: Add): void {\n const { page } = input;\n const canonicals = page.links.canonical ?? [];\n\n if (canonicals.length === 0) {\n add(\n \"indexing\",\n \"warning\",\n \"Canonical\",\n \"no <link rel=canonical>\",\n \"Add a self-referencing canonical — without one, the same page reached with a query string or a trailing \" +\n \"slash is a separate, competing URL.\",\n );\n return;\n }\n if (canonicals.length > 1) {\n add(\n \"indexing\",\n \"problem\",\n \"Canonical\",\n `${canonicals.length} canonical links (${canonicals.map((link) => link.href).join(\", \")}) — ` +\n \"search engines ignore all of them when they disagree\",\n \"Leave exactly one.\",\n );\n return;\n }\n\n const canonical = canonicals[0];\n if (!canonical.href) {\n add(\"indexing\", \"problem\", \"Canonical\", \"the canonical link has an empty href\", \"Point it at this page's own URL.\");\n return;\n }\n if (!/^https?:\\/\\//i.test(canonical.href)) {\n add(\n \"indexing\",\n \"warning\",\n \"Canonical\",\n `\"${canonical.href}\" is relative (resolves to ${canonical.resolved ?? \"?\"})`,\n \"Use the absolute URL — relative canonicals are honoured inconsistently.\",\n );\n return;\n }\n if (canonical.resolved && !sameAddress(canonical.resolved, page.url)) {\n add(\n \"indexing\",\n \"warning\",\n \"Canonical\",\n `points at ${canonical.resolved}, not at this page (${page.url}) — this URL will not be indexed on its own`,\n \"Deliberate for a duplicate; a mistake if this page is meant to rank.\",\n );\n return;\n }\n add(\"indexing\", \"pass\", \"Canonical\", canonical.resolved ?? canonical.href);\n}\n\n/** The two strings that are the page in a search result. */\nfunction auditContent(input: SeoAuditInput, add: Add): void {\n const { page } = input;\n\n const title = page.title ?? \"\";\n if (page.title_count === 0 || title === \"\") {\n add(\n \"content\",\n \"problem\",\n \"Title\",\n page.title_count === 0 ? \"no <title> element\" : \"the <title> is empty\",\n \"Write one — it is the headline of every search result and browser tab, and the strongest on-page signal there is.\",\n );\n } else {\n if (page.title_count > 1) {\n add(\"content\", \"warning\", \"Title\", `${page.title_count} <title> elements — only the first counts`);\n }\n const length = [...title].length;\n if (length > SEO_TITLE_MAX) {\n add(\n \"content\",\n \"warning\",\n \"Title\",\n `\"${title}\" (${length} characters) — past about ${SEO_TITLE_MAX} the end is cut off in results`,\n \"Put the distinguishing words first.\",\n );\n } else if (length < SEO_TITLE_MIN) {\n add(\n \"content\",\n \"warning\",\n \"Title\",\n `\"${title}\" (${length} characters) — short; there is room for about ${SEO_TITLE_MAX}`,\n \"Say what the page is for, not only what it is called.\",\n );\n } else {\n add(\"content\", \"pass\", \"Title\", `\"${title}\" (${length} characters)`);\n }\n }\n\n const descriptions = metaValues(page, \"description\");\n const description = descriptions[0] ?? \"\";\n if (descriptions.length === 0) {\n add(\n \"content\",\n \"problem\",\n \"Meta description\",\n \"missing\",\n 'Add <meta name=\"description\" content=\"…\"> — without one the snippet is whatever text the engine scrapes off the page.',\n );\n } else if (description === \"\") {\n add(\"content\", \"problem\", \"Meta description\", \"present but empty\", \"Write the snippet, or drop the tag.\");\n } else {\n if (descriptions.length > 1) {\n add(\"content\", \"warning\", \"Meta description\", `${descriptions.length} description tags — only the first counts`);\n }\n const length = [...description].length;\n if (length > SEO_DESCRIPTION_MAX) {\n add(\n \"content\",\n \"warning\",\n \"Meta description\",\n `${length} characters — cut off around ${SEO_DESCRIPTION_MAX}: \"${elide(description, MAX_SEO_TEXT_LENGTH)}\"`,\n );\n } else if (length < SEO_DESCRIPTION_MIN) {\n add(\"content\", \"warning\", \"Meta description\", `${length} characters — short: \"${description}\"`, `About ${SEO_DESCRIPTION_MIN}–${SEO_DESCRIPTION_MAX} characters uses the whole snippet.`);\n } else {\n add(\"content\", \"pass\", \"Meta description\", `\"${elide(description, MAX_SEO_TEXT_LENGTH)}\" (${length} characters)`);\n }\n }\n\n add(\n \"content\",\n \"info\",\n \"Body\",\n `${plural(page.word_count, \"word\")}, ${plural(page.anchors.total, \"link\")} ` +\n `(${page.anchors.internal} internal, ${page.anchors.external} external` +\n `${page.anchors.nofollow > 0 ? `, ${page.anchors.nofollow} nofollow` : \"\"})`,\n );\n if (page.anchors.empty > 0) {\n add(\n \"content\",\n \"warning\",\n \"Link text\",\n `${page.anchors.empty} link${page.anchors.empty === 1 ? \"\" : \"s\"} with no text and no accessible name`,\n \"A crawler follows the link but learns nothing about what is on the other side.\",\n );\n }\n}\n\n/** The outline: one h1, and no levels skipped on the way down. */\nfunction auditHeadings(input: SeoAuditInput, add: Add): void {\n const { page } = input;\n const h1s = page.headings.filter((heading) => heading.level === 1);\n const h1Count = page.heading_counts.h1 ?? 0;\n\n if (h1Count === 0) {\n add(\n \"headings\",\n \"problem\",\n \"H1\",\n page.heading_total === 0 ? \"the page has no headings at all\" : \"no <h1>\",\n \"Give the page one h1 that says what it is about.\",\n );\n } else if (h1Count > 1) {\n add(\n \"headings\",\n \"warning\",\n \"H1\",\n `${h1Count} h1 elements: ${h1s.map((heading) => `\"${heading.text}\"`).join(\", \")}`,\n \"One h1 per page; the rest are h2s.\",\n );\n } else if (h1s[0]?.empty) {\n add(\"headings\", \"problem\", \"H1\", \"the h1 is empty\", \"An h1 containing only an image or an icon says nothing to a crawler.\");\n } else {\n const only = h1s[0];\n add(\n \"headings\",\n \"pass\",\n \"H1\",\n only ? `\"${only.text}\"${only.hidden ? \" (not visible, but still crawled)\" : \"\"}` : \"one, further down the page than the outline below reaches\",\n );\n }\n\n const first = page.headings[0];\n if (first && first.level !== 1 && h1Count > 0) {\n add(\"headings\", \"info\", \"Outline\", `the first heading is an h${first.level}, not the h1`);\n }\n\n const skips: string[] = [];\n let previous = 0;\n for (const heading of page.headings) {\n if (previous > 0 && heading.level > previous + 1) {\n skips.push(`h${previous} → h${heading.level} (\"${heading.text}\")`);\n }\n previous = heading.level;\n }\n if (skips.length > 0) {\n add(\n \"headings\",\n \"warning\",\n \"Outline\",\n `${skips.length} skipped level${skips.length === 1 ? \"\" : \"s\"}: ${skips.slice(0, 3).join(\"; \")}` +\n (skips.length > 3 ? `; … and ${skips.length - 3} more` : \"\"),\n \"Headings are the page's table of contents; a jumped level breaks it for crawlers and screen readers alike.\",\n );\n } else if (page.heading_total > 1) {\n add(\"headings\", \"pass\", \"Outline\", `${describeCounts(page.heading_counts)} — no skipped levels`);\n }\n\n const empties = page.headings.filter((heading) => heading.empty && heading.level > 1).length;\n if (empties > 0) {\n add(\"headings\", \"warning\", \"Empty headings\", `${empties} heading${empties === 1 ? \"\" : \"s\"} with no text`);\n }\n}\n\n/** What the link looks like when someone shares it. */\nfunction auditSocial(input: SeoAuditInput, add: Add): void {\n const { page } = input;\n const og = (key: string): string => metaValues(page, key)[0] ?? \"\";\n\n const required: Array<[string, SeoLevel, string]> = [\n [\"og:title\", \"warning\", \"the headline on the share card\"],\n [\"og:description\", \"warning\", \"the text under it\"],\n [\"og:image\", \"warning\", \"the picture — a card without one is a grey box\"],\n [\"og:url\", \"info\", \"the canonical address of the shared page\"],\n [\"og:type\", \"info\", 'usually \"website\" or \"article\"'],\n ];\n\n const missing: string[] = [];\n for (const [key, level, why] of required) {\n const value = og(key);\n if (value) {\n add(\"social\", \"pass\", key, elide(value, MAX_SEO_TEXT_LENGTH));\n } else {\n missing.push(key);\n add(\"social\", level, key, `missing — ${why}`);\n }\n }\n\n if (missing.length === required.length) {\n add(\n \"social\",\n \"warning\",\n \"Open Graph\",\n \"no Open Graph tags at all — every share of this link renders as a bare URL\",\n \"Add og:title, og:description, og:image and og:url.\",\n );\n }\n\n const card = og(\"twitter:card\");\n if (card) {\n add(\"social\", \"pass\", \"twitter:card\", card);\n } else if (og(\"og:image\")) {\n add(\"social\", \"info\", \"twitter:card\", 'absent — X falls back to Open Graph, but \"summary_large_image\" is what makes the image full width');\n }\n\n auditOgImage(input, add);\n}\n\nfunction auditOgImage(input: SeoAuditInput, add: Add): void {\n const probe = input.og_image;\n if (!probe) return;\n\n const raw = metaValues(input.page, \"og:image\")[0] ?? \"\";\n if (raw && !/^https?:\\/\\//i.test(raw)) {\n add(\n \"social\",\n \"warning\",\n \"Share image\",\n `\"${raw}\" is not an absolute URL (fetched as ${probe.url})`,\n \"Open Graph requires an absolute URL — most networks will not resolve a relative one.\",\n );\n }\n\n if (!probe.ok) {\n add(\n \"social\",\n \"problem\",\n \"Share image\",\n `${probe.url} could not be fetched${probe.status ? ` (HTTP ${probe.status})` : ` — ${probe.error ?? \"no response\"}`} — ` +\n \"the share card will be blank\",\n \"Point og:image at an image that resolves publicly, with no auth in front of it.\",\n );\n return;\n }\n\n if (probe.width === undefined || probe.height === undefined) {\n add(\n \"social\",\n \"warning\",\n \"Share image\",\n `${probe.url} answered ${probe.status ?? 200}${probe.content_type ? ` as ${probe.content_type}` : \"\"} but could not be ` +\n `read as an image${probe.error ? ` (${probe.error})` : \"\"}`,\n );\n return;\n }\n\n const size = `${probe.width}x${probe.height}${probe.bytes ? `, ${kb(probe.bytes)}` : \"\"}`;\n if (probe.width < SEO_OG_IMAGE_MIN.width || probe.height < SEO_OG_IMAGE_MIN.height) {\n add(\n \"social\",\n \"problem\",\n \"Share image\",\n `${size} — below the ${SEO_OG_IMAGE_MIN.width}x${SEO_OG_IMAGE_MIN.height} floor, so the networks will not render a card at all`,\n `Use ${SEO_OG_IMAGE_IDEAL.width}x${SEO_OG_IMAGE_IDEAL.height}.`,\n );\n } else if (probe.width < SEO_OG_IMAGE_IDEAL.width || probe.height < SEO_OG_IMAGE_IDEAL.height) {\n add(\n \"social\",\n \"warning\",\n \"Share image\",\n `${size} — smaller than the ${SEO_OG_IMAGE_IDEAL.width}x${SEO_OG_IMAGE_IDEAL.height} card, so it renders as a small thumbnail beside the text`,\n );\n } else {\n add(\"social\", \"pass\", \"Share image\", `${size} — fetched and readable`);\n }\n}\n\n/** Alt text: what a crawler and a screen reader read instead of the picture. */\nfunction auditImages(input: SeoAuditInput, add: Add): void {\n const { images } = input.page;\n\n if (images.total === 0) {\n add(\"images\", \"info\", \"Images\", \"no <img> elements on the page\");\n return;\n }\n\n if (images.missing_alt_total > 0) {\n const listed = images.missing_alt.map((image) => image.description || image.src || \"?\");\n add(\n \"images\",\n \"problem\",\n \"Alt text\",\n `${images.missing_alt_total} of ${images.total} images have no alt attribute: ${listed.join(\", \")}` +\n (images.missing_alt_total > listed.length ? `, … and ${images.missing_alt_total - listed.length} more` : \"\"),\n 'Describe what the image shows, or mark it decorative with alt=\"\" — a missing attribute says neither.',\n );\n } else {\n add(\n \"images\",\n \"pass\",\n \"Alt text\",\n `all ${images.total} images have an alt attribute` + (images.empty_alt > 0 ? ` (${images.empty_alt} decorative, alt=\"\")` : \"\"),\n );\n }\n\n if (images.no_dimensions > 0) {\n add(\n \"images\",\n \"info\",\n \"Dimensions\",\n `${images.no_dimensions} of ${images.total} images have no width/height attributes — the browser cannot reserve space, which is where layout shift comes from`,\n );\n }\n}\n\n/** JSON-LD: the difference between a blue link and a rich result. */\nfunction auditStructuredData(input: SeoAuditInput, add: Add): void {\n const { page } = input;\n\n if (page.jsonld_total === 0) {\n const elsewhere =\n page.microdata > 0\n ? ` (there are ${page.microdata} microdata elements — Google reads those too, but JSON-LD is what it recommends)`\n : \"\";\n add(\n \"structured_data\",\n \"warning\",\n \"JSON-LD\",\n `none on the page${elsewhere}`,\n \"Add a <script type=\\\"application/ld+json\\\"> block — it is what makes a page eligible for rich results.\",\n );\n return;\n }\n\n const parsed = parseJsonLd(page.jsonld);\n for (const block of parsed) {\n if (!block.ok) {\n add(\n \"structured_data\",\n \"problem\",\n `JSON-LD block ${block.index}`,\n `is not valid JSON — ${block.error}`,\n \"A block that does not parse is ignored entirely, so the page has that much less structured data than it looks like.\",\n );\n continue;\n }\n if (block.nodes.length === 0) {\n add(\"structured_data\", \"warning\", `JSON-LD block ${block.index}`, \"parses, but declares no @type\", \"Without @type there is nothing to match against.\");\n continue;\n }\n for (const node of block.nodes) {\n const label = `${node.type}${node.name ? ` \"${elide(node.name, 60)}\"` : \"\"}`;\n if (node.missing_required.length > 0) {\n add(\n \"structured_data\",\n \"problem\",\n label,\n `missing required ${node.missing_required.join(\", \")} (has ${node.keys.join(\", \")})`,\n \"Google drops the whole item when a required property is absent.\",\n );\n } else if (node.missing_recommended.length > 0) {\n add(\"structured_data\", \"warning\", label, `missing recommended ${node.missing_recommended.join(\", \")}`);\n } else if (node.known) {\n add(\"structured_data\", \"pass\", label, `has ${node.keys.join(\", \")}`);\n } else {\n add(\"structured_data\", \"info\", label, `${node.keys.length} properties: ${node.keys.slice(0, 8).join(\", \")}`);\n }\n }\n }\n\n const truncated = page.jsonld.filter((block) => block.truncated).length;\n if (truncated > 0) {\n add(\"structured_data\", \"info\", \"JSON-LD\", `${truncated} block${truncated === 1 ? \"\" : \"s\"} too large to read in full — only the start was parsed`);\n }\n if (page.jsonld_total > page.jsonld.length) {\n add(\"structured_data\", \"info\", \"JSON-LD\", `${page.jsonld_total} blocks on the page, ${page.jsonld.length} read`);\n }\n}\n\n/**\n * Speed, measured rather than scored.\n *\n * These are lab numbers from one load of a headless browser on this machine:\n * useful for \"the LCP element is a 4MB hero image\", worthless as a prediction\n * of what Chrome will report from real visitors. The wording says so.\n */\nfunction auditPerformance(input: SeoAuditInput, add: Add): void {\n const perf = input.performance;\n if (!perf) return;\n\n if (perf.lcp_ms !== undefined) {\n const level = perf.lcp_ms <= SEO_LCP_GOOD_MS ? \"pass\" : perf.lcp_ms > SEO_LCP_POOR_MS ? \"problem\" : \"warning\";\n add(\n \"performance\",\n level,\n \"LCP\",\n `${perf.lcp_ms}ms${perf.lcp_element ? ` — largest element: ${perf.lcp_element}` : \"\"} ` +\n `(good ≤ ${SEO_LCP_GOOD_MS}ms, poor > ${SEO_LCP_POOR_MS}ms)`,\n level === \"pass\" ? undefined : \"Largest Contentful Paint is a ranking signal; the named element is what to make faster.\",\n );\n }\n\n if (perf.cls !== undefined) {\n const level = perf.cls <= SEO_CLS_GOOD ? \"pass\" : perf.cls > SEO_CLS_POOR ? \"problem\" : \"warning\";\n add(\n \"performance\",\n level,\n \"CLS\",\n `${perf.cls} (good ≤ ${SEO_CLS_GOOD}, poor > ${SEO_CLS_POOR})`,\n level === \"pass\" ? undefined : \"Reserve space for images, ads and late-loading fonts.\",\n );\n }\n\n if (perf.ttfb_ms !== undefined) {\n const level = perf.ttfb_ms <= SEO_TTFB_GOOD_MS ? \"pass\" : perf.ttfb_ms > SEO_TTFB_POOR_MS ? \"warning\" : \"info\";\n add(\"performance\", level, \"TTFB\", `${perf.ttfb_ms}ms to the first byte (good ≤ ${SEO_TTFB_GOOD_MS}ms)`);\n }\n if (perf.fcp_ms !== undefined) add(\"performance\", \"info\", \"FCP\", `${perf.fcp_ms}ms to the first paint`);\n if (perf.load_ms !== undefined) add(\"performance\", \"info\", \"Load\", `${perf.load_ms}ms to the load event`);\n\n const weight =\n `${perf.requests} requests, ${kb(perf.transfer_bytes)} transferred` +\n (perf.transfer_incomplete ? \" (a floor: cross-origin resources without Timing-Allow-Origin report 0)\" : \"\");\n const top = perf.resources\n .slice(0, 3)\n .map((group) => `${group.type} ${group.count}×${group.bytes > 0 ? ` ${kb(group.bytes)}` : \"\"}`)\n .join(\", \");\n add(\"performance\", \"info\", \"Page weight\", top ? `${weight} — ${top}` : weight);\n\n if (perf.long_tasks > 0) {\n add(\"performance\", \"info\", \"Long tasks\", `${perf.long_tasks} tasks over 50ms blocked the main thread during load`);\n }\n\n const nodes = input.page.dom_nodes;\n if (nodes > SEO_DOM_NODES_POOR) {\n add(\"performance\", \"warning\", \"DOM size\", `${nodes} elements — past ${SEO_DOM_NODES_POOR} the page costs memory and style recalculation on every interaction`);\n } else if (nodes > SEO_DOM_NODES_WARN) {\n add(\"performance\", \"info\", \"DOM size\", `${nodes} elements (Lighthouse starts warning at ${SEO_DOM_NODES_WARN})`);\n }\n}\n\n/* ── robots.txt ───────────────────────────────────────────────────────── */\n\nexport interface RobotsVerdict {\n allowed: boolean;\n /** The directive that decided it, e.g. `Disallow: /admin/`. */\n rule?: string;\n /** The user-agent group it came from. */\n group?: string;\n sitemaps: string[];\n reason: string;\n}\n\ninterface RobotsGroup {\n agents: string[];\n rules: Array<{ allow: boolean; path: string }>;\n}\n\n/**\n * Decide whether `userAgent` may crawl `url`, per RFC 9309.\n *\n * Two rules do the work. Group selection: the most specific user-agent line\n * that matches wins, and `*` is the fallback — so a `Disallow: /` under\n * `User-agent: *` does not apply to Googlebot if Googlebot has a group of its\n * own. Rule selection: the *longest* matching path wins regardless of order,\n * and Allow beats Disallow on a tie. That is why `Disallow: /` followed by\n * `Allow: /public/` permits `/public/page` — reading top to bottom would get\n * it exactly wrong.\n *\n * `*` matches any run of characters and a trailing `$` anchors to the end.\n */\nexport function evaluateRobots(text: string, url: string, userAgent: string): RobotsVerdict {\n const { groups, sitemaps } = parseRobots(text);\n const path = pathOf(url);\n const agent = userAgent.toLowerCase();\n\n let best: RobotsGroup | undefined;\n let bestToken = \"\";\n for (const group of groups) {\n for (const candidate of group.agents) {\n const matches = candidate === \"*\" ? true : agent === candidate || agent.startsWith(candidate);\n if (!matches) continue;\n // A named group always beats \"*\", and a longer name beats a shorter one.\n const better = candidate !== \"*\" && (bestToken === \"*\" || candidate.length > bestToken.length);\n if (best === undefined || better) {\n best = group;\n bestToken = candidate;\n }\n }\n }\n\n if (!best) {\n return { allowed: true, sitemaps, reason: `no group matches ${userAgent}` };\n }\n\n // Groups naming the same crawler are one group, however many times the file\n // repeats the header — two `User-agent: *` blocks is what a concatenated\n // config produces, and honouring only the first would call a disallowed path\n // crawlable.\n const rules = groups.filter((group) => group.agents.includes(bestToken)).flatMap((group) => group.rules);\n\n let winner: { allow: boolean; path: string } | undefined;\n for (const rule of rules) {\n if (!matchesRobotsPath(rule.path, path)) continue;\n if (\n winner === undefined ||\n rule.path.length > winner.path.length ||\n (rule.path.length === winner.path.length && rule.allow && !winner.allow)\n ) {\n winner = rule;\n }\n }\n\n if (!winner) {\n return { allowed: true, sitemaps, group: bestToken, reason: `no rule in the \"${bestToken}\" group matches ${path}` };\n }\n return {\n allowed: winner.allow,\n rule: `${winner.allow ? \"Allow\" : \"Disallow\"}: ${winner.path}`,\n group: bestToken,\n sitemaps,\n reason: `\"${winner.allow ? \"Allow\" : \"Disallow\"}: ${winner.path}\" in the \"${bestToken}\" group`,\n };\n}\n\nfunction parseRobots(text: string): { groups: RobotsGroup[]; sitemaps: string[] } {\n const groups: RobotsGroup[] = [];\n const sitemaps: string[] = [];\n let current: RobotsGroup | undefined;\n // Consecutive user-agent lines head the same group; the first rule line\n // closes the header, so the next user-agent starts a new group.\n let heading = false;\n\n for (const raw of text.split(/\\r?\\n/)) {\n const line = raw.split(\"#\")[0].trim();\n if (!line) continue;\n const at = line.indexOf(\":\");\n if (at === -1) continue;\n const field = line.slice(0, at).trim().toLowerCase();\n const value = line.slice(at + 1).trim();\n\n if (field === \"user-agent\") {\n if (!current || !heading) {\n current = { agents: [], rules: [] };\n groups.push(current);\n heading = true;\n }\n current.agents.push(value.toLowerCase());\n continue;\n }\n if (field === \"sitemap\") {\n if (value) sitemaps.push(value);\n continue;\n }\n if (field !== \"allow\" && field !== \"disallow\") continue;\n if (!current) continue;\n heading = false;\n // `Disallow:` with nothing after it means \"nothing is disallowed\" — it is\n // the absence of a rule, not a rule matching everything.\n if (value === \"\") continue;\n current.rules.push({ allow: field === \"allow\", path: value });\n }\n\n return { groups, sitemaps };\n}\n\n/** A robots path pattern against a request path. `*` is any run, `$` anchors the end. */\nexport function matchesRobotsPath(pattern: string, path: string): boolean {\n const anchored = pattern.endsWith(\"$\");\n const body = anchored ? pattern.slice(0, -1) : pattern;\n const source = body\n .split(\"*\")\n .map((part) => part.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\".*\");\n return new RegExp(`^${source}${anchored ? \"$\" : \"\"}`).test(path);\n}\n\nfunction pathOf(url: string): string {\n try {\n const parsed = new URL(url);\n return `${parsed.pathname}${parsed.search}`;\n } catch {\n return url.startsWith(\"/\") ? url : `/${url}`;\n }\n}\n\n/* ── Structured data ──────────────────────────────────────────────────── */\n\nexport interface SchemaExpectation {\n required: string[];\n recommended: string[];\n}\n\n/**\n * What Google's rich-result documentation asks for, per type.\n *\n * Deliberately short. Only types worth a rich result are listed, and only the\n * properties whose absence actually costs one — a validator that reports every\n * optional property of every schema.org type produces noise nobody reads, and\n * schema.org itself requires almost nothing.\n */\nexport const SCHEMA_EXPECTATIONS: Readonly<Record<string, SchemaExpectation>> = {\n Organization: { required: [\"name\", \"url\"], recommended: [\"logo\"] },\n LocalBusiness: { required: [\"name\", \"address\"], recommended: [\"telephone\", \"openingHours\", \"image\"] },\n Product: { required: [\"name\", \"image\"], recommended: [\"offers\", \"description\", \"brand\"] },\n Offer: { required: [\"price\", \"priceCurrency\"], recommended: [\"availability\"] },\n Article: { required: [\"headline\", \"image\", \"datePublished\"], recommended: [\"author\", \"dateModified\"] },\n BreadcrumbList: { required: [\"itemListElement\"], recommended: [] },\n WebSite: { required: [\"name\", \"url\"], recommended: [\"potentialAction\"] },\n WebPage: { required: [\"name\"], recommended: [\"description\"] },\n Event: { required: [\"name\", \"startDate\", \"location\"], recommended: [\"endDate\", \"offers\", \"image\"] },\n Recipe: { required: [\"name\", \"image\", \"recipeIngredient\", \"recipeInstructions\"], recommended: [\"author\", \"totalTime\"] },\n FAQPage: { required: [\"mainEntity\"], recommended: [] },\n VideoObject: { required: [\"name\", \"description\", \"thumbnailUrl\", \"uploadDate\"], recommended: [\"duration\"] },\n Person: { required: [\"name\"], recommended: [\"url\"] },\n SoftwareApplication: { required: [\"name\", \"offers\", \"applicationCategory\"], recommended: [\"aggregateRating\"] },\n};\n\n/** Subtypes that inherit another type's expectations. */\nconst SCHEMA_ALIASES: Readonly<Record<string, string>> = {\n Restaurant: \"LocalBusiness\",\n Store: \"LocalBusiness\",\n CafeOrCoffeeShop: \"LocalBusiness\",\n Bakery: \"LocalBusiness\",\n FoodEstablishment: \"LocalBusiness\",\n ProfessionalService: \"LocalBusiness\",\n MedicalBusiness: \"LocalBusiness\",\n NewsArticle: \"Article\",\n BlogPosting: \"Article\",\n TechArticle: \"Article\",\n ScholarlyArticle: \"Article\",\n Blog: \"WebPage\",\n CollectionPage: \"WebPage\",\n ItemPage: \"WebPage\",\n AboutPage: \"WebPage\",\n ContactPage: \"WebPage\",\n Corporation: \"Organization\",\n NGO: \"Organization\",\n EducationalOrganization: \"Organization\",\n OnlineStore: \"Organization\",\n IndividualProduct: \"Product\",\n ProductModel: \"Product\",\n AggregateOffer: \"Offer\",\n BusinessEvent: \"Event\",\n MusicEvent: \"Event\",\n SportsEvent: \"Event\",\n MobileApplication: \"SoftwareApplication\",\n WebApplication: \"SoftwareApplication\",\n};\n\nexport function expectationsFor(type: string): SchemaExpectation | undefined {\n return SCHEMA_EXPECTATIONS[type] ?? SCHEMA_EXPECTATIONS[SCHEMA_ALIASES[type] ?? \"\"];\n}\n\nexport interface JsonLdNode {\n type: string;\n /** Property names present on the node, `@`-prefixed keys excluded. */\n keys: string[];\n name?: string;\n missing_required: string[];\n missing_recommended: string[];\n /** Whether this type has expectations at all — an unknown type is reported, not judged. */\n known: boolean;\n}\n\nexport interface JsonLdParsed {\n /** 1-based position of the block on the page. */\n index: number;\n ok: boolean;\n error?: string;\n nodes: JsonLdNode[];\n}\n\n/**\n * Parse the JSON-LD blocks and check each node against its type.\n *\n * `@graph` is flattened and a top-level array is iterated, because both are\n * ordinary ways to put several entities in one block. Nesting below that is\n * left alone: a Product's `offers` is checked as part of the Product's own\n * expectations, and walking arbitrarily deep would report the same missing\n * property from three directions.\n */\nexport function parseJsonLd(blocks: JsonLdBlock[]): JsonLdParsed[] {\n return blocks.map((block, at) => {\n const index = at + 1;\n let value: unknown;\n try {\n value = JSON.parse(block.text);\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n return {\n index,\n ok: false,\n error: block.truncated ? `${reason} (the block was too large to read in full)` : reason,\n nodes: [],\n };\n }\n\n const nodes: JsonLdNode[] = [];\n for (const entity of flattenEntities(value)) {\n for (const type of typesOf(entity)) {\n nodes.push(describeNode(type, entity));\n }\n }\n return { index, ok: true, nodes };\n });\n}\n\nfunction flattenEntities(value: unknown): Array<Record<string, unknown>> {\n if (Array.isArray(value)) return value.flatMap(flattenEntities);\n if (typeof value !== \"object\" || value === null) return [];\n const entity = value as Record<string, unknown>;\n const graph = entity[\"@graph\"];\n if (graph !== undefined) {\n const rest = Object.keys(entity).some((key) => key !== \"@graph\" && key !== \"@context\") ? [entity] : [];\n return [...rest, ...flattenEntities(graph)];\n }\n return [entity];\n}\n\nfunction typesOf(entity: Record<string, unknown>): string[] {\n const raw = entity[\"@type\"];\n const types = (Array.isArray(raw) ? raw : [raw])\n .filter((type): type is string => typeof type === \"string\" && type.trim() !== \"\")\n // schema.org URLs are a legal way to spell a type.\n .map((type) => type.trim().replace(/^https?:\\/\\/schema\\.org\\//i, \"\"));\n return types.length > 0 ? types : [];\n}\n\nfunction describeNode(type: string, entity: Record<string, unknown>): JsonLdNode {\n const keys = Object.keys(entity).filter((key) => !key.startsWith(\"@\"));\n const present = new Set(keys);\n const expectation = expectationsFor(type);\n const node: JsonLdNode = {\n type,\n keys,\n missing_required: expectation ? expectation.required.filter((key) => !hasValue(entity, present, key)) : [],\n missing_recommended: expectation ? expectation.recommended.filter((key) => !hasValue(entity, present, key)) : [],\n known: expectation !== undefined,\n };\n const name = entity.name ?? entity.headline;\n if (typeof name === \"string\" && name.trim() !== \"\") node.name = name.trim();\n return node;\n}\n\n/** Present *and* non-empty: `\"name\": \"\"` is the same problem as no name at all. */\nfunction hasValue(entity: Record<string, unknown>, present: Set<string>, key: string): boolean {\n if (!present.has(key)) return false;\n const value = entity[key];\n if (value === null || value === undefined) return false;\n if (typeof value === \"string\") return value.trim() !== \"\";\n if (Array.isArray(value)) return value.length > 0;\n return true;\n}\n\n/* ── Small shared helpers ─────────────────────────────────────────────── */\n\n/** Read a meta value however the page spelled it: `property` first, then `name`. */\nexport function metaValues(page: PageSeo, key: string): string[] {\n const lower = key.toLowerCase();\n const values = [...(page.properties[lower] ?? []), ...(page.named[lower] ?? [])];\n return values.filter((value) => value !== undefined);\n}\n\n/** Two URLs that address the same page. The fragment is not part of the address. */\nexport function sameAddress(a: string, b: string): boolean {\n if (a === b) return true;\n try {\n const left = new URL(a);\n const right = new URL(b);\n left.hash = \"\";\n right.hash = \"\";\n if (left.href === right.href) return true;\n // A trailing slash on the root is the same address with or without it.\n return left.href.replace(/\\/$/, \"\") === right.href.replace(/\\/$/, \"\");\n } catch {\n return false;\n }\n}\n\nfunction describeCounts(counts: Record<string, number>): string {\n return Object.entries(counts)\n .filter(([, count]) => count > 0)\n .map(([level, count]) => `${count} ${level}`)\n .join(\", \");\n}\n\nfunction plural(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`;\n}\n\nfunction elide(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nfunction kb(bytes: number): string {\n if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)}MB`;\n return `${Math.max(1, Math.round(bytes / 1000))}KB`;\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `framewatch_snapshot` does with the tree Playwright hands it.
|
|
3
|
+
*
|
|
4
|
+
* Playwright's AI-mode aria snapshot is YAML-ish text, one element per line,
|
|
5
|
+
* indented by depth, each carrying a `[ref=eN]` that `aria-ref=eN` resolves
|
|
6
|
+
* back to the element. Everything here is text in, text out — no browser —
|
|
7
|
+
* so the counting, the filtering and the truncation can be tested in
|
|
8
|
+
* milliseconds against a tree written by hand.
|
|
9
|
+
*/
|
|
10
|
+
/** A bare ref as the caller writes it: `e8`. */
|
|
11
|
+
export declare function isRef(value: string): boolean;
|
|
12
|
+
/** The Playwright selector that resolves a snapshot ref. */
|
|
13
|
+
export declare function refSelector(ref: string): string;
|
|
14
|
+
export interface SnapshotCounts {
|
|
15
|
+
/** Lines carrying a ref — every element the snapshot can name. */
|
|
16
|
+
elements: number;
|
|
17
|
+
/** Of those, the ones a person can act on. */
|
|
18
|
+
interactive: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function countSnapshot(tree: string): SnapshotCounts;
|
|
21
|
+
/**
|
|
22
|
+
* The interactive lines only, flattened to one level and stripped of the
|
|
23
|
+
* trailing colon a parent line carries. A link's `/url` child, which is the
|
|
24
|
+
* only child worth keeping, is folded onto the link's own line.
|
|
25
|
+
*/
|
|
26
|
+
export declare function interactiveOnly(tree: string): string;
|
|
27
|
+
export interface TruncatedSnapshot {
|
|
28
|
+
text: string;
|
|
29
|
+
/** Whole lines dropped from the end to fit. */
|
|
30
|
+
cut_lines: number;
|
|
31
|
+
}
|
|
32
|
+
/** Cut the tree to at most `maxChars`, on a line boundary, and say how many lines went. */
|
|
33
|
+
export declare function truncateSnapshot(tree: string, maxChars: number): TruncatedSnapshot;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `framewatch_snapshot` does with the tree Playwright hands it.
|
|
3
|
+
*
|
|
4
|
+
* Playwright's AI-mode aria snapshot is YAML-ish text, one element per line,
|
|
5
|
+
* indented by depth, each carrying a `[ref=eN]` that `aria-ref=eN` resolves
|
|
6
|
+
* back to the element. Everything here is text in, text out — no browser —
|
|
7
|
+
* so the counting, the filtering and the truncation can be tested in
|
|
8
|
+
* milliseconds against a tree written by hand.
|
|
9
|
+
*/
|
|
10
|
+
/** Roles a person can act on. A line with one of these, or a pointer cursor, is "interactive". */
|
|
11
|
+
const INTERACTIVE_ROLES = new Set([
|
|
12
|
+
"button",
|
|
13
|
+
"link",
|
|
14
|
+
"textbox",
|
|
15
|
+
"searchbox",
|
|
16
|
+
"checkbox",
|
|
17
|
+
"radio",
|
|
18
|
+
"switch",
|
|
19
|
+
"slider",
|
|
20
|
+
"spinbutton",
|
|
21
|
+
"combobox",
|
|
22
|
+
"listbox",
|
|
23
|
+
"option",
|
|
24
|
+
"menuitem",
|
|
25
|
+
"menuitemcheckbox",
|
|
26
|
+
"menuitemradio",
|
|
27
|
+
"tab",
|
|
28
|
+
"treeitem",
|
|
29
|
+
]);
|
|
30
|
+
const REF_PATTERN = /\[ref=(e\d+)\]/;
|
|
31
|
+
const LINE_PATTERN = /^(\s*)- (\S+)(.*)$/;
|
|
32
|
+
/** A bare ref as the caller writes it: `e8`. */
|
|
33
|
+
export function isRef(value) {
|
|
34
|
+
return /^e\d+$/.test(value);
|
|
35
|
+
}
|
|
36
|
+
/** The Playwright selector that resolves a snapshot ref. */
|
|
37
|
+
export function refSelector(ref) {
|
|
38
|
+
return `aria-ref=${ref}`;
|
|
39
|
+
}
|
|
40
|
+
export function countSnapshot(tree) {
|
|
41
|
+
let elements = 0;
|
|
42
|
+
let interactive = 0;
|
|
43
|
+
for (const line of tree.split("\n")) {
|
|
44
|
+
if (!REF_PATTERN.test(line))
|
|
45
|
+
continue;
|
|
46
|
+
elements++;
|
|
47
|
+
if (isInteractiveLine(line))
|
|
48
|
+
interactive++;
|
|
49
|
+
}
|
|
50
|
+
return { elements, interactive };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The interactive lines only, flattened to one level and stripped of the
|
|
54
|
+
* trailing colon a parent line carries. A link's `/url` child, which is the
|
|
55
|
+
* only child worth keeping, is folded onto the link's own line.
|
|
56
|
+
*/
|
|
57
|
+
export function interactiveOnly(tree) {
|
|
58
|
+
const lines = tree.split("\n");
|
|
59
|
+
const out = [];
|
|
60
|
+
for (let i = 0; i < lines.length; i++) {
|
|
61
|
+
const line = lines[i];
|
|
62
|
+
if (!REF_PATTERN.test(line) || !isInteractiveLine(line))
|
|
63
|
+
continue;
|
|
64
|
+
let text = line.trim().replace(/:$/, "");
|
|
65
|
+
const url = childUrl(lines, i);
|
|
66
|
+
if (url !== undefined)
|
|
67
|
+
text += ` → ${url}`;
|
|
68
|
+
out.push(text);
|
|
69
|
+
}
|
|
70
|
+
return out.join("\n");
|
|
71
|
+
}
|
|
72
|
+
/** Cut the tree to at most `maxChars`, on a line boundary, and say how many lines went. */
|
|
73
|
+
export function truncateSnapshot(tree, maxChars) {
|
|
74
|
+
if (tree.length <= maxChars)
|
|
75
|
+
return { text: tree, cut_lines: 0 };
|
|
76
|
+
const lines = tree.split("\n");
|
|
77
|
+
const kept = [];
|
|
78
|
+
let length = 0;
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
const next = length + line.length + (kept.length > 0 ? 1 : 0);
|
|
81
|
+
if (next > maxChars)
|
|
82
|
+
break;
|
|
83
|
+
kept.push(line);
|
|
84
|
+
length = next;
|
|
85
|
+
}
|
|
86
|
+
return { text: kept.join("\n"), cut_lines: lines.length - kept.length };
|
|
87
|
+
}
|
|
88
|
+
function isInteractiveLine(line) {
|
|
89
|
+
const match = LINE_PATTERN.exec(line);
|
|
90
|
+
if (!match)
|
|
91
|
+
return false;
|
|
92
|
+
const role = match[2];
|
|
93
|
+
if (INTERACTIVE_ROLES.has(role))
|
|
94
|
+
return true;
|
|
95
|
+
return line.includes("[cursor=pointer]");
|
|
96
|
+
}
|
|
97
|
+
/** The `/url` line directly under `index`, when the element has one. */
|
|
98
|
+
function childUrl(lines, index) {
|
|
99
|
+
const own = lines[index].search(/\S/);
|
|
100
|
+
for (let j = index + 1; j < lines.length; j++) {
|
|
101
|
+
const line = lines[j];
|
|
102
|
+
const indent = line.search(/\S/);
|
|
103
|
+
if (indent === -1 || indent <= own)
|
|
104
|
+
break;
|
|
105
|
+
const url = /^\s*- \/url: (.*)$/.exec(line);
|
|
106
|
+
if (url)
|
|
107
|
+
return url[1].trim();
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=snapshot-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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\nconst REF_PATTERN = /\\[ref=(e\\d+)\\]/;\nconst LINE_PATTERN = /^(\\s*)- (\\S+)(.*)$/;\n\n/** A bare ref as the caller writes it: `e8`. */\nexport function isRef(value: string): boolean {\n return /^e\\d+$/.test(value);\n}\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"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { BrowserContext, Page } from "playwright";
|
|
3
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Saved browser state — cookies plus per-origin localStorage.
|
|
6
|
+
*
|
|
7
|
+
* This is exactly what Playwright's `context.storageState()` returns and what
|
|
8
|
+
* `browser.newContext({ storageState })` accepts, which is the whole point:
|
|
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 CLAUDE.md
|
|
11
|
+
* "Auth persistence").
|
|
12
|
+
*
|
|
13
|
+
* The type is spelled out rather than imported because Playwright exports it
|
|
14
|
+
* only as the return type of a method, and the tools need to name it.
|
|
15
|
+
*/
|
|
16
|
+
export type StorageState = Awaited<ReturnType<BrowserContext["storageState"]>>;
|
|
17
|
+
/**
|
|
18
|
+
* Read a state file written by `framewatch_save_auth`.
|
|
19
|
+
*
|
|
20
|
+
* Every failure — no file, unreadable file, not JSON, JSON of the wrong shape
|
|
21
|
+
* — comes back as one line naming the path and the tool that produces a good
|
|
22
|
+
* one. A stale or missing auth file is the single most likely thing to go
|
|
23
|
+
* wrong in an authenticated capture, and "ENOENT: no such file or directory,
|
|
24
|
+
* open '…'" is not what the reader needs to see.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readStorageState(path: string): Promise<StorageState>;
|
|
27
|
+
/** The word that turns automatic pickup off for one call. */
|
|
28
|
+
export declare const NO_STORAGE_STATE = "none";
|
|
29
|
+
/** Where the saved auth lives unless `FRAMEWATCH_AUTH_STATE` says otherwise. */
|
|
30
|
+
export declare function defaultAuthPath(env?: NodeJS.ProcessEnv, cwd?: string): string;
|
|
31
|
+
/** A state that is about to be applied to a page, and where it came from. */
|
|
32
|
+
export interface ResolvedAuth {
|
|
33
|
+
path: string;
|
|
34
|
+
state: StorageState;
|
|
35
|
+
/** Picked up from the default location without being asked for. */
|
|
36
|
+
auto: boolean;
|
|
37
|
+
/** The file's modification time, when it could be read. */
|
|
38
|
+
saved_at?: Date;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The auth to apply for one call.
|
|
42
|
+
*
|
|
43
|
+
* An explicit path is read as it always was, and a missing one still fails
|
|
44
|
+
* loudly. With nothing asked for, the default file is used when it exists —
|
|
45
|
+
* the agent then cannot forget to pass it, which is the whole point — and
|
|
46
|
+
* `"none"` opens the page signed out. A default file that cannot be read is an
|
|
47
|
+
* error rather than a silent sign-out, because "the page showed the login
|
|
48
|
+
* form" would be blamed on the app.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveStorageState(requested: string | undefined, env?: NodeJS.ProcessEnv, cwd?: string): Promise<ResolvedAuth | null>;
|
|
51
|
+
/**
|
|
52
|
+
* The line a tool prints about the auth it applied, or null when there is
|
|
53
|
+
* nothing to say (an explicit file that worked). A login form on the page
|
|
54
|
+
* after restoring a session is the expiry signal, and it is named as such
|
|
55
|
+
* whether the file was automatic or not.
|
|
56
|
+
*/
|
|
57
|
+
export declare function describeAuth(auth: ResolvedAuth, loginVisible: boolean): string | null;
|
|
58
|
+
/** Whether the page is showing a login form: a visible password field. */
|
|
59
|
+
export declare function loginFormVisible(page: Page): Promise<boolean>;
|
|
60
|
+
/** The text block for `describeAuth`, when there is one. */
|
|
61
|
+
export declare function authNote(auth: ResolvedAuth | null, loginVisible: boolean): {
|
|
62
|
+
type: "text";
|
|
63
|
+
text: string;
|
|
64
|
+
} | null;
|
|
65
|
+
/** A tool result with the auth line appended, when there is one. Error results are left alone. */
|
|
66
|
+
export declare function withAuthNote(result: CallToolResult, auth: ResolvedAuth | null, loginVisible?: boolean): CallToolResult;
|
|
67
|
+
/** Write a state file, creating the directories on the way to it. */
|
|
68
|
+
export declare function writeStorageState(state: StorageState, path: string): Promise<void>;
|
|
69
|
+
/** One phrase describing what a state actually carries, for the tool's summary. */
|
|
70
|
+
export declare function storageStateSummary(state: StorageState): string;
|
|
71
|
+
/**
|
|
72
|
+
* The `storage_state` input every page-opening tool accepts. One definition so
|
|
73
|
+
* the wording (and therefore what an agent believes it does) is the same in
|
|
74
|
+
* all of them.
|
|
75
|
+
*/
|
|
76
|
+
export declare const storageStateField: z.ZodOptional<z.ZodString>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { DEFAULT_AUTH_STATE_PATH } from "../constants.js";
|
|
5
|
+
/**
|
|
6
|
+
* Read a state file written by `framewatch_save_auth`.
|
|
7
|
+
*
|
|
8
|
+
* Every failure — no file, unreadable file, not JSON, JSON of the wrong shape
|
|
9
|
+
* — comes back as one line naming the path and the tool that produces a good
|
|
10
|
+
* one. A stale or missing auth file is the single most likely thing to go
|
|
11
|
+
* wrong in an authenticated capture, and "ENOENT: no such file or directory,
|
|
12
|
+
* open '…'" is not what the reader needs to see.
|
|
13
|
+
*/
|
|
14
|
+
export async function readStorageState(path) {
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
raw = await readFile(path, "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const code = error.code;
|
|
21
|
+
const reason = code === "ENOENT"
|
|
22
|
+
? "not found"
|
|
23
|
+
: code === "EISDIR"
|
|
24
|
+
? "is a directory, not a file"
|
|
25
|
+
: code === "EACCES"
|
|
26
|
+
? "could not be read (permission denied)"
|
|
27
|
+
: `could not be read (${firstLine(error)})`;
|
|
28
|
+
throw new Error(`${prefix(path)} ${reason}. ${RECREATE}`);
|
|
29
|
+
}
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(raw);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new Error(`${prefix(path)} is not valid JSON (${firstLine(error)}). ${RECREATE}`);
|
|
36
|
+
}
|
|
37
|
+
if (!isStorageState(parsed)) {
|
|
38
|
+
throw new Error(`${prefix(path)} is not a browser state file — it has no \`cookies\` and \`origins\` arrays. ${RECREATE}`);
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
/** The word that turns automatic pickup off for one call. */
|
|
43
|
+
export const NO_STORAGE_STATE = "none";
|
|
44
|
+
/** Where the saved auth lives unless `FRAMEWATCH_AUTH_STATE` says otherwise. */
|
|
45
|
+
export function defaultAuthPath(env = process.env, cwd = process.cwd()) {
|
|
46
|
+
const override = env.FRAMEWATCH_AUTH_STATE;
|
|
47
|
+
return override && override.trim() !== "" ? override : join(cwd, DEFAULT_AUTH_STATE_PATH);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The auth to apply for one call.
|
|
51
|
+
*
|
|
52
|
+
* An explicit path is read as it always was, and a missing one still fails
|
|
53
|
+
* loudly. With nothing asked for, the default file is used when it exists —
|
|
54
|
+
* the agent then cannot forget to pass it, which is the whole point — and
|
|
55
|
+
* `"none"` opens the page signed out. A default file that cannot be read is an
|
|
56
|
+
* error rather than a silent sign-out, because "the page showed the login
|
|
57
|
+
* form" would be blamed on the app.
|
|
58
|
+
*/
|
|
59
|
+
export async function resolveStorageState(requested, env = process.env, cwd = process.cwd()) {
|
|
60
|
+
if (requested === NO_STORAGE_STATE)
|
|
61
|
+
return null;
|
|
62
|
+
if (requested !== undefined && requested.trim() !== "") {
|
|
63
|
+
return { path: requested, state: await readStorageState(requested), auto: false, ...(await savedAt(requested)) };
|
|
64
|
+
}
|
|
65
|
+
const path = defaultAuthPath(env, cwd);
|
|
66
|
+
let exists;
|
|
67
|
+
try {
|
|
68
|
+
exists = (await stat(path)).isFile();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
exists = false;
|
|
72
|
+
}
|
|
73
|
+
if (!exists)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
return { path, state: await readStorageState(path), auto: true, ...(await savedAt(path)) };
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw new Error(`${firstLine(error)} It was picked up automatically; pass storage_state: "${NO_STORAGE_STATE}" to open the page without it.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function savedAt(path) {
|
|
83
|
+
try {
|
|
84
|
+
return { saved_at: (await stat(path)).mtime };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The line a tool prints about the auth it applied, or null when there is
|
|
92
|
+
* nothing to say (an explicit file that worked). A login form on the page
|
|
93
|
+
* after restoring a session is the expiry signal, and it is named as such
|
|
94
|
+
* whether the file was automatic or not.
|
|
95
|
+
*/
|
|
96
|
+
export function describeAuth(auth, loginVisible) {
|
|
97
|
+
const when = auth.saved_at ? ` (saved ${ago(auth.saved_at)})` : "";
|
|
98
|
+
if (loginVisible) {
|
|
99
|
+
return `Auth: ${auth.path}${when} did not sign you in — this page shows a login form. Run framewatch_save_auth again to refresh it.`;
|
|
100
|
+
}
|
|
101
|
+
if (!auth.auto)
|
|
102
|
+
return null;
|
|
103
|
+
return `Auth: using ${auth.path}${when} — pass storage_state: "${NO_STORAGE_STATE}" to open the page signed out.`;
|
|
104
|
+
}
|
|
105
|
+
/** Whether the page is showing a login form: a visible password field. */
|
|
106
|
+
export async function loginFormVisible(page) {
|
|
107
|
+
try {
|
|
108
|
+
return await page.evaluate(() => {
|
|
109
|
+
const doc = globalThis.document;
|
|
110
|
+
if (!doc)
|
|
111
|
+
return false;
|
|
112
|
+
const fields = doc.querySelectorAll('input[type="password"]');
|
|
113
|
+
for (let i = 0; i < fields.length; i++) {
|
|
114
|
+
const rect = fields[i].getBoundingClientRect();
|
|
115
|
+
if (rect.width > 0 && rect.height > 0)
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** The text block for `describeAuth`, when there is one. */
|
|
126
|
+
export function authNote(auth, loginVisible) {
|
|
127
|
+
if (!auth)
|
|
128
|
+
return null;
|
|
129
|
+
const text = describeAuth(auth, loginVisible);
|
|
130
|
+
return text ? { type: "text", text } : null;
|
|
131
|
+
}
|
|
132
|
+
/** A tool result with the auth line appended, when there is one. Error results are left alone. */
|
|
133
|
+
export function withAuthNote(result, auth, loginVisible = false) {
|
|
134
|
+
const note = authNote(auth, loginVisible);
|
|
135
|
+
if (!note || result.isError)
|
|
136
|
+
return result;
|
|
137
|
+
return { ...result, content: [...result.content, note] };
|
|
138
|
+
}
|
|
139
|
+
function ago(date) {
|
|
140
|
+
const seconds = Math.max(0, Math.round((Date.now() - date.getTime()) / 1000));
|
|
141
|
+
if (seconds < 60)
|
|
142
|
+
return "just now";
|
|
143
|
+
const minutes = Math.round(seconds / 60);
|
|
144
|
+
if (minutes < 60)
|
|
145
|
+
return `${minutes}m ago`;
|
|
146
|
+
const hours = Math.round(minutes / 60);
|
|
147
|
+
if (hours < 48)
|
|
148
|
+
return `${hours}h ago`;
|
|
149
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
150
|
+
}
|
|
151
|
+
/** Write a state file, creating the directories on the way to it. */
|
|
152
|
+
export async function writeStorageState(state, path) {
|
|
153
|
+
const parent = dirname(path);
|
|
154
|
+
if (parent)
|
|
155
|
+
await mkdir(parent, { recursive: true });
|
|
156
|
+
await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
157
|
+
}
|
|
158
|
+
/** One phrase describing what a state actually carries, for the tool's summary. */
|
|
159
|
+
export function storageStateSummary(state) {
|
|
160
|
+
const cookies = state.cookies.length;
|
|
161
|
+
const origins = state.origins.length;
|
|
162
|
+
const keys = state.origins.reduce((total, origin) => total + origin.localStorage.length, 0);
|
|
163
|
+
if (cookies === 0 && keys === 0)
|
|
164
|
+
return "no cookies and nothing in storage";
|
|
165
|
+
return `${count(cookies, "cookie")}, ${count(origins, "origin")} with ${count(keys, "stored key")}`;
|
|
166
|
+
}
|
|
167
|
+
function count(n, noun) {
|
|
168
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
169
|
+
}
|
|
170
|
+
const RECREATE = "Run framewatch_save_auth to create it.";
|
|
171
|
+
function prefix(path) {
|
|
172
|
+
return `Auth state file "${path}"`;
|
|
173
|
+
}
|
|
174
|
+
function firstLine(error) {
|
|
175
|
+
return (error instanceof Error ? error.message : String(error)).split("\n")[0];
|
|
176
|
+
}
|
|
177
|
+
function isStorageState(value) {
|
|
178
|
+
if (typeof value !== "object" || value === null)
|
|
179
|
+
return false;
|
|
180
|
+
const { cookies, origins } = value;
|
|
181
|
+
return Array.isArray(cookies) && Array.isArray(origins);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The `storage_state` input every page-opening tool accepts. One definition so
|
|
185
|
+
* the wording (and therefore what an agent believes it does) is the same in
|
|
186
|
+
* all of them.
|
|
187
|
+
*/
|
|
188
|
+
export const storageStateField = z
|
|
189
|
+
.string()
|
|
190
|
+
.optional()
|
|
191
|
+
.describe("Path to an auth state file saved by framewatch_save_auth. The page opens with those cookies and " +
|
|
192
|
+
"storage already in place, so a login/gate does not have to be replayed. Omit it and the default file " +
|
|
193
|
+
"(.framewatch/auth.json) is used automatically when it exists; pass \"none\" to open the page signed out. " +
|
|
194
|
+
"If the saved session has expired the result says so — re-run framewatch_save_auth then.");
|
|
195
|
+
//# sourceMappingURL=storage-state.js.map
|