@eternify-labs/seo-audit 0.1.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/LICENSE +33 -0
- package/README.md +85 -0
- package/dist/aiAudit-BMGGUSUE.js +2 -0
- package/dist/aiAudit-VMLJU5MR.js +2 -0
- package/dist/chunk-5LRP76QX.js +2 -0
- package/dist/chunk-6YM6XKUS.js +2 -0
- package/dist/chunk-7WE77JR3.js +10 -0
- package/dist/chunk-GMBYTQMU.js +10 -0
- package/dist/chunk-LXZFV4HU.js +11 -0
- package/dist/chunk-LYSA6WGH.js +10 -0
- package/dist/chunk-VDJKW42K.js +11 -0
- package/dist/chunk-W2JU7LEZ.js +10 -0
- package/dist/chunk-ZMLT3V5C.js +2 -0
- package/dist/cli.js +37 -0
- package/dist/render-CCORCUAB.js +2 -0
- package/dist/report-NAVA5HJU.js +2 -0
- package/dist/report-NRD5GHHS.js +2 -0
- package/dist/report-TQRVRW54.js +2 -0
- package/dist/report-Y6V3LFPY.js +2 -0
- package/dist/scoring-ZBWTEXWJ.js +2 -0
- package/dist/ui/assets/index-C8yCj8Iz.css +1 -0
- package/dist/ui/assets/index-DB7HIYjL.js +67 -0
- package/dist/ui/index.html +13 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Eternify Labs SEO/AEO Audit — End User License Agreement
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eternify Labs. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software is proprietary. It is provided free of charge for use under the
|
|
6
|
+
following terms:
|
|
7
|
+
|
|
8
|
+
1. GRANT. You are granted a non-exclusive, non-transferable, revocable license
|
|
9
|
+
to install and use this software, in unmodified binary form, for auditing
|
|
10
|
+
websites and codebases you own or are authorized to audit.
|
|
11
|
+
|
|
12
|
+
2. RESTRICTIONS. You may NOT:
|
|
13
|
+
a. copy, redistribute, sell, rent, sublicense, or host the software for
|
|
14
|
+
third parties;
|
|
15
|
+
b. modify, adapt, translate, or create derivative works of the software;
|
|
16
|
+
c. reverse engineer, decompile, or disassemble the software, except to the
|
|
17
|
+
extent such restriction is prohibited by applicable law;
|
|
18
|
+
d. remove or alter any proprietary notices.
|
|
19
|
+
|
|
20
|
+
3. AI PROVIDERS. The software connects to AI services (Anthropic, OpenAI) using
|
|
21
|
+
credentials you supply. Your use of those services is governed by your own
|
|
22
|
+
agreements with those providers, and you are responsible for the associated
|
|
23
|
+
usage and costs.
|
|
24
|
+
|
|
25
|
+
4. NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
26
|
+
EXPRESS OR IMPLIED. AUDIT RESULTS AND AUTOMATED CODE CHANGES ARE ADVISORY;
|
|
27
|
+
REVIEW ALL CHANGES BEFORE DEPLOYING THEM.
|
|
28
|
+
|
|
29
|
+
5. LIMITATION OF LIABILITY. IN NO EVENT SHALL ETERNIFY LABS BE LIABLE FOR ANY
|
|
30
|
+
CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
|
|
31
|
+
|
|
32
|
+
6. TERMINATION. This license terminates automatically if you breach these
|
|
33
|
+
terms. Upon termination you must delete all copies of the software.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Eternify SEO/AEO Audit
|
|
2
|
+
|
|
3
|
+
CLI tool with a local web UI that audits **domains** and **codebases** for SEO
|
|
4
|
+
(search engine optimization) and AEO (answer engine optimization — visibility in
|
|
5
|
+
ChatGPT/Claude/Perplexity answers). Free to use, closed source.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx @eternify-labs/seo-audit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
First run walks through a Claude Code–style setup: pick a provider (Claude via
|
|
12
|
+
your Claude Code subscription login, or OpenAI via ChatGPT sign-in / API key),
|
|
13
|
+
pick a model, verify the connection — then the web UI opens at
|
|
14
|
+
`http://localhost:4173`.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
Every audit runs in **two rounds**:
|
|
19
|
+
|
|
20
|
+
1. **Rules** — a deterministic engine checks ~40 measurable facts (titles,
|
|
21
|
+
canonicals, structured data, robots/sitemap, llms.txt, AI-crawler access,
|
|
22
|
+
alt text, rendering strategy…). Reproducible, evidence-backed, free.
|
|
23
|
+
2. **AI auditor** — the model then audits like an expert consultant: for
|
|
24
|
+
domains it reviews the actual crawled content; for codebases it **explores
|
|
25
|
+
the repository itself with read-only tools** (read/search/list — the same
|
|
26
|
+
way Claude Code works), whatever the stack: WordPress/PHP, Django, Flask,
|
|
27
|
+
Rails, Laravel, Next.js, plain HTML… Its findings merge with the rule
|
|
28
|
+
findings (badged "AI" in the UI) and the same penalty engine scores the
|
|
29
|
+
combined set into the 0–100 scorecard.
|
|
30
|
+
|
|
31
|
+
- **Domain Audit** — crawl (optionally JS-rendered via headless Chrome) →
|
|
32
|
+
two-round findings + Core Web Vitals (PageSpeed Insights/CrUX) → five-category
|
|
33
|
+
scorecard + prioritized action plan.
|
|
34
|
+
- **Keywords** — coverage/gap/intent analysis of target keywords against the
|
|
35
|
+
crawled content; enriched with **real Search Console queries** (when
|
|
36
|
+
connected) and **real search volumes** (DataForSEO, optional).
|
|
37
|
+
- **AI Visibility** — prompt tracking: asks your model the questions real
|
|
38
|
+
users ask, measures whether your site is the answer (primary / mentioned /
|
|
39
|
+
absent), scores visibility 0–100, and shows which competitors the AI
|
|
40
|
+
recommends instead.
|
|
41
|
+
- **Codebase** — two-round scan of any local repo, then **auto-fix**: an AI
|
|
42
|
+
agent edits the code, every change is shown as a git diff, and any fix can
|
|
43
|
+
be reverted with one click. Requires a clean git tree so everything stays
|
|
44
|
+
revertible.
|
|
45
|
+
|
|
46
|
+
## Integrations (all optional, configured in Settings)
|
|
47
|
+
|
|
48
|
+
- **Google Search Console** — bring your own free Desktop OAuth client; real
|
|
49
|
+
query/impression/position data feeds the Keywords page and analysis.
|
|
50
|
+
- **PageSpeed Insights** — works keyless (tiny shared quota) or with a free
|
|
51
|
+
Google API key; adds the Performance category (LCP/INP/CLS + Lighthouse).
|
|
52
|
+
- **JS rendering** — playwright-core + your installed Chrome; audits
|
|
53
|
+
client-rendered sites the way crawlers with rendering see them.
|
|
54
|
+
- **DataForSEO** — real monthly search volumes on the Keywords page.
|
|
55
|
+
|
|
56
|
+
## CLI
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
eternify-seo # start the web UI (default)
|
|
60
|
+
eternify-seo login # (re)configure provider / auth / model
|
|
61
|
+
eternify-seo audit <domain> # headless domain audit (--pages N, --no-ai, --json)
|
|
62
|
+
eternify-seo scan [path] # headless codebase audit (--no-ai, --json)
|
|
63
|
+
eternify-seo config # show current configuration
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Scores come from one penalty engine applied to both rounds' findings — the AI
|
|
67
|
+
contributes by *finding issues with evidence*, never by inventing numbers.
|
|
68
|
+
With `--no-ai` only round 1 runs and the score is purely rule-based.
|
|
69
|
+
|
|
70
|
+
Config lives in `~/.eternify/config.json` (chmod 600); reports in
|
|
71
|
+
`~/.eternify/reports/`. Everything runs locally — page content is sent only to
|
|
72
|
+
the AI provider you chose.
|
|
73
|
+
|
|
74
|
+
## Development (Eternify Labs internal)
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
npm install
|
|
78
|
+
npm run dev # CLI + server (tsx)
|
|
79
|
+
npm run dev:ui # Vite dev server for the UI (proxies /api to :4173)
|
|
80
|
+
npm test # rule-engine unit tests (vitest)
|
|
81
|
+
npm run build # dist/cli.js (minified) + dist/ui/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Publishing ships **only** the minified `dist/` (see `files` in package.json) —
|
|
85
|
+
source stays in this private monorepo.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{z as e}from"zod";var n=e.enum(["claude","openai"]),x=e.object({provider:n,model:e.string().optional(),authMethod:e.enum(["subscription","api-key"]).optional(),anthropicApiKey:e.string().optional(),openaiApiKey:e.string().optional(),crawl:e.object({maxPages:e.number().int().min(1).max(200).default(25),timeoutMs:e.number().int().default(1e4),render:e.boolean().default(!1)}).default({maxPages:25,timeoutMs:1e4,render:!1}),performance:e.boolean().default(!0),psiApiKey:e.string().optional(),dataForSeoLogin:e.string().optional(),dataForSeoPassword:e.string().optional(),gsc:e.object({clientId:e.string(),clientSecret:e.string(),refreshToken:e.string().optional()}).optional(),createdAt:e.string().optional(),updatedAt:e.string().optional()}),a=e.enum(["critical","warning","info","pass"]),t=e.enum(["technical","content","structured-data","aeo","performance"]),p=e.object({location:e.string(),snippet:e.string().optional()}),r=e.object({id:e.string(),ruleId:e.string(),category:t,severity:a,title:e.string(),description:e.string(),recommendation:e.string(),evidence:e.array(p).default([]),occurrences:e.number().int().default(1),source:e.enum(["rule","ai"]).default("rule")}),s=e.object({category:t,label:e.string(),score:e.number().min(0).max(100)}),o=e.object({overall:e.number().min(0).max(100),categories:e.array(s)}),c=e.object({priority:e.number().int().min(1),title:e.string(),detail:e.string(),impact:e.enum(["high","medium","low"]),effort:e.enum(["high","medium","low"])}),i=e.object({summary:e.string(),strengths:e.array(e.string()),actionPlan:e.array(c)}),d=e.object({url:e.string(),status:e.number(),title:e.string().optional(),metaDescription:e.string().optional(),h1:e.string().optional(),wordCount:e.number()}),g=e.object({id:e.string(),type:e.literal("domain"),domain:e.string(),createdAt:e.string(),pagesCrawled:e.number(),pages:e.array(d),scorecard:o,findings:e.array(r),llmReport:i.optional()}),y=e.object({keyword:e.string(),intent:e.enum(["informational","navigational","transactional","commercial"]),coverage:e.enum(["strong","partial","missing"]),bestPage:e.string().optional(),aeoCitable:e.boolean(),notes:e.string(),searchVolume:e.number().int().optional()}),m=e.object({id:e.string(),type:e.literal("keywords"),auditId:e.string(),domain:e.string(),createdAt:e.string(),keywords:e.array(y),gaps:e.array(e.object({keyword:e.string(),rationale:e.string()})),summary:e.string()}),l=e.object({id:e.string(),type:e.literal("codebase"),path:e.string(),framework:e.string(),createdAt:e.string(),filesScanned:e.number(),scorecard:o,findings:e.array(r),llmReport:i.optional()}),u=e.object({prompt:e.string(),presence:e.enum(["primary","mentioned","absent"]),answerExcerpt:e.string(),competitors:e.array(e.string())}),f=e.object({id:e.string(),type:e.literal("ai-visibility"),domain:e.string(),createdAt:e.string(),visibilityScore:e.number().min(0).max(100),prompts:e.array(u),competitorLeaderboard:e.array(e.object({name:e.string(),count:e.number().int()})),summary:e.string()}),A=e.discriminatedUnion("type",[g,m,l,f]);export{x as a,r as b,i as c,g as d,m as e,l as f,f as g,A as h};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var i={technical:"Technical SEO",content:"On-Page Content","structured-data":"Structured Data",aeo:"AEO Readiness"},u={technical:.3,content:.3,"structured-data":.15,aeo:.25},g={critical:25,warning:9,info:3,pass:0};function h(o){let t=g[o.severity]??0,c=Math.min(1.8,1+Math.log10(Math.max(1,o.occurrences))*.5);return t*c}function m(o){let t=Object.keys(i).map(n=>{let e=o.filter(r=>r.category===n).reduce((r,a)=>r+h(a),0);return{category:n,label:i[n],score:Math.max(0,Math.round(100-e))}});return{overall:Math.round(t.reduce((n,e)=>n+e.score*u[e.category],0)),categories:t}}var d=.3;function p(o,t){let c=o.categories.map(e=>{let r=t.find(l=>l.category===e.category);if(!r)return e;let a=e.ruleScore??e.score,s=Math.max(0,Math.min(100,Math.round(r.score)));return{...e,ruleScore:a,aiScore:s,score:Math.round(a*(1-d)+s*d)}});return{overall:Math.round(c.reduce((e,r)=>e+r.score*u[r.category],0)),categories:c}}export{m as a,p as b};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{c as i}from"./chunk-5LRP76QX.js";var p={type:"object",additionalProperties:!1,required:["summary","strengths","actionPlan"],properties:{summary:{type:"string",description:"3-5 sentence executive summary of the audit for the site owner"},strengths:{type:"array",items:{type:"string"},description:"What the site already does well"},actionPlan:{type:"array",description:"Prioritized, concrete actions. Priority 1 = do first.",items:{type:"object",additionalProperties:!1,required:["priority","title","detail","impact","effort"],properties:{priority:{type:"integer",minimum:1},title:{type:"string"},detail:{type:"string",description:"Specific how-to for THIS site, referencing actual URLs/pages where possible"},impact:{type:"string",enum:["high","medium","low"]},effort:{type:"string",enum:["high","medium","low"]}}}}}};async function l(n,t){let r=t.findings.map(e=>({rule:e.ruleId,foundBy:e.source,category:e.category,severity:e.severity,title:e.title,occurrences:e.occurrences,examples:e.evidence.slice(0,3).map(c=>c.location)})),o="You are a senior SEO and AEO (answer engine optimization) consultant writing up an audit. The findings come from two rounds \u2014 a deterministic rule engine and an AI audit pass \u2014 plus content samples. Produce the report for the site owner. Be concrete and specific to this subject \u2014 never generic advice. Do not invent new findings; synthesize the given ones into themes and a plan.",s=[`Audit subject: ${t.subject}`,`Scores (computed, do not change): overall ${t.scorecard.overall}/100; `+t.scorecard.categories.map(e=>`${e.label} ${e.score}`).join(", "),`
|
|
3
|
+
Findings (JSON):
|
|
4
|
+
${JSON.stringify(r,null,1)}`,`
|
|
5
|
+
Content samples:
|
|
6
|
+
${t.contentSamples.slice(0,5).map(e=>`--- ${e.location}
|
|
7
|
+
${e.sample.slice(0,1200)}`).join(`
|
|
8
|
+
`)}`,`
|
|
9
|
+
Write the executive summary, list genuine strengths, and produce a prioritized action plan (max 10 items) ordered by impact-per-effort.`].join(`
|
|
10
|
+
`),a=await n.analyzeJson({system:o,prompt:s,jsonSchema:p});return i.parse(a)}export{l as a};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{c as i}from"./chunk-ZMLT3V5C.js";var p={type:"object",additionalProperties:!1,required:["summary","strengths","actionPlan"],properties:{summary:{type:"string",description:"3-5 sentence executive summary of the audit for the site owner"},strengths:{type:"array",items:{type:"string"},description:"What the site already does well"},actionPlan:{type:"array",description:"Prioritized, concrete actions. Priority 1 = do first.",items:{type:"object",additionalProperties:!1,required:["priority","title","detail","impact","effort"],properties:{priority:{type:"integer",minimum:1},title:{type:"string"},detail:{type:"string",description:"Specific how-to for THIS site, referencing actual URLs/pages where possible"},impact:{type:"string",enum:["high","medium","low"]},effort:{type:"string",enum:["high","medium","low"]}}}}}};async function l(n,t){let r=t.findings.map(e=>({rule:e.ruleId,foundBy:e.source,category:e.category,severity:e.severity,title:e.title,occurrences:e.occurrences,examples:e.evidence.slice(0,3).map(c=>c.location)})),o="You are a senior SEO and AEO (answer engine optimization) consultant writing up an audit. The findings come from two rounds \u2014 a deterministic rule engine and an AI audit pass \u2014 plus content samples. Produce the report for the site owner. Be concrete and specific to this subject \u2014 never generic advice. Do not invent new findings; synthesize the given ones into themes and a plan.",s=[`Audit subject: ${t.subject}`,`Scores (computed, do not change): overall ${t.scorecard.overall}/100; `+t.scorecard.categories.map(e=>`${e.label} ${e.score}`).join(", "),`
|
|
3
|
+
Findings (JSON):
|
|
4
|
+
${JSON.stringify(r,null,1)}`,`
|
|
5
|
+
Content samples:
|
|
6
|
+
${t.contentSamples.slice(0,5).map(e=>`--- ${e.location}
|
|
7
|
+
${e.sample.slice(0,1200)}`).join(`
|
|
8
|
+
`)}`,`
|
|
9
|
+
Write the executive summary, list genuine strengths, and produce a prioritized action plan (max 10 items) ordered by impact-per-effort.`].join(`
|
|
10
|
+
`),a=await n.analyzeJson({system:o,prompt:s,jsonSchema:p});return i.parse(a)}export{l as a};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{b as d}from"./chunk-5LRP76QX.js";import{z as r}from"zod";var l={type:"object",additionalProperties:!1,required:["findings"],properties:{findings:{type:"array",maxItems:12,items:{type:"object",additionalProperties:!1,required:["category","severity","title","description","recommendation","evidence"],properties:{category:{type:"string",enum:["technical","content","structured-data","aeo"]},severity:{type:"string",enum:["critical","warning","info"]},title:{type:"string",description:"Short, specific issue title"},description:{type:"string",description:"What is wrong and why it hurts SEO/AEO, specific to THIS subject"},recommendation:{type:"string",description:"Concrete fix for this specific site/codebase"},evidence:{type:"array",maxItems:5,items:{type:"object",additionalProperties:!1,required:["location"],properties:{location:{type:"string",description:"Exact URL or file path where you observed this"},snippet:{type:"string"}}}}}}}}},y=r.object({findings:r.array(d.pick({category:!0,severity:!0,title:!0,description:!0,recommendation:!0}).extend({evidence:r.array(r.object({location:r.string(),snippet:r.string().optional()}))}))}),m=0;function u(i){return y.parse(i).findings.map(n=>d.parse({...n,id:`ai-${++m}`,ruleId:"ai-audit",occurrences:Math.max(1,n.evidence.length),source:"ai"}))}function p(i){return i.map(t=>`- ${t.ruleId}: ${t.title}`).join(`
|
|
3
|
+
`)}var g="Report ONLY issues NOT already covered by the rule findings listed above \u2014 your value is finding what mechanical rules cannot: content quality, messaging clarity, topical authority, E-E-A-T signals, answerability for AI engines, information architecture, intent mismatches, misleading markup, framework-specific pitfalls. Every finding must cite real evidence you actually observed (exact URLs or file paths). If something is genuinely fine, do not invent problems \u2014 returning few findings is a valid outcome. Severity guide: critical = actively losing rankings/citations today; warning = meaningful lost opportunity; info = polish.";async function v(i,t,n){let s=t.pages.map(e=>({url:e.url,status:e.status,title:e.title,metaDescription:e.metaDescription,canonical:e.canonical,h1s:e.h1s,headings:e.headings.slice(0,20).map(c=>`h${c.level}: ${c.text}`),jsonLdTypes:e.jsonLd.length,og:e.og,wordCount:e.wordCount,textSample:e.textSample.slice(0,1500)})),a="You are a senior SEO/AEO auditor doing the second, human-quality pass of a website audit. A deterministic rule engine already ran (its findings are listed). Now audit the site the way an expert consultant reading the actual pages would.",o=[`Site: ${t.origin}`,`robots.txt:
|
|
4
|
+
${t.robots.raw.slice(0,2e3)||"(none)"}`,`Crawled pages (JSON):
|
|
5
|
+
${JSON.stringify(s,null,1)}`,`Rule findings already reported (do NOT repeat these):
|
|
6
|
+
${p(n)}`,g].join(`
|
|
7
|
+
|
|
8
|
+
`);return u(await i.analyzeJson({system:a,prompt:o,jsonSchema:l}))}async function w(i,t,n,s,a){let o="You are a senior SEO/AEO engineer auditing a codebase, exactly as if the owner asked you to 'review and improve my SEO'. You have read-only access to the repository: explore it yourself with the available tools (read files, search, list). Work stack-agnostically \u2014 this could be WordPress/PHP, Django/Flask, Rails, Next.js, plain HTML, or anything else; first understand how this project renders its pages and where head tags, routing, templates, and static assets live, then audit it.",e=[`Repository: ${t} (detected as: ${n} \u2014 verify this yourself, detection may be wrong).`,"Audit for: metadata handling (titles/descriptions/canonicals per route), structured data, Open Graph, robots/sitemap/llms.txt generation, rendering strategy (is content in the HTML crawlers receive?), image alt handling, semantic HTML, redirects/status handling, i18n/hreflang, and AEO readiness (answerable content structure, AI-crawler access).",`Rule findings already reported (do NOT repeat these):
|
|
9
|
+
${p(s)}`,g,"Cite file paths relative to the repository root as evidence."].join(`
|
|
10
|
+
|
|
11
|
+
`);return u(await i.exploreJson({cwd:t,system:o,prompt:e,jsonSchema:l,onEvent:a}))}export{v as a,w as b};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{z as e}from"zod";var l=e.enum(["claude","openai"]),z=e.object({provider:l,model:e.string().optional(),authMethod:e.enum(["subscription","api-key"]).optional(),anthropicApiKey:e.string().optional(),openaiApiKey:e.string().optional(),crawl:e.object({maxPages:e.number().int().min(1).max(200).default(25),timeoutMs:e.number().int().default(1e4)}).default({maxPages:25,timeoutMs:1e4}),createdAt:e.string().optional(),updatedAt:e.string().optional()}),m=e.enum(["critical","warning","info","pass"]),i=e.enum(["technical","content","structured-data","aeo"]),u=e.object({location:e.string(),snippet:e.string().optional()}),n=e.object({id:e.string(),ruleId:e.string(),category:i,severity:m,title:e.string(),description:e.string(),recommendation:e.string(),evidence:e.array(u).default([]),occurrences:e.number().int().default(1)}),f=e.object({category:i,label:e.string(),score:e.number().min(0).max(100),ruleScore:e.number().min(0).max(100).optional(),aiScore:e.number().min(0).max(100).optional()}),a=e.object({overall:e.number().min(0).max(100),categories:e.array(f)}),b=e.object({priority:e.number().int().min(1),title:e.string(),detail:e.string(),impact:e.enum(["high","medium","low"]),effort:e.enum(["high","medium","low"])}),x=e.object({category:i,score:e.number().min(0).max(100),rationale:e.string()}),o=e.object({summary:e.string(),strengths:e.array(e.string()),actionPlan:e.array(b),categoryOpinions:e.array(x).optional()}),h=e.object({url:e.string(),status:e.number(),title:e.string().optional(),metaDescription:e.string().optional(),h1:e.string().optional(),wordCount:e.number()}),v=e.object({id:e.string(),type:e.literal("domain"),domain:e.string(),createdAt:e.string(),pagesCrawled:e.number(),pages:e.array(h),scorecard:a,findings:e.array(n),llmReport:o.optional()}),w=e.object({keyword:e.string(),intent:e.enum(["informational","navigational","transactional","commercial"]),coverage:e.enum(["strong","partial","missing"]),bestPage:e.string().optional(),aeoCitable:e.boolean(),notes:e.string()}),j=e.object({id:e.string(),type:e.literal("keywords"),auditId:e.string(),domain:e.string(),createdAt:e.string(),keywords:e.array(w),gaps:e.array(e.object({keyword:e.string(),rationale:e.string()})),summary:e.string()}),R=e.object({id:e.string(),type:e.literal("codebase"),path:e.string(),framework:e.string(),createdAt:e.string(),filesScanned:e.number(),scorecard:a,findings:e.array(n),llmReport:o.optional()}),C=e.discriminatedUnion("type",[v,j,R]);var S={type:"object",additionalProperties:!1,required:["summary","strengths","actionPlan","categoryOpinions"],properties:{summary:{type:"string",description:"3-5 sentence executive summary of the audit for the site owner"},strengths:{type:"array",items:{type:"string"},description:"What the site already does well"},categoryOpinions:{type:"array",description:"Your own expert 0-100 rating of each category, judged from the actual content and findings \u2014 not a copy of the rule scores. Rate all four categories.",items:{type:"object",additionalProperties:!1,required:["category","score","rationale"],properties:{category:{type:"string",enum:["technical","content","structured-data","aeo"]},score:{type:"integer",minimum:0,maximum:100},rationale:{type:"string",description:"1-2 sentences justifying your rating"}}}},actionPlan:{type:"array",description:"Prioritized, concrete actions. Priority 1 = do first.",items:{type:"object",additionalProperties:!1,required:["priority","title","detail","impact","effort"],properties:{priority:{type:"integer",minimum:1},title:{type:"string"},detail:{type:"string",description:"Specific how-to for THIS site, referencing actual URLs/pages where possible"},impact:{type:"string",enum:["high","medium","low"]},effort:{type:"string",enum:["high","medium","low"]}}}}}};async function O(s,r){let p=r.findings.map(t=>({rule:t.ruleId,category:t.category,severity:t.severity,title:t.title,occurrences:t.occurrences,examples:t.evidence.slice(0,3).map(y=>y.location)})),c="You are a senior SEO and AEO (answer engine optimization) consultant. You are given the deterministic findings of an automated audit plus content samples. Produce a report for the site owner. Be concrete and specific to this site \u2014 never generic advice. Do not invent findings, but you may connect findings into higher-level themes. Additionally, give your OWN expert 0-100 rating per category (categoryOpinions): judge content quality, topical authority, answerability and technical craft from the actual samples \u2014 your rating is independent of the rule-based scores and will be blended into the final scorecard.",d=[`Audit subject: ${r.subject}`,`Rule-based scores (for reference): overall ${r.scorecard.overall}/100; `+r.scorecard.categories.map(t=>`${t.label} ${t.score}`).join(", "),`
|
|
3
|
+
Findings (JSON):
|
|
4
|
+
${JSON.stringify(p,null,1)}`,`
|
|
5
|
+
Content samples:
|
|
6
|
+
${r.contentSamples.slice(0,5).map(t=>`--- ${t.location}
|
|
7
|
+
${t.sample.slice(0,1200)}`).join(`
|
|
8
|
+
`)}`,`
|
|
9
|
+
Write the executive summary, list genuine strengths, and produce a prioritized action plan (max 10 items) ordered by impact-per-effort.`].join(`
|
|
10
|
+
`),g=await s.analyzeJson({system:c,prompt:d,jsonSchema:S});return o.parse(g)}export{z as a,v as b,j as c,R as d,C as e,O as f};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{b as d}from"./chunk-ZMLT3V5C.js";import{z as r}from"zod";var l={type:"object",additionalProperties:!1,required:["findings"],properties:{findings:{type:"array",maxItems:12,items:{type:"object",additionalProperties:!1,required:["category","severity","title","description","recommendation","evidence"],properties:{category:{type:"string",enum:["technical","content","structured-data","aeo"]},severity:{type:"string",enum:["critical","warning","info"]},title:{type:"string",description:"Short, specific issue title"},description:{type:"string",description:"What is wrong and why it hurts SEO/AEO, specific to THIS subject"},recommendation:{type:"string",description:"Concrete fix for this specific site/codebase"},evidence:{type:"array",maxItems:5,items:{type:"object",additionalProperties:!1,required:["location"],properties:{location:{type:"string",description:"Exact URL or file path where you observed this"},snippet:{type:"string"}}}}}}}}},y=r.object({findings:r.array(d.pick({category:!0,severity:!0,title:!0,description:!0,recommendation:!0}).extend({evidence:r.array(r.object({location:r.string(),snippet:r.string().optional()}))}))}),m=0;function u(i){return y.parse(i).findings.map(n=>d.parse({...n,id:`ai-${++m}`,ruleId:"ai-audit",occurrences:Math.max(1,n.evidence.length),source:"ai"}))}function p(i){return i.map(t=>`- ${t.ruleId}: ${t.title}`).join(`
|
|
3
|
+
`)}var g="Report ONLY issues NOT already covered by the rule findings listed above \u2014 your value is finding what mechanical rules cannot: content quality, messaging clarity, topical authority, E-E-A-T signals, answerability for AI engines, information architecture, intent mismatches, misleading markup, framework-specific pitfalls. Every finding must cite real evidence you actually observed (exact URLs or file paths). If something is genuinely fine, do not invent problems \u2014 returning few findings is a valid outcome. Severity guide: critical = actively losing rankings/citations today; warning = meaningful lost opportunity; info = polish.";async function v(i,t,n){let s=t.pages.map(e=>({url:e.url,status:e.status,title:e.title,metaDescription:e.metaDescription,canonical:e.canonical,h1s:e.h1s,headings:e.headings.slice(0,20).map(c=>`h${c.level}: ${c.text}`),jsonLdTypes:e.jsonLd.length,og:e.og,wordCount:e.wordCount,textSample:e.textSample.slice(0,1500)})),a="You are a senior SEO/AEO auditor doing the second, human-quality pass of a website audit. A deterministic rule engine already ran (its findings are listed). Now audit the site the way an expert consultant reading the actual pages would.",o=[`Site: ${t.origin}`,`robots.txt:
|
|
4
|
+
${t.robots.raw.slice(0,2e3)||"(none)"}`,`Crawled pages (JSON):
|
|
5
|
+
${JSON.stringify(s,null,1)}`,`Rule findings already reported (do NOT repeat these):
|
|
6
|
+
${p(n)}`,g].join(`
|
|
7
|
+
|
|
8
|
+
`);return u(await i.analyzeJson({system:a,prompt:o,jsonSchema:l}))}async function w(i,t,n,s,a){let o="You are a senior SEO/AEO engineer auditing a codebase, exactly as if the owner asked you to 'review and improve my SEO'. You have read-only access to the repository: explore it yourself with the available tools (read files, search, list). Work stack-agnostically \u2014 this could be WordPress/PHP, Django/Flask, Rails, Next.js, plain HTML, or anything else; first understand how this project renders its pages and where head tags, routing, templates, and static assets live, then audit it.",e=[`Repository: ${t} (detected as: ${n} \u2014 verify this yourself, detection may be wrong).`,"Audit for: metadata handling (titles/descriptions/canonicals per route), structured data, Open Graph, robots/sitemap/llms.txt generation, rendering strategy (is content in the HTML crawlers receive?), image alt handling, semantic HTML, redirects/status handling, i18n/hreflang, and AEO readiness (answerable content structure, AI-crawler access).",`Rule findings already reported (do NOT repeat these):
|
|
9
|
+
${p(s)}`,g,"Cite file paths relative to the repository root as evidence."].join(`
|
|
10
|
+
|
|
11
|
+
`);return u(await i.exploreJson({cwd:t,system:o,prompt:e,jsonSchema:l,onEvent:a}))}export{v as a,w as b};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{z as e}from"zod";var y=e.enum(["claude","openai"]),j=e.object({provider:y,model:e.string().optional(),authMethod:e.enum(["subscription","api-key"]).optional(),anthropicApiKey:e.string().optional(),openaiApiKey:e.string().optional(),crawl:e.object({maxPages:e.number().int().min(1).max(200).default(25),timeoutMs:e.number().int().default(1e4)}).default({maxPages:25,timeoutMs:1e4}),createdAt:e.string().optional(),updatedAt:e.string().optional()}),l=e.enum(["critical","warning","info","pass"]),i=e.enum(["technical","content","structured-data","aeo"]),u=e.object({location:e.string(),snippet:e.string().optional()}),n=e.object({id:e.string(),ruleId:e.string(),category:i,severity:l,title:e.string(),description:e.string(),recommendation:e.string(),evidence:e.array(u).default([]),occurrences:e.number().int().default(1)}),f=e.object({category:i,label:e.string(),score:e.number().min(0).max(100)}),a=e.object({overall:e.number().min(0).max(100),categories:e.array(f)}),b=e.object({priority:e.number().int().min(1),title:e.string(),detail:e.string(),impact:e.enum(["high","medium","low"]),effort:e.enum(["high","medium","low"])}),o=e.object({summary:e.string(),strengths:e.array(e.string()),actionPlan:e.array(b)}),x=e.object({url:e.string(),status:e.number(),title:e.string().optional(),metaDescription:e.string().optional(),h1:e.string().optional(),wordCount:e.number()}),h=e.object({id:e.string(),type:e.literal("domain"),domain:e.string(),createdAt:e.string(),pagesCrawled:e.number(),pages:e.array(x),scorecard:a,findings:e.array(n),llmReport:o.optional()}),v=e.object({keyword:e.string(),intent:e.enum(["informational","navigational","transactional","commercial"]),coverage:e.enum(["strong","partial","missing"]),bestPage:e.string().optional(),aeoCitable:e.boolean(),notes:e.string()}),w=e.object({id:e.string(),type:e.literal("keywords"),auditId:e.string(),domain:e.string(),createdAt:e.string(),keywords:e.array(v),gaps:e.array(e.object({keyword:e.string(),rationale:e.string()})),summary:e.string()}),S=e.object({id:e.string(),type:e.literal("codebase"),path:e.string(),framework:e.string(),createdAt:e.string(),filesScanned:e.number(),scorecard:a,findings:e.array(n),llmReport:o.optional()}),z=e.discriminatedUnion("type",[h,w,S]);var R={type:"object",additionalProperties:!1,required:["summary","strengths","actionPlan"],properties:{summary:{type:"string",description:"3-5 sentence executive summary of the audit for the site owner"},strengths:{type:"array",items:{type:"string"},description:"What the site already does well"},actionPlan:{type:"array",description:"Prioritized, concrete actions. Priority 1 = do first.",items:{type:"object",additionalProperties:!1,required:["priority","title","detail","impact","effort"],properties:{priority:{type:"integer",minimum:1},title:{type:"string"},detail:{type:"string",description:"Specific how-to for THIS site, referencing actual URLs/pages where possible"},impact:{type:"string",enum:["high","medium","low"]},effort:{type:"string",enum:["high","medium","low"]}}}}}};async function I(s,r){let p=r.findings.map(t=>({rule:t.ruleId,category:t.category,severity:t.severity,title:t.title,occurrences:t.occurrences,examples:t.evidence.slice(0,3).map(m=>m.location)})),c="You are a senior SEO and AEO (answer engine optimization) consultant. You are given the deterministic findings of an automated audit plus content samples. Produce a report for the site owner. Be concrete and specific to this site \u2014 never generic advice. Do not invent findings or scores beyond what is given, but you may connect findings into higher-level themes.",d=[`Audit subject: ${r.subject}`,`Scores (computed, do not change): overall ${r.scorecard.overall}/100; `+r.scorecard.categories.map(t=>`${t.label} ${t.score}`).join(", "),`
|
|
3
|
+
Findings (JSON):
|
|
4
|
+
${JSON.stringify(p,null,1)}`,`
|
|
5
|
+
Content samples:
|
|
6
|
+
${r.contentSamples.slice(0,5).map(t=>`--- ${t.location}
|
|
7
|
+
${t.sample.slice(0,1200)}`).join(`
|
|
8
|
+
`)}`,`
|
|
9
|
+
Write the executive summary, list genuine strengths, and produce a prioritized action plan (max 10 items) ordered by impact-per-effort.`].join(`
|
|
10
|
+
`),g=await s.analyzeJson({system:c,prompt:d,jsonSchema:R});return o.parse(g)}export{j as a,h as b,w as c,S as d,z as e,I as f};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{z as e}from"zod";var i=e.enum(["claude","openai"]),f=e.object({provider:i,model:e.string().optional(),authMethod:e.enum(["subscription","api-key"]).optional(),anthropicApiKey:e.string().optional(),openaiApiKey:e.string().optional(),crawl:e.object({maxPages:e.number().int().min(1).max(200).default(25),timeoutMs:e.number().int().default(1e4)}).default({maxPages:25,timeoutMs:1e4}),createdAt:e.string().optional(),updatedAt:e.string().optional()}),a=e.enum(["critical","warning","info","pass"]),t=e.enum(["technical","content","structured-data","aeo"]),p=e.object({location:e.string(),snippet:e.string().optional()}),r=e.object({id:e.string(),ruleId:e.string(),category:t,severity:a,title:e.string(),description:e.string(),recommendation:e.string(),evidence:e.array(p).default([]),occurrences:e.number().int().default(1),source:e.enum(["rule","ai"]).default("rule")}),s=e.object({category:t,label:e.string(),score:e.number().min(0).max(100)}),o=e.object({overall:e.number().min(0).max(100),categories:e.array(s)}),c=e.object({priority:e.number().int().min(1),title:e.string(),detail:e.string(),impact:e.enum(["high","medium","low"]),effort:e.enum(["high","medium","low"])}),n=e.object({summary:e.string(),strengths:e.array(e.string()),actionPlan:e.array(c)}),d=e.object({url:e.string(),status:e.number(),title:e.string().optional(),metaDescription:e.string().optional(),h1:e.string().optional(),wordCount:e.number()}),g=e.object({id:e.string(),type:e.literal("domain"),domain:e.string(),createdAt:e.string(),pagesCrawled:e.number(),pages:e.array(d),scorecard:o,findings:e.array(r),llmReport:n.optional()}),y=e.object({keyword:e.string(),intent:e.enum(["informational","navigational","transactional","commercial"]),coverage:e.enum(["strong","partial","missing"]),bestPage:e.string().optional(),aeoCitable:e.boolean(),notes:e.string()}),m=e.object({id:e.string(),type:e.literal("keywords"),auditId:e.string(),domain:e.string(),createdAt:e.string(),keywords:e.array(y),gaps:e.array(e.object({keyword:e.string(),rationale:e.string()})),summary:e.string()}),l=e.object({id:e.string(),type:e.literal("codebase"),path:e.string(),framework:e.string(),createdAt:e.string(),filesScanned:e.number(),scorecard:o,findings:e.array(r),llmReport:n.optional()}),x=e.discriminatedUnion("type",[g,m,l]);export{f as a,r as b,n as c,g as d,m as e,l as f,x as g};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{a as $e,b as Ce}from"./chunk-LXZFV4HU.js";import{a as B}from"./chunk-7WE77JR3.js";import{a as ve,d as ke,e as Se,f as Ae,g as Pe,h as Re}from"./chunk-5LRP76QX.js";import{Command as On}from"commander";import A from"picocolors";import Ln from"open";import W from"fs";import ae from"path";import dt from"os";var je=ae.join(dt.homedir(),".eternify"),G=ae.join(je,"config.json"),N=ae.join(je,"reports");function V(){W.mkdirSync(N,{recursive:!0})}function T(){try{let n=W.readFileSync(G,"utf8");return ve.parse(JSON.parse(n))}catch{return null}}function U(n){V();let e=new Date().toISOString(),t={...n,createdAt:n.createdAt??e,updatedAt:e};W.writeFileSync(G,JSON.stringify(t,null,2)+`
|
|
3
|
+
`,{mode:384}),W.chmodSync(G,384)}import{spawn as mt}from"child_process";import*as b from"@clack/prompts";import K from"picocolors";import Ie from"fs";import Ee from"path";import Te from"os";import{query as Q}from"@anthropic-ai/claude-agent-sdk";var Y=class{constructor(e){this.config=e}config;id="claude";label="Claude";env(){let e={...process.env};return this.config.anthropicApiKey&&(e.ANTHROPIC_API_KEY=this.config.anthropicApiKey),e}authStatus(){if(this.config.anthropicApiKey||process.env.ANTHROPIC_API_KEY)return{method:"api-key",detail:"Using Anthropic API key"};let e=Ee.join(Te.homedir(),".claude",".credentials.json");return Ie.existsSync(e)||Ie.existsSync(Ee.join(Te.homedir(),".claude.json"))?{method:"subscription",detail:"Reusing your Claude Code login"}:{method:"unknown",detail:"No Claude credentials detected yet"}}async verify(){try{for await(let e of Q({prompt:"Reply with exactly the single word: ok",options:{model:this.config.model,tools:[],maxTurns:1,persistSession:!1,permissionMode:"dontAsk",env:this.env()}}))if(e.type==="result")return e.subtype==="success"?{ok:!0,detail:`Connected (model responded in ${e.duration_ms}ms)`}:{ok:!1,detail:`Claude returned ${e.subtype}`};return{ok:!1,detail:"No result from Claude"}}catch(e){return{ok:!1,detail:e instanceof Error?e.message:String(e)}}}async analyzeJson(e){for await(let t of Q({prompt:`${e.system}
|
|
4
|
+
|
|
5
|
+
${e.prompt}`,options:{model:this.config.model,tools:[],maxTurns:5,persistSession:!1,permissionMode:"dontAsk",env:this.env(),outputFormat:{type:"json_schema",schema:e.jsonSchema}}}))if(t.type==="result"){if(t.subtype==="success"&&t.structured_output!==void 0)return t.structured_output;throw new Error(`Claude analysis failed: ${t.subtype}`)}throw new Error("Claude analysis produced no result")}async exploreJson(e){for await(let t of Q({prompt:`${e.system}
|
|
6
|
+
|
|
7
|
+
${e.prompt}`,options:{model:this.config.model,cwd:e.cwd,allowedTools:["Read","Glob","Grep"],permissionMode:"dontAsk",maxTurns:40,persistSession:!1,env:this.env(),outputFormat:{type:"json_schema",schema:e.jsonSchema}}}))if(t.type==="assistant"){for(let r of t.message.content)if(r.type==="tool_use"){let o=r.input,i=typeof o?.file_path=="string"?` ${o.file_path}`:typeof o?.pattern=="string"?` ${o.pattern}`:"";e.onEvent?.({kind:"tool",message:`${r.name}${i}`})}}else if(t.type==="result"){if(t.subtype==="success"&&t.structured_output!==void 0)return t.structured_output;throw new Error(`Claude exploration failed: ${t.subtype}`)}throw new Error("Claude exploration produced no result")}async runAgent(e){let t=o=>e.onEvent?.(o),r="";for await(let o of Q({prompt:e.prompt,options:{model:this.config.model,cwd:e.cwd,allowedTools:["Read","Edit","Write","Glob","Grep"],permissionMode:"dontAsk",maxTurns:40,persistSession:!1,env:this.env()}}))if(o.type==="assistant"){for(let i of o.message.content)if(i.type==="text"&&i.text.trim())t({kind:"text",message:i.text.trim()});else if(i.type==="tool_use"){let a=i.input,d=typeof a?.file_path=="string"?` ${a.file_path}`:"";t({kind:"tool",message:`${i.name}${d}`})}}else if(o.type==="result")if(o.subtype==="success")r=o.result;else throw new Error(`Claude agent failed: ${o.subtype}`);return r}};import Oe from"fs";import le from"path";import ce from"os";import{createRequire as pt}from"module";import{Codex as ut}from"@openai/codex-sdk";var gt=pt(import.meta.url);function de(){let e={"darwin-arm64":"aarch64-apple-darwin","darwin-x64":"x86_64-apple-darwin","linux-arm64":"aarch64-unknown-linux-musl","linux-x64":"x86_64-unknown-linux-musl","win32-arm64":"aarch64-pc-windows-msvc","win32-x64":"x86_64-pc-windows-msvc"}[`${process.platform}-${process.arch}`];if(!e)return null;try{let t=le.dirname(gt.resolve("@openai/codex-sdk/package.json")),r=le.join(t,"vendor",e,"codex",process.platform==="win32"?"codex.exe":"codex");return Oe.existsSync(r)?r:null}catch{return null}}var X=class{constructor(e){this.config=e}config;id="openai";label="OpenAI";client(){let e=this.config.openaiApiKey??process.env.OPENAI_API_KEY;return new ut(e?{apiKey:e}:{})}authStatus(){return this.config.openaiApiKey||process.env.OPENAI_API_KEY?{method:"api-key",detail:"Using OpenAI API key"}:Oe.existsSync(le.join(ce.homedir(),".codex","auth.json"))?{method:"subscription",detail:"Reusing your Codex / ChatGPT login"}:{method:"unknown",detail:"No OpenAI credentials detected yet"}}threadDefaults(){return{model:this.config.model,skipGitRepoCheck:!0}}async verify(){try{let e=this.client().startThread({...this.threadDefaults(),sandboxMode:"read-only",workingDirectory:ce.tmpdir()}),t=Date.now();return(await e.run("Reply with exactly the single word: ok")).finalResponse?{ok:!0,detail:`Connected (model responded in ${Date.now()-t}ms)`}:{ok:!1,detail:"Empty response from OpenAI"}}catch(e){return{ok:!1,detail:e instanceof Error?e.message:String(e)}}}async analyzeJson(e){let o=(await this.client().startThread({...this.threadDefaults(),sandboxMode:"read-only",workingDirectory:ce.tmpdir()}).run(`${e.system}
|
|
8
|
+
|
|
9
|
+
${e.prompt}`,{outputSchema:e.jsonSchema})).finalResponse?.trim()??"",i=o.replace(/^```(?:json)?\s*/i,"").replace(/```\s*$/,"");try{return JSON.parse(i)}catch{throw new Error(`OpenAI analysis returned non-JSON output: ${o.slice(0,200)}`)}}async exploreJson(e){let o=(await this.client().startThread({...this.threadDefaults(),sandboxMode:"read-only",workingDirectory:e.cwd}).run(`${e.system}
|
|
10
|
+
|
|
11
|
+
${e.prompt}`,{outputSchema:e.jsonSchema})).finalResponse?.trim()??"",i=o.replace(/^```(?:json)?\s*/i,"").replace(/```\s*$/,"");try{return JSON.parse(i)}catch{throw new Error(`OpenAI exploration returned non-JSON output: ${o.slice(0,200)}`)}}async runAgent(e){let t=this.client().startThread({...this.threadDefaults(),sandboxMode:"workspace-write",workingDirectory:e.cwd}),r="",{events:o}=await t.runStreamed(e.prompt);for await(let i of o)if(i.type==="item.completed"){let a=i.item;if(a.type==="agent_message"&&a.text.trim())r=a.text.trim(),e.onEvent?.({kind:"text",message:r});else if(a.type==="command_execution")e.onEvent?.({kind:"tool",message:`$ ${a.command}`});else if(a.type==="file_change"){let d=a.changes.map(g=>`${g.kind} ${g.path}`).join(", ");e.onEvent?.({kind:"tool",message:`edit: ${d}`})}else a.type==="error"&&e.onEvent?.({kind:"text",message:`error: ${a.message}`})}else{if(i.type==="turn.failed")throw new Error(`OpenAI agent failed: ${i.error.message}`);if(i.type==="error")throw new Error(`OpenAI agent stream error: ${i.message}`)}return r}};function $(n){return n.provider==="claude"?new Y(n):new X(n)}var ht=[{value:void 0,label:"Default (recommended)",hint:"whatever your Claude login defaults to"},{value:"claude-sonnet-5",label:"Sonnet 5",hint:"best balance for audits"},{value:"claude-opus-5",label:"Opus 5",hint:"deepest analysis, slower"},{value:"claude-haiku-4-5",label:"Haiku 4.5",hint:"fastest, lighter analysis"}],ft=[{value:void 0,label:"Default (recommended)",hint:"Codex default model"},{value:"gpt-5-codex",label:"GPT-5 Codex",hint:"agentic coding model"}];function O(n="Setup cancelled."){b.cancel(n),process.exit(1)}async function yt(){let n=de();return n?(b.log.step("Opening ChatGPT sign-in in your browser\u2026"),await new Promise(t=>{let r=mt(n,["login"],{stdio:"inherit"});r.on("close",o=>t(o??1)),r.on("error",()=>t(1))})===0):(b.log.error("No bundled codex binary for this platform. Install the Codex CLI and run `codex login`, or use an API key."),!1)}async function pe(n){b.intro(K.bgCyan(K.black(" Eternify SEO/AEO Audit ")));let e=await b.select({message:"Which AI provider do you want to use?",options:[{value:"claude",label:"Claude (Anthropic)",hint:"reuses your Claude Code subscription login"},{value:"openai",label:"OpenAI",hint:"sign in with ChatGPT or use an API key"}],initialValue:n?.provider??"claude"});b.isCancel(e)&&O();let t={provider:e,crawl:n?.crawl??{maxPages:25,timeoutMs:1e4,render:!1},performance:n?.performance??!0,psiApiKey:n?.psiApiKey,dataForSeoLogin:n?.dataForSeoLogin,dataForSeoPassword:n?.dataForSeoPassword,gsc:n?.gsc,anthropicApiKey:n?.anthropicApiKey,openaiApiKey:n?.openaiApiKey,createdAt:n?.createdAt};if(t.provider==="claude"){let d=$(t).authStatus();if(d.method!=="unknown")b.log.success(`Auth detected: ${d.detail}`),t.authMethod=d.method;else{let g=await b.select({message:"How do you want to authenticate with Claude?",options:[{value:"subscription",label:"Claude subscription (recommended)",hint:"requires Claude Code: run `claude` once to log in"},{value:"api-key",label:"Anthropic API key"}]});if(b.isCancel(g)&&O(),t.authMethod=g,g==="api-key"){let y=await b.password({message:"Paste your Anthropic API key (stored in ~/.eternify/config.json, chmod 600):"});b.isCancel(y)&&O(),t.anthropicApiKey=y.trim()}else b.log.info("If verification fails below, run `claude` in another terminal to log in, then re-run this setup.")}}else{let d=$(t).authStatus();if(d.method!=="unknown")b.log.success(`Auth detected: ${d.detail}`),t.authMethod=d.method;else{let g=await b.select({message:"How do you want to authenticate with OpenAI?",options:[{value:"subscription",label:"Sign in with ChatGPT (recommended)",hint:"opens your browser"},{value:"api-key",label:"OpenAI API key"}]});if(b.isCancel(g)&&O(),t.authMethod=g,g==="api-key"){let y=await b.password({message:"Paste your OpenAI API key (stored in ~/.eternify/config.json, chmod 600):"});b.isCancel(y)&&O(),t.openaiApiKey=y.trim()}else await yt()||b.log.warn("ChatGPT sign-in did not complete \u2014 verification below will tell us where we stand.")}}let r=t.provider==="claude"?ht:ft,o=await b.select({message:"Which model should power the analysis?",options:[...r.map(d=>({value:d.value??"__default",label:d.label,hint:d.hint})),{value:"__custom",label:"Other (type a model ID)"}],initialValue:n?.model??"__default"});if(b.isCancel(o)&&O(),o==="__custom"){let d=await b.text({message:"Model ID:"});b.isCancel(d)&&O(),t.model=d.trim()}else o!=="__default"&&(t.model=o);let i=b.spinner();i.start(`Verifying ${t.provider==="claude"?"Claude":"OpenAI"} connection`);let a=await $(t).verify();if(a.ok)i.stop(K.green(`\u2713 ${a.detail}`));else{i.stop(K.red(`\u2717 ${a.detail}`));let d=await b.confirm({message:"Verification failed. Save this configuration anyway?",initialValue:!1});(b.isCancel(d)||!d)&&O("Setup aborted \u2014 nothing saved.")}return U(t),b.outro(`Configuration saved. ${K.dim("Change it anytime with `eternify-seo login` or in the web UI settings.")}`),t}async function Z(){return T()??pe()}import z from"fs";import M from"path";import{fileURLToPath as $n}from"url";import{Hono as Cn}from"hono";import{streamSSE as jn}from"hono/streaming";import{serve as at}from"@hono/node-server";import{z as v}from"zod";import{randomUUID as rn}from"crypto";import*as De from"cheerio";var wt="EternifySEOAudit/0.1 (+https://eternifylabs.com)";function Me(n){return n?n.match(/<([^>]+)>\s*;\s*rel="?canonical"?/i)?.[1]:void 0}async function q(n,e){let t=Date.now();try{let r=await fetch(n,{headers:{"user-agent":wt,accept:"text/html,application/xhtml+xml,application/xml,text/plain,*/*"},redirect:"follow",signal:AbortSignal.timeout(e)}),o=await r.text();return{status:r.status,text:o,contentType:r.headers.get("content-type")??"",finalUrl:r.url,ms:Date.now()-t,xRobotsTag:r.headers.get("x-robots-tag")??void 0,headerCanonical:Me(r.headers.get("link"))}}catch{return null}}function ue(n){let e=n.trim();/^https?:\/\//i.test(e)||(e=`https://${e}`);let t=new URL(e);return`${t.protocol}//${t.host}`}function Fe(n,e){try{let t=new URL(n,e);return/^https?:$/.test(t.protocol)?(t.hash="",t.toString()):null}catch{return null}}function bt(n){let e=[],t=[],r=[],o=!1;for(let i of n.split(/\r?\n/)){let d=i.replace(/#.*$/,"").trim().match(/^([A-Za-z-]+)\s*:\s*(.*)$/);if(!d)continue;let g=d[1].toLowerCase(),y=d[2].trim();g==="sitemap"&&y?e.push(y):g==="user-agent"?(o&&(r=[]),r.push(y),o=!1):g==="disallow"?(o=!0,y==="/"&&t.push(...r)):g==="allow"&&(o=!0)}return{sitemaps:e,blockedAgents:t}}function Le(n,e){let t=[],r=/<loc>\s*([^<]+?)\s*<\/loc>/g,o;for(;(o=r.exec(n))!==null;){let i=Fe(o[1],e);i&&t.push(i)}return t}function xt(n,e){let t=De.load(e),r=new URL(n).origin,o=[];t("h1, h2, h3, h4, h5, h6").each((m,p)=>{o.push({level:Number(p.tagName[1]),text:t(p).text().trim().slice(0,200)})});let i=[];t("img").each((m,p)=>{let h=t(p).attr("src")??t(p).attr("data-src")??"",x=t(p).attr("alt");i.push({src:h.slice(0,300),alt:x===void 0?void 0:x})});let a=new Set,d=0;t("a[href]").each((m,p)=>{let h=Fe(t(p).attr("href"),n);h&&(h.startsWith(r)?a.add(h):d++)});let g=[],y=0;t('script[type="application/ld+json"]').each((m,p)=>{try{g.push(JSON.parse(t(p).text()))}catch{y++}});let u={},c={};t("meta").each((m,p)=>{let h=t(p).attr("property")??t(p).attr("name")??"",x=t(p).attr("content")??"";h.startsWith("og:")&&(u[h.slice(3)]=x),h.startsWith("twitter:")&&(c[h.slice(8)]=x)});let s=[];t('link[rel="alternate"][hreflang]').each((m,p)=>{s.push({lang:t(p).attr("hreflang"),href:t(p).attr("href")??""})}),t("script, style, noscript, svg").remove();let l=t("body").text().replace(/\s+/g," ").trim();return{url:n,title:t("head title").first().text().trim()||void 0,metaDescription:t('head meta[name="description"]').attr("content")?.trim()||void 0,canonical:t('head link[rel="canonical"]').attr("href")?.trim()||void 0,metaRobots:t('head meta[name="robots"]').attr("content")?.trim()||void 0,lang:t("html").attr("lang")?.trim()||void 0,viewport:t('head meta[name="viewport"]').attr("content")?.trim()||void 0,h1s:t("h1").map((m,p)=>t(p).text().trim()).get(),headings:o,images:i,internalLinks:[...a],externalLinks:d,jsonLd:g,jsonLdErrors:y,og:u,twitter:c,hreflang:s,wordCount:l?l.split(" ").length:0,textSample:l.slice(0,2500)}}async function _e(n,e){let t=ue(n),[r,o,i]=await Promise.all([q(`${t}/robots.txt`,e.timeoutMs),q(`${t}/sitemap.xml`,e.timeoutMs),q(`${t}/llms.txt`,e.timeoutMs)]),a=r&&r.status===200?r.text:"",d=bt(a),g={exists:!!r&&r.status===200,raw:a.slice(0,1e4),sitemaps:d.sitemaps,blockedAgents:d.blockedAgents},y=[],u=!1,c=[...o&&o.status===200&&o.text.includes("<")?[o.text]:[]];if(c.length===0&&g.sitemaps.length>0){let w=await q(g.sitemaps[0],e.timeoutMs);w&&w.status===200&&c.push(w.text)}for(let w of c){u=!0;let k=Le(w,t);if(w.includes("<sitemapindex")){let f=k.slice(0,3);k=[];for(let R of f){let E=await q(R,e.timeoutMs);E&&E.status===200&&k.push(...Le(E.text,t))}}y=k.filter(f=>f.startsWith(t))}let s=null;if(e.render){let{createRenderer:w}=await import("./render-CCORCUAB.js");s=await w()}let l=async w=>{if(s){let k=Date.now(),f=await s.render(w,e.timeoutMs);if(f)return{status:f.status,text:f.html,contentType:f.headers["content-type"]??"text/html",finalUrl:f.finalUrl,ms:Date.now()-k,xRobotsTag:f.headers["x-robots-tag"],headerCanonical:Me(f.headers.link??null)}}return q(w,e.timeoutMs)},m=[t,...y],p=new Set,h=[],x=[];try{for(;m.length>0&&h.length<e.maxPages;){let w=[];for(;w.length<4&&m.length>0&&h.length+w.length<e.maxPages;){let k=m.shift(),f=k.replace(/\/$/,"");p.has(f)||(p.add(f),w.push(k))}if(w.length===0)break;await Promise.all(w.map(async k=>{let f=await l(k);if(!f){x.push({url:k,reason:"fetch failed or timed out"});return}if(!f.contentType.includes("html")){x.push({url:k,reason:`not HTML (${f.contentType||"unknown"})`});return}let R=xt(f.finalUrl,f.text);h.push({...R,url:k,redirectedTo:f.finalUrl!==k?f.finalUrl:void 0,status:f.status,contentType:f.contentType,fetchTimeMs:f.ms,xRobotsTag:f.xRobotsTag,headerCanonical:f.headerCanonical,rendered:!!s});for(let E of R.internalLinks)p.has(E.replace(/\/$/,""))||m.push(E);e.onProgress?.(h.length,e.maxPages,k)}))}}finally{await s?.close()}return{origin:t,robots:g,sitemapExists:u,sitemapUrls:y,llmsTxtExists:!!i&&i.status===200&&!i.contentType.includes("html"),pages:h,failedUrls:x}}var vt=0;function S(n,e,t,r,o,i,a,d=a.length||1){return{id:`f-${++vt}-${n}`,ruleId:n,category:e,severity:t,title:r,description:o,recommendation:i,evidence:a.slice(0,8),occurrences:d,source:"rule"}}var J=n=>n.pages.filter(e=>e.status>=200&&e.status<300);function P(n,e){return J(n).filter(e)}var kt=["GPTBot","ClaudeBot","Claude-Web","PerplexityBot","Google-Extended","OAI-SearchBot","anthropic-ai"],St=n=>{let e=n.pages.filter(t=>t.status>=400);return e.length===0?null:S("http-errors","technical","critical","Pages returning HTTP errors",`${e.length} crawled page(s) returned a 4xx/5xx status. Broken pages waste crawl budget and lose rankings.`,"Fix or redirect the broken URLs, and remove them from the sitemap and internal links.",e.map(t=>({location:t.url,snippet:`HTTP ${t.status}`})),e.length)},At=n=>{let e=P(n,t=>!t.title);return e.length===0?null:S("missing-title","technical","critical","Pages without a <title> tag",`${e.length} page(s) have no title tag. The title is the single strongest on-page ranking signal and becomes the clickable headline in search results.`,"Add a unique, descriptive <title> (15\u201360 characters) to every page.",e.map(t=>({location:t.url})),e.length)},Pt=n=>{let e=P(n,t=>!!t.title&&(t.title.length<15||t.title.length>60));return e.length===0?null:S("title-length","technical","warning","Titles too short or too long",`${e.length} page(s) have titles outside the 15\u201360 character range; long ones get truncated in results, short ones waste the slot.`,"Rewrite titles to 15\u201360 characters, front-loading the primary topic.",e.map(t=>({location:t.url,snippet:`"${t.title}" (${t.title.length} chars)`})),e.length)},Rt=n=>{let e=new Map;for(let r of P(n,o=>!!o.title)){let o=r.title.toLowerCase();e.set(o,[...e.get(o)??[],r])}let t=[...e.values()].filter(r=>r.length>1);return t.length===0?null:S("duplicate-titles","technical","warning","Duplicate page titles",`${t.length} title(s) are shared by multiple pages, making it hard for search engines to pick the right page for a query.`,"Give every page a unique title describing its specific content.",t.flatMap(r=>r.map(o=>({location:o.url,snippet:o.title}))),t.reduce((r,o)=>r+o.length,0))},$t=n=>{let e=P(n,t=>!t.metaDescription);return e.length===0?null:S("missing-meta-description","technical","warning","Pages without a meta description",`${e.length} page(s) have no meta description; search engines will improvise a snippet, and answer engines lose a concise summary of the page.`,"Add a 50\u2013160 character meta description that summarizes the page and includes its primary topic.",e.map(t=>({location:t.url})),e.length)},Ct=n=>{let e=P(n,t=>!!t.metaDescription&&(t.metaDescription.length<50||t.metaDescription.length>160));return e.length===0?null:S("meta-description-length","technical","info","Meta descriptions outside 50\u2013160 characters",`${e.length} page(s) have meta descriptions that are too short to be useful or long enough to be truncated.`,"Rewrite them to 50\u2013160 characters.",e.map(t=>({location:t.url,snippet:`${t.metaDescription.length} chars`})),e.length)},jt=n=>{let e=P(n,t=>!t.canonical);return e.length===0?null:S("missing-canonical","technical","warning","Pages without a canonical URL",`${e.length} page(s) declare no rel=canonical, risking duplicate-content dilution (e.g. trailing slash, query-string, and www variants competing).`,'Add <link rel="canonical" href="..."> pointing to the preferred URL of each page.',e.map(t=>({location:t.url})),e.length)},It=n=>{let e=P(n,t=>!t.viewport);return e.length===0?null:S("missing-viewport","technical","critical","Pages without a viewport meta tag",`${e.length} page(s) lack a viewport tag; Google indexes mobile-first, and pages that render poorly on mobile rank poorly.`,'Add <meta name="viewport" content="width=device-width, initial-scale=1"> to every page.',e.map(t=>({location:t.url})),e.length)},Et=n=>{let e=P(n,t=>!t.lang);return e.length===0?null:S("missing-lang","technical","info","Pages without an html lang attribute",`${e.length} page(s) don't declare a language, which hurts accessibility and language targeting.`,'Set <html lang="en"> (or the correct language) on every page.',e.map(t=>({location:t.url})),e.length)},Tt=n=>{let e=P(n,t=>/noindex/i.test(t.metaRobots??"")||/noindex/i.test(t.xRobotsTag??""));return e.length===0?null:S("noindex-pages","technical","warning","Crawlable pages marked noindex",`${e.length} page(s) reachable from internal links or the sitemap are marked noindex (via meta robots or the X-Robots-Tag header) and will never rank.`,"Confirm these pages should be excluded; if not, remove the noindex directive. If they should, remove them from the sitemap.",e.map(t=>({location:t.url,snippet:t.metaRobots??`X-Robots-Tag: ${t.xRobotsTag}`})),e.length)},Ot=n=>{let e=P(n,t=>!!t.headerCanonical&&!!t.canonical&&t.headerCanonical.replace(/\/$/,"")!==t.canonical.replace(/\/$/,""));return e.length===0?null:S("canonical-conflict","technical","warning","HTTP Link header canonical contradicts the HTML canonical",`${e.length} page(s) send a rel=canonical Link header that differs from the <link rel="canonical"> in the HTML. Conflicting signals make search engines pick one arbitrarily.`,"Serve a single canonical signal \u2014 remove one of the two or make them identical.",e.map(t=>({location:t.url,snippet:`header: ${t.headerCanonical} vs html: ${t.canonical}`})),e.length)},Lt=n=>n.robots.exists?null:S("robots-missing","technical","warning","No robots.txt","The site has no robots.txt, so crawlers get no crawling guidance and no sitemap pointer.","Add a robots.txt that allows crawling and declares the sitemap URL.",[{location:`${n.origin}/robots.txt`,snippet:"404 / unreachable"}]),Dt=n=>n.sitemapExists?null:S("sitemap-missing","technical","warning","No XML sitemap","No sitemap.xml was found at the default location or declared in robots.txt; new and deep pages get discovered more slowly.","Generate a sitemap.xml, keep it updated automatically, and reference it from robots.txt.",[{location:`${n.origin}/sitemap.xml`,snippet:"not found"}]),Mt=n=>{let e=P(n,t=>t.fetchTimeMs>2500);return e.length===0?null:S("slow-pages","technical","info","Slow server responses",`${e.length} page(s) took over 2.5s to deliver HTML. Slow responses hurt Core Web Vitals and crawl rate.`,"Investigate server/CDN caching for these routes; aim for HTML delivery under 800ms.",e.map(t=>({location:t.url,snippet:`${t.fetchTimeMs}ms`})),e.length)},Ft=n=>{let e=P(n,t=>t.h1s.length===0);return e.length===0?null:S("missing-h1","content","critical","Pages without an H1",`${e.length} page(s) have no H1 heading. Both search and answer engines use the H1 to understand what a page is about.`,"Add exactly one descriptive H1 to every page.",e.map(t=>({location:t.url})),e.length)},_t=n=>{let e=P(n,t=>t.h1s.length>1);return e.length===0?null:S("multiple-h1","content","warning","Pages with multiple H1s",`${e.length} page(s) have more than one H1, diluting the page's topical focus.`,"Keep one H1 per page and demote the rest to H2.",e.map(t=>({location:t.url,snippet:t.h1s.slice(0,3).join(" | ")})),e.length)},Nt=n=>{let e=P(n,t=>{let r=0;for(let o of t.headings){if(r>0&&o.level>r+1)return!0;r=o.level}return!1});return e.length===0?null:S("heading-hierarchy","content","info","Skipped heading levels",`${e.length} page(s) skip heading levels (e.g. H1 \u2192 H3), which weakens the document outline machines rely on.`,"Nest headings sequentially: H1 \u2192 H2 \u2192 H3.",e.map(t=>({location:t.url})),e.length)},Ut=n=>{let e=P(n,t=>t.wordCount>0&&t.wordCount<150);return e.length===0?null:S("thin-content","content","warning","Thin content pages",`${e.length} page(s) have under 150 words of visible text. Thin pages rarely rank and give answer engines nothing to cite. (Note: for client-rendered apps the crawler only sees server HTML.)`,"Expand these pages with substantive content, or noindex genuinely utility-only pages. If your site is client-rendered, ensure key content is server-rendered or prerendered.",e.map(t=>({location:t.url,snippet:`${t.wordCount} words`})),e.length)},qt=n=>{let e=J(n).map(r=>({p:r,missing:r.images.filter(o=>o.alt===void 0||o.alt==="")})).filter(r=>r.missing.length>0);if(e.length===0)return null;let t=e.reduce((r,o)=>r+o.missing.length,0);return S("images-missing-alt","content","warning","Images without alt text",`${t} image(s) across ${e.length} page(s) have missing or empty alt attributes, losing image-search traffic and accessibility.`,'Add descriptive alt text to meaningful images; use alt="" only for purely decorative ones.',e.map(r=>({location:r.p.url,snippet:`${r.missing.length} image(s), e.g. ${r.missing[0].src||"(inline)"}`})),t)},Jt=n=>{if(!n.sitemapExists||n.sitemapUrls.length===0)return null;let e=new Set;for(let r of n.pages)for(let o of r.internalLinks)e.add(o.replace(/\/$/,""));let t=n.sitemapUrls.filter(r=>{let o=r.replace(/\/$/,"");return!e.has(o)&&o!==n.origin});return t.length===0?null:S("orphan-pages","content","info","Sitemap pages with no internal links",`${t.length} sitemap URL(s) were never linked from any crawled page. Pages without internal links get little authority and are treated as unimportant.`,"Link to these pages from related content or navigation.",t.slice(0,8).map(r=>({location:r})),t.length)};function Ne(n){let e=[],t=r=>{if(Array.isArray(r))return r.forEach(t);if(r&&typeof r=="object"){let o=r["@type"];typeof o=="string"&&e.push(o),Array.isArray(o)&&e.push(...o.filter(a=>typeof a=="string"));let i=r["@graph"];i&&t(i)}};return n.jsonLd.forEach(t),e}var Ht=n=>{let e=P(n,t=>t.jsonLd.length===0);return e.length===0?null:S("no-structured-data","structured-data","warning","Pages without JSON-LD structured data",`${e.length} page(s) carry no schema.org JSON-LD. Structured data powers rich results and is a primary way answer engines identify entities and facts.`,"Add JSON-LD appropriate to each page type (Organization, WebSite, Article, Product, FAQPage\u2026).",e.map(t=>({location:t.url})),e.length)},Gt=n=>{let e=P(n,t=>t.jsonLdErrors>0);return e.length===0?null:S("jsonld-parse-errors","structured-data","warning","Invalid JSON-LD blocks",`${e.length} page(s) contain JSON-LD that fails to parse; invalid blocks are silently ignored by search engines.`,"Fix the JSON syntax (validate with Google's Rich Results Test).",e.map(t=>({location:t.url,snippet:`${t.jsonLdErrors} invalid block(s)`})),e.length)},Kt=n=>J(n).some(t=>Ne(t).some(r=>["Organization","WebSite","Person","LocalBusiness"].includes(r)))?null:S("missing-entity-schema","structured-data","warning","No Organization/WebSite schema anywhere","No crawled page declares who is behind the site (Organization, WebSite, or Person schema). Entity clarity is foundational for both knowledge panels and AI answer attribution.","Add Organization + WebSite JSON-LD (name, url, logo, sameAs social links) to the home page.",[{location:n.origin}]),zt=n=>{let e=P(n,t=>!t.og.title&&!t.og.image);return e.length===0?null:S("missing-open-graph","structured-data","warning","Pages without Open Graph tags",`${e.length} page(s) have no og:title/og:image, so shares on social and chat apps render as bare links.`,"Add og:title, og:description, og:image and og:url to every indexable page.",e.map(t=>({location:t.url})),e.length)},Bt=n=>{let e=P(n,t=>Object.keys(t.og).length>0&&!t.twitter.card);return e.length===0?null:S("missing-twitter-card","structured-data","info","Pages without a twitter:card tag",`${e.length} page(s) have Open Graph tags but no twitter:card, so X/Twitter may not render a large preview.`,'Add <meta name="twitter:card" content="summary_large_image">.',e.map(t=>({location:t.url})),e.length)},Wt=n=>{let e=n.robots.blockedAgents.filter(o=>kt.some(i=>o.toLowerCase()===i.toLowerCase())),t=n.robots.blockedAgents.includes("*");if(e.length===0&&!t)return null;let r=t?["* (all crawlers)"]:e;return S("ai-bots-blocked","aeo","critical","AI crawlers blocked in robots.txt",`robots.txt disallows ${r.join(", ")} from the whole site. Blocked AI crawlers (GPTBot, ClaudeBot, PerplexityBot\u2026) mean your content can never be cited in AI answers.`,"Unless exclusion is deliberate policy, allow AI answer-engine crawlers in robots.txt.",[{location:`${n.origin}/robots.txt`,snippet:r.map(o=>`User-agent: ${o} / Disallow: /`).join("; ")}])},Vt=n=>n.llmsTxtExists?null:S("llms-txt-missing","aeo","info","No llms.txt","The emerging llms.txt convention gives AI systems a curated, markdown map of your most important content. Early adopters get disproportionate representation in AI answers.","Publish /llms.txt with a short site description and links to key pages in markdown.",[{location:`${n.origin}/llms.txt`,snippet:"not found"}]),Qt=n=>J(n).some(t=>Ne(t).some(r=>["FAQPage","HowTo","QAPage","Article","BlogPosting"].includes(r)))?null:S("no-answer-schema","aeo","warning","No FAQ/HowTo/Article schema on any page","No crawled page uses answer-oriented schema (FAQPage, HowTo, QAPage, Article). These formats map directly onto the question\u2192answer shape AI assistants extract.","Mark up FAQ sections with FAQPage schema and articles with Article schema.",[{location:n.origin}]),Yt=n=>{let e=J(n);return e.length===0||e.filter(r=>r.headings.some(o=>/\?\s*$/.test(o.text)||/^(what|how|why|when|where|who|which|can|should|is|are|does|do)\b/i.test(o.text))).length>0?null:S("no-question-headings","aeo","info","No question-style headings found","None of the crawled pages use question-phrased headings. Answer engines strongly prefer content whose structure mirrors the user's question.",'Add sections with headings like "How does X work?" followed by a direct 2\u20133 sentence answer.',[{location:n.origin}])},Xt=n=>{let e=J(n);if(e.length===0)return null;let t=e.filter(r=>r.wordCount<40&&r.title);return t.length<Math.max(2,e.length*.5)?null:S("client-rendered-content","aeo","critical","Content appears to be client-side rendered",`${t.length} of ${e.length} pages deliver almost no text in their HTML. Many AI crawlers do not execute JavaScript, so they see empty pages.`,"Server-render or prerender your pages (SSR/SSG) so full content is present in the initial HTML.",t.slice(0,8).map(r=>({location:r.url,snippet:`${r.wordCount} words in HTML`})),t.length)},Zt=[St,At,Pt,Rt,$t,Ct,jt,It,Et,Tt,Ot,Lt,Dt,Mt,Ft,_t,Nt,Ut,qt,Jt,Ht,Gt,Kt,zt,Bt,Wt,Vt,Qt,Yt,Xt];function Ue(n){let e=[];for(let t of Zt){let r=t(n);r&&e.push(r)}return e}var qe={technical:"Technical SEO",content:"On-Page Content","structured-data":"Structured Data",aeo:"AEO Readiness",performance:"Performance"},Je={technical:.25,content:.25,"structured-data":.15,aeo:.2,performance:.15},en={critical:25,warning:9,info:3,pass:0};function tn(n){let e=en[n.severity]??0,t=Math.min(1.8,1+Math.log10(Math.max(1,n.occurrences))*.5);return e*t}function ee(n,e={}){let t=a=>a!=="performance"||e.performanceMeasured||n.some(d=>d.category==="performance"),r=Object.keys(qe).filter(t).map(a=>{let d=n.filter(g=>g.category===a).reduce((g,y)=>g+tn(y),0);return{category:a,label:qe[a],score:Math.max(0,Math.round(100-d))}}),o=r.reduce((a,d)=>a+Je[d.category],0);return{overall:Math.round(r.reduce((a,d)=>a+d.score*Je[d.category],0)/o),categories:r}}async function He(n,e,t=6e4){let r=new URL("https://www.googleapis.com/pagespeedonline/v5/runPagespeed");r.searchParams.set("url",n),r.searchParams.set("strategy","mobile"),r.searchParams.set("category","performance"),e&&r.searchParams.set("key",e);let o=await fetch(r,{signal:AbortSignal.timeout(t)});if(o.status===429)throw new Error(e?"PageSpeed API quota exceeded for your key":"PageSpeed keyless quota exhausted \u2014 add a free Google API key in Settings to enable performance measurement");if(!o.ok){let c=await o.text().catch(()=>"");throw new Error(`PageSpeed API ${o.status}: ${c.slice(0,160)}`)}let i=await o.json(),a=i.loadingExperience?.metrics??{},d=a.LARGEST_CONTENTFUL_PAINT_MS?.percentile,g=a.INTERACTION_TO_NEXT_PAINT?.percentile,y=a.CUMULATIVE_LAYOUT_SHIFT_SCORE?.percentile,u=i.lighthouseResult?.categories?.performance?.score;return{url:n,lighthouseScore:u!==void 0?Math.round(u*100):void 0,lcpMs:d,inpMs:g,cls:y!==void 0?y/100:void 0,fieldDataAvailable:Object.keys(a).length>0}}var nn=0;function te(n,e,t,r,o,i,a){return{id:`pf-${++nn}-${n}`,ruleId:n,category:"performance",severity:e,title:t,description:r,recommendation:o,evidence:[{location:i,snippet:a}],occurrences:1,source:"rule"}}function Ge(n){let e=[];return n.lighthouseScore!==void 0&&n.lighthouseScore<80&&e.push(te("perf-lighthouse",n.lighthouseScore<50?"critical":"warning",`Lighthouse performance score ${n.lighthouseScore}/100`,"Lab performance is below Google's 'good' band. Slow pages lose rankings, conversions, and crawl budget.","Run PageSpeed Insights for the detailed opportunity list (render-blocking resources, image sizing, JS payload) and fix the top items.",n.url,`Lighthouse ${n.lighthouseScore}/100 (mobile)`)),n.lcpMs!==void 0&&n.lcpMs>2500&&e.push(te("perf-lcp",n.lcpMs>4e3?"critical":"warning","Largest Contentful Paint is too slow (field data)",`Real users experience LCP of ${(n.lcpMs/1e3).toFixed(1)}s; Google's 'good' threshold is 2.5s. LCP is a confirmed ranking signal.`,"Optimize the hero image/text delivery: preload the LCP resource, use a CDN, compress images, and cut render-blocking CSS/JS.",n.url,`LCP p75 = ${(n.lcpMs/1e3).toFixed(1)}s`)),n.inpMs!==void 0&&n.inpMs>200&&e.push(te("perf-inp",n.inpMs>500?"critical":"warning","Interaction to Next Paint is too slow (field data)",`Real-user INP is ${n.inpMs}ms; 'good' is under 200ms. Sluggish interactions hurt the page experience signal.`,"Break up long JavaScript tasks, defer non-critical scripts, and minimize main-thread work on interaction handlers.",n.url,`INP p75 = ${n.inpMs}ms`)),n.cls!==void 0&&n.cls>.1&&e.push(te("perf-cls",n.cls>.25?"critical":"warning","Cumulative Layout Shift is too high (field data)",`Real-user CLS is ${n.cls.toFixed(2)}; 'good' is under 0.10. Shifting layouts frustrate users and hurt rankings.`,"Set explicit width/height on images and embeds, reserve space for ads/banners, and avoid injecting content above existing content.",n.url,`CLS p75 = ${n.cls.toFixed(2)}`)),e}async function ne(n,e,t){let r=ue(e),o=t.onProgress??(()=>{});o(`Crawling ${r}${t.render?" (JS rendering)":""}`,.05);let i=await _e(r,{maxPages:t.maxPages,timeoutMs:t.timeoutMs,render:t.render,onProgress:(u,c,s)=>o(`Crawled ${u}/${c}: ${s}`,.05+.45*(u/c))});if(i.pages.length===0)throw new Error(`Could not fetch any page from ${r} \u2014 is the domain reachable?`);o(`Round 1: rule engine over ${i.pages.length} pages`,.5);let a=Ue(i),d=!1;if(t.measurePerformance){o("Measuring Core Web Vitals (PageSpeed Insights)",.55);try{let u=await He(i.origin,t.psiApiKey);a=[...a,...Ge(u)],d=u.lighthouseScore!==void 0||u.fieldDataAvailable,o(`Performance: Lighthouse ${u.lighthouseScore??"n/a"}${u.fieldDataAvailable?", CrUX field data available":""}`,.6)}catch(u){o(`PageSpeed measurement failed (${u instanceof Error?u.message:u}); skipping performance category`,.6)}}if(n&&!t.skipLlm){o("Round 2: AI auditor reviewing the crawl",.65);try{let u=await $e(n,i,a);a=[...a,...u],o(`AI auditor added ${u.length} finding(s)`,.8)}catch(u){o(`AI audit round failed (${u instanceof Error?u.message:u}); continuing with rule findings`,.8)}}let g=ee(a,{performanceMeasured:d}),y=ke.parse({id:rn(),type:"domain",domain:r,createdAt:new Date().toISOString(),pagesCrawled:i.pages.length,pages:i.pages.map(u=>({url:u.url,status:u.status,title:u.title,metaDescription:u.metaDescription,h1:u.h1s[0],wordCount:u.wordCount})),scorecard:g,findings:a});if(n&&!t.skipLlm){o("Writing the report",.85);try{y.llmReport=await B(n,{subject:r,scorecard:g,findings:a,contentSamples:i.pages.slice(0,5).map(u=>({location:u.url,sample:u.textSample}))})}catch(u){o(`AI report failed (${u instanceof Error?u.message:u}); returning rule-based report`,.95)}}return o("Done",1),y}import{randomUUID as on}from"crypto";var sn={type:"object",additionalProperties:!1,required:["keywords","gaps","summary"],properties:{summary:{type:"string",description:"3-4 sentence overview of keyword coverage and the biggest opportunity"},keywords:{type:"array",items:{type:"object",additionalProperties:!1,required:["keyword","intent","coverage","aeoCitable","notes"],properties:{keyword:{type:"string"},intent:{type:"string",enum:["informational","navigational","transactional","commercial"]},coverage:{type:"string",enum:["strong","partial","missing"],description:"How well the crawled site currently covers this keyword"},bestPage:{type:"string",description:"URL of the best existing page for this keyword, omit if none"},aeoCitable:{type:"boolean",description:"Would an AI answer engine plausibly cite this site for this query today?"},notes:{type:"string",description:"Specific advice for this keyword on this site"}}}},gaps:{type:"array",description:"High-value related keywords the site does NOT target but should",items:{type:"object",additionalProperties:!1,required:["keyword","rationale"],properties:{keyword:{type:"string"},rationale:{type:"string"}}}}}};async function Ke(n,e,t,r={}){let o="You are a senior SEO/AEO keyword strategist. You are given a crawled snapshot of a website (page titles, descriptions, headings, word counts) and a list of target keywords. Analyze how well the site covers each keyword for both classic search and AI answer engines. Base every judgment strictly on the provided pages; if the site has no relevant page, say coverage is missing.",i=e.pages.map(u=>({url:u.url,title:u.title,description:u.metaDescription,h1:u.h1,words:u.wordCount})),a=r.gscQueries&&r.gscQueries.length>0?`REAL Google Search Console data for this site (last 90 days, top queries):
|
|
12
|
+
${r.gscQueries.slice(0,60).map(u=>`- "${u.query}": ${u.impressions} impressions, ${u.clicks} clicks, avg position ${u.position.toFixed(1)}`).join(`
|
|
13
|
+
`)}
|
|
14
|
+
Use this real data: queries with high impressions but poor position/CTR are prime opportunities; include the best of them in your analysis even if not in the target list.`:null,d=[`Site: ${e.domain}`,`Crawled pages (JSON):
|
|
15
|
+
${JSON.stringify(i,null,1)}`,a,t.length>0?`Target keywords: ${t.join(", ")}`:"No target keywords were provided: infer the 8-12 keywords this site is implicitly targeting (prioritize real Search Console queries when given) and analyze those.","For each keyword: classify intent, rate coverage, name the best existing page (exact URL from the list, or omit), judge AEO citability, and give one specific note. Then list up to 8 gap keywords with rationale."].filter(Boolean).join(`
|
|
16
|
+
|
|
17
|
+
`),g=await n.analyzeJson({system:o,prompt:d,jsonSchema:sn}),y=Se.parse({id:on(),type:"keywords",auditId:e.id,domain:e.domain,createdAt:new Date().toISOString(),...g});if(r.volumes)for(let u of y.keywords){let c=r.volumes.get(u.keyword.toLowerCase());c!==void 0&&(u.searchVolume=c)}return y}import j from"fs";import I from"path";import{randomUUID as an}from"crypto";var cn=new Set(["node_modules",".git",".next","dist","build","out",".vercel",".turbo","coverage",".cache","vendor","venv",".venv","__pycache__","wp-admin","wp-includes","storage","tmp","log"]),ln=new Set([".html",".htm",".jsx",".tsx",".js",".ts",".mjs",".astro",".vue",".svelte",".md",".mdx",".php",".twig",".liquid",".erb",".ejs",".hbs",".py",".rb"]),ze=500;function dn(n,e){let t=(...i)=>i.some(a=>j.existsSync(I.join(e,a))),r=(i,a)=>{try{return j.readFileSync(I.join(e,i),"utf8").toLowerCase().includes(a)}catch{return!1}};if(t("wp-config.php","wp-content")||r("style.css","theme name:"))return"WordPress";if(t("artisan"))return"Laravel";if(t("manage.py"))return"Django";if(r("requirements.txt","flask")||r("pyproject.toml","flask"))return"Flask";if(r("requirements.txt","fastapi")||r("pyproject.toml","fastapi"))return"FastAPI";if(r("Gemfile","rails"))return"Ruby on Rails";if(t("config.rb")&&r("Gemfile","middleman"))return"Middleman";if(t("_config.yml"))return"Jekyll";if(t("config.toml","hugo.toml")&&t("content"))return"Hugo";let o={...n?.dependencies??{},...n?.devDependencies??{}};return o.next?"Next.js":o.astro?"Astro":o.nuxt?"Nuxt":o["@sveltejs/kit"]?"SvelteKit":o.gatsby?"Gatsby":o["@remix-run/react"]||o["react-router"]?"Remix/React Router":o.express||o.fastify||o.hono||o.koa?"Node.js server":o.react&&o.vite?"React (Vite SPA)":o.react?"React":o.vue?"Vue":t("composer.json")||t("index.php")?"PHP":j.existsSync(I.join(e,"index.html"))?"Static HTML":"Unknown"}function re(n){let e=I.resolve(n);if(!j.existsSync(e)||!j.statSync(e).isDirectory())throw new Error(`Not a directory: ${e}`);let t=[],r=i=>{if(t.length>=ze)return;let a;try{a=j.readdirSync(i,{withFileTypes:!0})}catch{return}for(let d of a){if(t.length>=ze)return;if(d.name.startsWith(".")&&d.name!==".well-known")continue;let g=I.join(i,d.name);if(d.isDirectory())cn.has(d.name)||r(g);else{let y=I.extname(d.name).toLowerCase();if(!ln.has(y))continue;try{if(j.statSync(g).size>4e5)continue;t.push({rel:I.relative(e,g),abs:g,ext:y,content:j.readFileSync(g,"utf8")})}catch{}}}};r(e);let o=null;try{o=JSON.parse(j.readFileSync(I.join(e,"package.json"),"utf8"))}catch{}return{root:e,framework:dn(o,e),files:t,packageJson:o,hasPublicFile:i=>j.existsSync(I.join(e,"public",i))||j.existsSync(I.join(e,"static",i))||j.existsSync(I.join(e,i))}}var pn=0;function C(n,e,t,r,o,i,a,d=a.length||1){return{id:`cf-${++pn}-${n}`,ruleId:n,category:e,severity:t,title:r,description:o,recommendation:i,evidence:a.slice(0,8),occurrences:d,source:"rule"}}function un(n){return n.files.some(e=>/^(src\/)?app\/layout\.(tsx|jsx|js|ts)$/.test(e.rel))}function oe(n){let e=[],t=n.framework==="Next.js",r=t&&un(n),o=["WordPress","Ruby on Rails","Django","Laravel"].includes(n.framework),i=n.files.filter(s=>[".jsx",".tsx",".astro",".vue",".svelte",".html",".htm",".php",".twig",".liquid",".erb",".ejs",".hbs"].includes(s.ext)),a=n.files.map(s=>s.content).join(`
|
|
18
|
+
`);if(!(n.hasPublicFile("robots.txt")||t&&n.files.some(s=>/^(src\/)?app\/robots\.(ts|js|tsx)$/.test(s.rel)))&&!o&&e.push(C("cb-robots-missing","technical","warning","No robots.txt in the project","The project ships no robots.txt (static or generated), so crawlers get no guidance and no sitemap pointer.",t?"Add an app/robots.ts using Next.js Metadata routes (or public/robots.txt).":"Add a robots.txt to the public/static directory.",[{location:"public/robots.txt",snippet:"not found"}])),!(n.hasPublicFile("sitemap.xml")||t&&n.files.some(s=>/^(src\/)?app\/sitemap\.(ts|js|tsx)$/.test(s.rel))||JSON.stringify(n.packageJson?.dependencies??{}).includes("next-sitemap"))&&!o&&e.push(C("cb-sitemap-missing","technical","warning","No sitemap generation","No sitemap.xml or sitemap generation code was found; new pages will be discovered slowly.",t?"Add an app/sitemap.ts Metadata route that enumerates your routes.":"Generate a sitemap.xml at build time and ship it in public/.",[{location:"public/sitemap.xml",snippet:"not found"}])),n.hasPublicFile("llms.txt")||e.push(C("cb-llms-txt","aeo","info","No llms.txt","The project doesn't publish an llms.txt \u2014 an emerging convention giving AI systems a curated map of your content.","Add public/llms.txt with a short markdown description of the site and links to key pages.",[{location:"public/llms.txt",snippet:"not found"}])),r){let s=n.files.filter(m=>/^(src\/)?app\/.*(layout|page)\.(tsx|jsx|js|ts)$/.test(m.rel));if(s.filter(m=>/export\s+(const|async function|function)\s+(metadata|generateMetadata)\b/.test(m.content)).length===0)e.push(C("cb-no-metadata-export","technical","critical","No Metadata API usage in the App Router","No layout or page exports `metadata`/`generateMetadata`, so pages ship without titles and descriptions.","Export `metadata` from app/layout.tsx (site defaults) and per-page metadata (or generateMetadata) for every route.",[{location:"app/layout.tsx"}]));else{let p=s.filter(h=>/page\.(tsx|jsx|js|ts)$/.test(h.rel)).filter(h=>!/export\s+(const|async function|function)\s+(metadata|generateMetadata)\b/.test(h.content));p.length>0&&e.push(C("cb-pages-without-metadata","technical","warning","Routes without their own metadata",`${p.length} page(s) rely on inherited metadata, so several routes share the same title/description.`,"Export route-specific `metadata` (title, description, openGraph) from each page.tsx.",p.map(h=>({location:h.rel})),p.length))}}else{let s=n.files.filter(h=>[".html",".htm"].includes(h.ext)),l=s.filter(h=>!/<title[\s>]/i.test(h.content));l.length>0&&e.push(C("cb-html-no-title","technical","critical","HTML files without <title>",`${l.length} HTML file(s) have no title tag.`,"Add a unique, descriptive <title> to every HTML document.",l.map(h=>({location:h.rel})),l.length));let m=s.filter(h=>!/name=["']description["']/i.test(h.content));m.length>0&&e.push(C("cb-html-no-description","technical","warning","HTML files without a meta description",`${m.length} HTML file(s) lack a meta description.`,'Add <meta name="description" content="..."> to every HTML document.',m.map(h=>({location:h.rel})),m.length));let p=s.filter(h=>!/name=["']viewport["']/i.test(h.content));p.length>0&&e.push(C("cb-html-no-viewport","technical","critical","HTML files without a viewport meta tag",`${p.length} HTML file(s) lack the responsive viewport tag; mobile-first indexing will penalize them.`,'Add <meta name="viewport" content="width=device-width, initial-scale=1">.',p.map(h=>({location:h.rel})),p.length))}let y=[],u=0;for(let s of i){let m=(s.content.match(/<(?:img|Image)\b[^>]*>/g)??[]).filter(p=>!/\balt\s*=/.test(p));m.length>0&&(u+=m.length,y.push({location:s.rel,snippet:m[0].slice(0,120)}))}if(u>0&&e.push(C("cb-img-no-alt","content","warning","Image elements without alt text",`${u} <img>/<Image> element(s) across ${y.length} file(s) have no alt attribute.`,'Add descriptive alt text to meaningful images; use alt="" for decorative ones.',y,u)),t){let s=i.filter(l=>/<img\b/.test(l.content));s.length>0&&e.push(C("cb-raw-img-next","technical","info","Raw <img> instead of next/image",`${s.length} file(s) use raw <img> tags; next/image provides automatic optimization, lazy loading and correct sizing.`,"Replace raw <img> with the next/image <Image> component where practical.",s.map(l=>({location:l.rel})),s.length))}return/application\/ld\+json|schema\.org/i.test(a)||e.push(C("cb-no-jsonld","structured-data","warning","No JSON-LD structured data in the codebase","No file references schema.org or emits application/ld+json. Structured data is a primary input for rich results and AI answer attribution.","Add Organization + WebSite JSON-LD to the root layout, and page-type schema (Article, FAQPage\u2026) where relevant.",[{location:n.root}])),/og:|openGraph/i.test(a)||e.push(C("cb-no-og","structured-data","warning","No Open Graph tags","No Open Graph configuration was found, so link shares render without preview cards.",r?"Add an `openGraph` section to your metadata exports (title, description, images).":"Add og:title, og:description and og:image meta tags to your page heads.",[{location:n.root}])),/canonical|alternates/i.test(a)||e.push(C("cb-no-canonical","technical","info","No canonical URL handling","No canonical link handling was found; duplicate URL variants may compete with each other.",r?"Set `metadataBase` and `alternates.canonical` in your metadata exports.":'Emit <link rel="canonical"> on each page.',[{location:n.root}])),e}function ie(n,e){return Ae.parse({id:an(),type:"codebase",path:n.root,framework:n.framework,createdAt:new Date().toISOString(),filesScanned:n.files.length,scorecard:ee(e),findings:e})}import{execFile as gn}from"child_process";import{promisify as mn}from"util";var hn=mn(gn);async function L(n,...e){let{stdout:t}=await hn("git",e,{cwd:n,maxBuffer:10485760});return t}async function Be(n){try{return await L(n,"rev-parse","--is-inside-work-tree"),!0}catch{return!1}}async function We(n){return(await L(n,"status","--porcelain")).trim()===""}async function ge(n){return(await L(n,"status","--porcelain")).split(`
|
|
19
|
+
`).map(t=>t.trim()).filter(Boolean).map(t=>t.replace(/^[A-Z?!]{1,2}\s+/,"").replace(/^"|"$/g,""))}async function fn(n,e){return e.length===0?"":(await L(n,"add","-N","--",...e).catch(()=>{}),L(n,"diff","--",...e))}async function Ve(n,e){if(e.length===0)return;await L(n,"reset","--",...e).catch(()=>{}),await L(n,"checkout","--",...e).catch(()=>{});let t=await ge(n),r=e.filter(o=>t.includes(o));r.length>0&&await L(n,"clean","-f","--",...r).catch(()=>{})}function yn(n,e){return[`You are fixing one specific SEO/AEO issue in a ${e} codebase. Make the smallest correct change; do not refactor unrelated code, do not reformat files, and match the project's existing code style.`,"",`Issue: ${n.title}`,`Details: ${n.description}`,`Recommended fix: ${n.recommendation}`,n.evidence.length>0?`Affected files/locations:
|
|
20
|
+
${n.evidence.map(t=>`- ${t.location}${t.snippet?` (${t.snippet})`:""}`).join(`
|
|
21
|
+
`)}`:"","","Apply the fix now by editing the files. When content is needed (titles, descriptions, alt text, structured data), derive it from the actual project content \u2014 read neighboring files to understand what the site is about. Finish with a one-paragraph summary of exactly what you changed."].filter(Boolean).join(`
|
|
22
|
+
`)}async function Qe(n,e,t,r,o){let i=await ge(e),a=await n.runAgent({cwd:e,prompt:yn(t,r),onEvent:o}),g=(await ge(e)).filter(u=>!i.includes(u)),y=await fn(e,g);return{findingId:t.id,agentSummary:a,changes:g.map(u=>({file:u,diff:y.split(/^(?=diff --git )/m).find(c=>c.includes(`b/${u}`))??""}))}}import{randomUUID as wn}from"crypto";var bn={type:"object",additionalProperties:!1,required:["prompts"],properties:{prompts:{type:"array",minItems:6,maxItems:10,items:{type:"string"},description:"Realistic questions actual users would ask an AI assistant in this site's space. Never mention the site by name \u2014 these must be neutral discovery questions."}}},xn={type:"object",additionalProperties:!1,required:["results"],properties:{results:{type:"array",items:{type:"object",additionalProperties:!1,required:["prompt","answer","recommendedBrands"],properties:{prompt:{type:"string"},answer:{type:"string",description:"Answer the question exactly as you would for a real user asking you this \u2014 recommend whatever you would genuinely recommend. 3-5 sentences."},recommendedBrands:{type:"array",items:{type:"string"},description:"Every brand, product, site or organization your answer recommended or cited, in order of prominence."}}}}}};function vn(n){let e=n.replace(/^https?:\/\//,"").replace(/^www\./,""),t=e.split(".")[0];return[e.toLowerCase(),t.toLowerCase()]}function Ye(n,e,t){let r=vn(n),o=a=>r.some(d=>a.toLowerCase().includes(d)),i=t.findIndex(o);return i===0?"primary":i>0||o(e)?"mentioned":"absent"}function kn(n){if(n.length===0)return 0;let e=n.reduce((t,r)=>t+(r.presence==="primary"?1:r.presence==="mentioned"?.5:0),0);return Math.round(e/n.length*100)}async function Xe(n,e,t,r){let o=r??(()=>{}),i=e.domain,a=t.filter(Boolean);if(a.length===0){o("Generating realistic buyer prompts for this niche",.15);let m=e.pages.map(h=>({url:h.url,title:h.title,description:h.metaDescription})).slice(0,15);a=(await n.analyzeJson({system:"You design AI-visibility test prompts. Given a crawled site, infer its niche and audience, then write the neutral questions real users would ask an AI assistant when they need what this site offers.",prompt:`Site pages:
|
|
23
|
+
${JSON.stringify(m,null,1)}
|
|
24
|
+
|
|
25
|
+
Write 8 such questions. NEVER name the site or brand \u2014 the point is to test whether it comes up on its own.`,jsonSchema:bn})).prompts}o(`Asking the model ${a.length} discovery questions`,.4);let d=await n.analyzeJson({system:"Answer each question exactly as you would for a real user who asked you it directly \u2014 give your genuine recommendations from your own knowledge. Do NOT try to guess what this test wants; just answer naturally and honestly, then list the brands/sites your answer recommended.",prompt:`Questions:
|
|
26
|
+
${a.map((m,p)=>`${p+1}. ${m}`).join(`
|
|
27
|
+
`)}`,jsonSchema:xn});o("Scoring visibility",.8);let g=d.results.map(m=>({prompt:m.prompt,presence:Ye(i,m.answer,m.recommendedBrands),answerExcerpt:m.answer.slice(0,400),competitors:m.recommendedBrands.filter(p=>Ye(i,"",[p])==="absent").slice(0,6)})),y=new Map;for(let m of g)for(let p of m.competitors)y.set(p,(y.get(p)??0)+1);let u=[...y.entries()].map(([m,p])=>({name:m,count:p})).sort((m,p)=>p.count-m.count).slice(0,10),c=kn(g),s=g.filter(m=>m.presence==="absent").length,l=`${i} appeared in ${g.length-s} of ${g.length} AI discovery answers (${g.filter(m=>m.presence==="primary").length} as the primary recommendation). `+(c>=60?"The model already treats this site as a go-to source in its niche.":c>=25?"The site has partial AI visibility \u2014 competitors are cited more often; strengthen topical authority and entity signals.":"The site is largely invisible to AI answers today; the competitors above own this space in the model's knowledge.");return Pe.parse({id:wn(),type:"ai-visibility",domain:i,createdAt:new Date().toISOString(),visibilityScore:c,prompts:g,competitorLeaderboard:u,summary:l})}var Sn="https://www.googleapis.com/auth/webmasters.readonly",An="https://accounts.google.com/o/oauth2/v2/auth",Pn="https://oauth2.googleapis.com/token";function Ze(n,e){let t=new URL(An);return t.searchParams.set("client_id",n),t.searchParams.set("redirect_uri",e),t.searchParams.set("response_type","code"),t.searchParams.set("scope",Sn),t.searchParams.set("access_type","offline"),t.searchParams.set("prompt","consent"),t.toString()}async function et(n){let e=await fetch(Pn,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams(n).toString()}),t=await e.json();if(!e.ok||t.error)throw new Error(`Google OAuth: ${t.error??e.status} ${t.error_description??""}`.trim());return t}async function tt(n,e,t,r){let o=await et({client_id:n,client_secret:e,code:t,grant_type:"authorization_code",redirect_uri:r});if(!o.refresh_token)throw new Error("Google did not return a refresh token \u2014 remove the app's prior grant at myaccount.google.com/permissions and reconnect.");return{refreshToken:o.refresh_token}}async function me(n,e,t){let r=await et({client_id:n,client_secret:e,refresh_token:t,grant_type:"refresh_token"});if(!r.access_token)throw new Error("Google OAuth refresh returned no access token");return r.access_token}async function nt(n,e,t){let r=await fetch(`https://www.googleapis.com/webmasters/v3${e}`,{...t,headers:{authorization:`Bearer ${n}`,"content-type":"application/json",...t?.headers}});if(!r.ok)throw new Error(`Search Console API ${r.status}: ${(await r.text()).slice(0,200)}`);return await r.json()}async function he(n){return(await nt(n,"/sites")).siteEntry??[]}function fe(n,e){let t=e.replace(/^https?:\/\//,"").replace(/^www\./,"").replace(/\/$/,"");for(let r of n){let o=r.siteUrl;if(o===`sc-domain:${t}`||o.replace(/^https?:\/\//,"").replace(/^www\./,"").replace(/\/$/,"")===t)return o}return null}async function ye(n,e,t=90,r=100){let o=new Date,i=new Date(o.getTime()-t*864e5),a=g=>g.toISOString().slice(0,10);return((await nt(n,`/sites/${encodeURIComponent(e)}/searchAnalytics/query`,{method:"POST",body:JSON.stringify({startDate:a(i),endDate:a(o),dimensions:["query"],rowLimit:r})})).rows??[]).map(g=>({query:g.keys[0],clicks:g.clicks,impressions:g.impressions,ctr:g.ctr,position:g.position}))}async function rt(n,e,t,r=3e4){let o=new Map;if(t.length===0)return o;let i=await fetch("https://api.dataforseo.com/v3/keywords_data/google_ads/search_volume/live",{method:"POST",headers:{authorization:`Basic ${Buffer.from(`${n}:${e}`).toString("base64")}`,"content-type":"application/json"},body:JSON.stringify([{keywords:t.slice(0,100),language_code:"en"}]),signal:AbortSignal.timeout(r)});if(!i.ok)throw new Error(`DataForSEO ${i.status}: ${(await i.text()).slice(0,200)}`);let a=await i.json();if(a.status_code!==2e4)throw new Error(`DataForSEO error status ${a.status_code}`);for(let d of a.tasks??[])for(let g of d.result??[])g.search_volume!==null&&g.search_volume!==void 0&&o.set(g.keyword.toLowerCase(),g.search_volume);return o}import{randomUUID as Rn}from"crypto";var we=class{info;listeners=new Set;payload=null;constructor(e){this.info={id:Rn(),kind:e,status:"running",events:[]}}emit(e,t,r){let o={ts:Date.now(),kind:e,message:t,fraction:r};this.info.events.push(o),this.info.events.length>500&&this.info.events.splice(0,this.info.events.length-500);for(let i of this.listeners)i(o)}done(e){this.info.status="done",this.info.resultId=e,this.emit("done",e??"done")}fail(e){this.info.status="error",this.info.error=e instanceof Error?e.message:String(e),this.emit("error",this.info.error)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}},ot=new Map;function H(n,e){let t=new we(n);return ot.set(t.info.id,t),e(t).catch(r=>{t.info.status==="running"&&t.fail(r)}),t}function be(n){return ot.get(n)}import se from"fs";import xe from"path";function D(n){V(),se.writeFileSync(xe.join(N,`${n.id}.json`),JSON.stringify(n,null,2))}function F(n){if(!/^[a-zA-Z0-9-]+$/.test(n))return null;try{let e=se.readFileSync(xe.join(N,`${n}.json`),"utf8");return Re.parse(JSON.parse(e))}catch{return null}}function it(){V();let n=[];for(let e of se.readdirSync(N)){if(!e.endsWith(".json"))continue;let t=F(e.slice(0,-5));t&&n.push(t)}return n.sort((e,t)=>t.createdAt.localeCompare(e.createdAt))}function st(n){if(!/^[a-zA-Z0-9-]+$/.test(n))return!1;try{return se.unlinkSync(xe.join(N,`${n}.json`)),!0}catch{return!1}}var In={".html":"text/html",".js":"text/javascript",".css":"text/css",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2",".json":"application/json"};function En(){let n=M.dirname($n(import.meta.url));for(let e of[M.join(n,"ui"),M.join(n,"../../dist/ui")])if(z.existsSync(M.join(e,"index.html")))return e;return null}function Tn(){let n=new Cn,e=()=>{let c=T();if(!c)throw new Error("Not configured \u2014 run `eternify-seo` in a terminal first");return c};n.get("/api/health",c=>c.json({ok:!0,version:"0.1.0"})),n.get("/api/config",c=>{let s=T();if(!s)return c.json({configured:!1});let l=$(s);return c.json({configured:!0,provider:s.provider,model:s.model??null,crawl:s.crawl,performance:s.performance,auth:l.authStatus(),hasAnthropicKey:!!s.anthropicApiKey,hasOpenaiKey:!!s.openaiApiKey,hasPsiKey:!!s.psiApiKey,hasDataForSeo:!!(s.dataForSeoLogin&&s.dataForSeoPassword),gsc:{configured:!!s.gsc,connected:!!s.gsc?.refreshToken}})});let t=v.object({provider:v.enum(["claude","openai"]).optional(),model:v.string().nullable().optional(),maxPages:v.number().int().min(1).max(200).optional(),render:v.boolean().optional(),performance:v.boolean().optional(),psiApiKey:v.string().nullable().optional(),dataForSeoLogin:v.string().nullable().optional(),dataForSeoPassword:v.string().nullable().optional()});n.put("/api/config",async c=>{let s=t.parse(await c.req.json()),l=e();return s.provider&&(l.provider=s.provider),s.model!==void 0&&(l.model=s.model??void 0),s.maxPages&&(l.crawl.maxPages=s.maxPages),s.render!==void 0&&(l.crawl.render=s.render),s.performance!==void 0&&(l.performance=s.performance),s.psiApiKey!==void 0&&(l.psiApiKey=s.psiApiKey??void 0),s.dataForSeoLogin!==void 0&&(l.dataForSeoLogin=s.dataForSeoLogin??void 0),s.dataForSeoPassword!==void 0&&(l.dataForSeoPassword=s.dataForSeoPassword??void 0),U(l),c.json({ok:!0})});let r=v.object({domain:v.string().min(3),maxPages:v.number().int().min(1).max(200).optional(),skipLlm:v.boolean().optional()});n.post("/api/audits",async c=>{let s=r.parse(await c.req.json()),l=e(),m=$(l),p=H("domain-audit",async h=>{let x=await ne(m,s.domain,{maxPages:s.maxPages??l.crawl.maxPages,timeoutMs:l.crawl.timeoutMs,skipLlm:s.skipLlm,render:l.crawl.render,measurePerformance:l.performance,psiApiKey:l.psiApiKey,onProgress:(w,k)=>h.emit("progress",w,k)});D(x),h.done(x.id)});return c.json({jobId:p.info.id})});let o=v.object({auditId:v.string(),keywords:v.array(v.string().min(1)).max(30)});n.post("/api/keywords",async c=>{let s=o.parse(await c.req.json()),l=e(),m=$(l),p=F(s.auditId);if(!p||p.type!=="domain")return c.json({error:"audit not found"},404);let h=H("keywords",async x=>{let w={};if(l.gsc?.refreshToken){x.emit("progress","Fetching real Search Console data",.1);try{let f=await me(l.gsc.clientId,l.gsc.clientSecret,l.gsc.refreshToken),R=fe(await he(f),p.domain);R?w.gscQueries=await ye(f,R):x.emit("log",`No Search Console property matches ${p.domain}`)}catch(f){x.emit("log",`Search Console fetch failed: ${f instanceof Error?f.message:f}`)}}x.emit("progress",`Analyzing ${s.keywords.length||"auto-detected"} keywords against ${p.pages.length} pages`,.3);let k=await Ke(m,p,s.keywords,w);if(l.dataForSeoLogin&&l.dataForSeoPassword){x.emit("progress","Fetching search volumes (DataForSEO)",.8);try{let f=await rt(l.dataForSeoLogin,l.dataForSeoPassword,k.keywords.map(R=>R.keyword));for(let R of k.keywords){let E=f.get(R.keyword.toLowerCase());E!==void 0&&(R.searchVolume=E)}}catch(f){x.emit("log",`DataForSEO fetch failed: ${f instanceof Error?f.message:f}`)}}D(k),x.done(k.id)});return c.json({jobId:h.info.id})});let i=v.object({auditId:v.string(),prompts:v.array(v.string()).max(15).default([])});n.post("/api/visibility",async c=>{let s=i.parse(await c.req.json()),l=e(),m=$(l),p=F(s.auditId);if(!p||p.type!=="domain")return c.json({error:"audit not found"},404);let h=H("ai-visibility",async x=>{let w=await Xe(m,p,s.prompts,(k,f)=>x.emit("progress",k,f));D(w),x.done(w.id)});return c.json({jobId:h.info.id})});let a=v.object({clientId:v.string().min(10),clientSecret:v.string().min(5)});n.post("/api/gsc/credentials",async c=>{let s=a.parse(await c.req.json()),l=e();return l.gsc={clientId:s.clientId.trim(),clientSecret:s.clientSecret.trim()},U(l),c.json({ok:!0})}),n.get("/api/gsc/connect",c=>{let s=e();if(!s.gsc)return c.json({error:"Save your Google OAuth client ID and secret first."},400);let l=`${new URL(c.req.url).origin}/api/gsc/callback`;return c.json({authUrl:Ze(s.gsc.clientId,l)})}),n.get("/api/gsc/callback",async c=>{let s=c.req.query("code"),l=e();if(!s||!l.gsc)return c.text("Missing code or GSC configuration",400);let m=`${new URL(c.req.url).origin}/api/gsc/callback`,{refreshToken:p}=await tt(l.gsc.clientId,l.gsc.clientSecret,s,m);return l.gsc.refreshToken=p,U(l),c.redirect("/settings?gsc=connected")}),n.get("/api/gsc/queries",async c=>{let s=c.req.query("domain")??"",l=e();if(!l.gsc?.refreshToken)return c.json({error:"Search Console is not connected"},400);let m=await me(l.gsc.clientId,l.gsc.clientSecret,l.gsc.refreshToken),p=fe(await he(m),s);return p?c.json({property:p,rows:await ye(m,p)}):c.json({error:`No Search Console property matches ${s}`},404)});let d=v.object({path:v.string().min(1),skipLlm:v.boolean().optional()});n.post("/api/codebase/scan",async c=>{let s=d.parse(await c.req.json()),l=e(),m=$(l),p=H("codebase-scan",async h=>{h.emit("progress",`Round 1: rule scan of ${s.path}`,.1);let x=re(s.path);h.emit("progress",`Detected ${x.framework}; ${x.files.length} source files`,.2);let w=oe(x);if(!s.skipLlm){h.emit("progress","Round 2: AI agent exploring the codebase (read-only)",.3);try{let f=await Ce(m,x.root,x.framework,w,R=>h.emit("agent",R.kind==="tool"?`[tool] ${R.message}`:R.message));w=[...w,...f],h.emit("progress",`AI auditor added ${f.length} finding(s)`,.7)}catch(f){h.emit("log",`AI audit round failed: ${f instanceof Error?f.message:f}`)}}let k=ie(x,w);if(!s.skipLlm){h.emit("progress","Writing the report",.85);try{k.llmReport=await B(m,{subject:`${x.framework} codebase at ${x.root}`,scorecard:k.scorecard,findings:w,contentSamples:x.files.slice(0,4).map(f=>({location:f.rel,sample:f.content.slice(0,1e3)}))})}catch(f){h.emit("log",`AI report failed: ${f instanceof Error?f.message:f}`)}}D(k),h.done(k.id)});return c.json({jobId:p.info.id})});let g=v.object({reportId:v.string(),findingId:v.string(),allowDirty:v.boolean().optional()});n.post("/api/codebase/fix",async c=>{let s=g.parse(await c.req.json()),l=e(),m=$(l),p=F(s.reportId);if(!p||p.type!=="codebase")return c.json({error:"codebase report not found"},404);let h=p.findings.find(w=>w.id===s.findingId);if(!h)return c.json({error:"finding not found"},404);if(!await Be(p.path))return c.json({error:"Not a git repository \u2014 auto-fix requires git so changes are revertible."},400);if(!s.allowDirty&&!await We(p.path))return c.json({error:"dirty-tree",detail:"The working tree has uncommitted changes. Commit/stash them, or retry allowing a dirty tree."},409);let x=H("codebase-fix",async w=>{w.emit("progress",`Agent fixing: ${h.title}`,.1);let k=await Qe(m,p.path,h,p.framework,f=>w.emit("agent",f.kind==="tool"?`[tool] ${f.message}`:f.message));w.payload=k,w.done()});return c.json({jobId:x.info.id})});let y=v.object({reportId:v.string(),files:v.array(v.string()).min(1)});n.post("/api/codebase/revert",async c=>{let s=y.parse(await c.req.json()),l=F(s.reportId);return!l||l.type!=="codebase"?c.json({error:"codebase report not found"},404):(await Ve(l.path,s.files),c.json({ok:!0}))}),n.get("/api/jobs/:id",c=>{let s=be(c.req.param("id"));return s?c.json({...s.info,payload:s.payload}):c.json({error:"not found"},404)}),n.get("/api/jobs/:id/events",c=>{let s=be(c.req.param("id"));return s?jn(c,async l=>{for(let m of s.info.events)await l.writeSSE({data:JSON.stringify(m)});s.info.status==="running"&&await new Promise(m=>{let p=s.subscribe(async h=>{await l.writeSSE({data:JSON.stringify(h)}),(h.kind==="done"||h.kind==="error")&&(p(),m())});l.onAbort(()=>{p(),m()})})}):c.json({error:"not found"},404)}),n.get("/api/reports",c=>{let s=it().map(l=>({id:l.id,type:l.type,createdAt:l.createdAt,subject:l.type==="domain"?l.domain:l.type==="codebase"?l.path:l.domain,overall:"scorecard"in l?l.scorecard.overall:void 0}));return c.json(s)}),n.get("/api/reports/:id",c=>{let s=F(c.req.param("id"));return s?c.json(s):c.json({error:"not found"},404)}),n.delete("/api/reports/:id",c=>c.json({ok:st(c.req.param("id"))}));let u=En();return n.get("*",c=>{if(!u)return c.text("UI not built. Run `npm run build:ui`.",500);let s=c.req.path==="/"?"/index.html":c.req.path,l=M.join(u,M.normalize(s));if(l.startsWith(u)&&z.existsSync(l)&&z.statSync(l).isFile()){let m=M.extname(l);return c.body(z.readFileSync(l),200,{"content-type":In[m]??"application/octet-stream"})}return c.html(z.readFileSync(M.join(u,"index.html"),"utf8"))}),n.onError((c,s)=>s.json({error:c.message},500)),n}function ct(n){let e=Tn();return new Promise((t,r)=>{at({fetch:e.fetch,port:n,hostname:"127.0.0.1"},i=>t(i.port)).on("error",i=>{i.code==="EADDRINUSE"?at({fetch:e.fetch,port:0,hostname:"127.0.0.1"},d=>t(d.port)).on("error",r):r(i)})})}var _=new On;function lt(n,e,t){let r=i=>i>=80?A.green:i>=55?A.yellow:A.red;console.log(""),console.log(A.bold(` ${n}`)),console.log(` Overall: ${r(e.overall)(A.bold(`${e.overall}/100`))}`);for(let i of e.categories){let a="\u2588".repeat(Math.round(i.score/5)).padEnd(20,"\u2591");console.log(` ${i.label.padEnd(18)} ${r(i.score)(a)} ${String(i.score).padStart(3)}`)}let o={critical:0,warning:0,info:0};for(let i of t)i.severity in o&&o[i.severity]++;console.log(`
|
|
28
|
+
${A.red(`${o.critical} critical`)} ${A.yellow(`${o.warning} warnings`)} ${A.dim(`${o.info} suggestions`)}`);for(let i of t.filter(a=>a.severity==="critical"))console.log(` ${A.red("\u2717")} ${i.title} ${A.dim(`(${i.occurrences}\xD7)`)}`);console.log("")}_.name("eternify-seo").description("SEO & AEO auditor for domains and codebases, powered by Claude or OpenAI").version("0.1.0");_.command("start",{isDefault:!0}).description("Start the web UI (default command)").option("-p, --port <port>","port for the local server","4173").option("--no-open","don't open the browser automatically").action(async n=>{await Z();let t=`http://localhost:${await ct(Number(n.port))}`;console.log(`
|
|
29
|
+
${A.bold("Eternify SEO/AEO Audit")} running at ${A.cyan(t)}
|
|
30
|
+
${A.dim("Press Ctrl+C to stop.")}
|
|
31
|
+
`),n.open&&await Ln(t).catch(()=>{})});_.command("login").description("(Re)configure provider, authentication and model").action(async()=>{await pe(T())});_.command("audit <domain>").description("Audit a domain from the terminal (no UI)").option("--pages <n>","max pages to crawl","25").option("--no-ai","skip the AI narrative report").option("--render","render pages with headless Chrome (JS execution)").option("--no-perf","skip Core Web Vitals measurement").option("--json","output the full report as JSON").action(async(n,e)=>{let t=await Z(),r=$(t),o=await ne(r,n,{maxPages:Number(e.pages),timeoutMs:t.crawl.timeoutMs,skipLlm:!e.ai,render:e.render??t.crawl.render,measurePerformance:e.perf&&t.performance,psiApiKey:t.psiApiKey,onProgress:i=>{e.json||process.stderr.write(`\r\x1B[2K ${A.dim(i.slice(0,100))}`)}});if(e.json||process.stderr.write("\r\x1B[2K"),D(o),e.json){console.log(JSON.stringify(o,null,2));return}if(lt(o.domain,o.scorecard,o.findings),o.llmReport){console.log(` ${A.bold("Summary:")} ${o.llmReport.summary}
|
|
32
|
+
`);for(let i of o.llmReport.actionPlan.slice(0,5))console.log(` ${A.cyan(`${i.priority}.`)} ${i.title} ${A.dim(`[impact: ${i.impact}, effort: ${i.effort}]`)}`);console.log("")}console.log(A.dim(" Full report saved \u2014 open the UI with `eternify-seo` to explore it.\n"))});_.command("scan [path]").description("Audit a codebase from the terminal (no UI)").option("--no-ai","skip the AI narrative report").option("--json","output the full report as JSON").action(async(n,e)=>{let t=await Z(),r=re(n??process.cwd()),o=oe(r),i=$(t);if(e.ai){process.stderr.write(A.dim(` Round 2: AI agent exploring the codebase (read-only)\u2026
|
|
33
|
+
`));try{let{aiCodebaseAudit:d}=await import("./aiAudit-VMLJU5MR.js"),g=await d(i,r.root,r.framework,o,y=>{y.kind==="tool"&&process.stderr.write(A.dim(` ${y.message}
|
|
34
|
+
`))});o=[...o,...g]}catch(d){process.stderr.write(A.yellow(` AI audit round failed: ${d instanceof Error?d.message:d}
|
|
35
|
+
`))}}let a=ie(r,o);if(e.ai){let{buildLlmReport:d}=await import("./report-TQRVRW54.js");try{a.llmReport=await d(i,{subject:`${r.framework} codebase at ${r.root}`,scorecard:a.scorecard,findings:o,contentSamples:r.files.slice(0,4).map(g=>({location:g.rel,sample:g.content.slice(0,1e3)}))})}catch{}}if(D(a),e.json){console.log(JSON.stringify(a,null,2));return}lt(`${r.root} (${r.framework}, ${r.files.length} files)`,a.scorecard,a.findings),console.log(A.dim(" Open the UI with `eternify-seo` to review findings and apply AI fixes.\n"))});_.command("config").description("Show the current configuration").action(()=>{let n=T();if(!n){console.log(`Not configured yet \u2014 run ${A.cyan("eternify-seo login")}.`);return}console.log(` Config file: ${G}`),console.log(` Provider: ${n.provider}`),console.log(` Model: ${n.model??"(provider default)"}`),console.log(` Auth: ${$(n).authStatus().detail}`),console.log(` Crawl limit: ${n.crawl.maxPages} pages`)});_.parseAsync().catch(n=>{console.error(A.red(`
|
|
36
|
+
${n instanceof Error?n.message:n}
|
|
37
|
+
`)),process.exit(1)});
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
async function u(){let r;try{({chromium:r}=await import("playwright-core"))}catch{throw new Error("JS rendering requires playwright-core \u2014 run `npm install playwright-core` where the tool is installed.")}let t;try{t=await r.launch({channel:"chrome",headless:!0})}catch{try{t=await r.launch({channel:"msedge",headless:!0})}catch{throw new Error("JS rendering needs Google Chrome (or Edge) installed on this machine.")}}let a=await t.newContext({userAgent:"EternifySEOAudit/0.1 (+https://eternifylabs.com)"});return{async render(c,i){let e=await a.newPage();try{let n=await e.goto(c,{timeout:i,waitUntil:"domcontentloaded"});await e.waitForLoadState("networkidle",{timeout:Math.min(5e3,i)}).catch(()=>{});let s=await e.content(),o={};if(n){let l=await n.allHeaders().catch(()=>({}));for(let[d,h]of Object.entries(l))o[d.toLowerCase()]=h}return{html:s,status:n?.status()??200,finalUrl:e.url(),headers:o}}catch{return null}finally{await e.close().catch(()=>{})}},async close(){await a.close().catch(()=>{}),await t.close().catch(()=>{})}}}export{u as createRenderer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-cyan-400:oklch(78.9% .154 211.53);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-500:oklch(68.5% .169 237.323);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-2xl:42rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-widest:.1em;--leading-relaxed:1.625;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#0b0f17;--color-panel:#111827;--color-panel-2:#1a2332;--color-line:#263042;--color-accent:#22d3ee;--color-accent-2:#818cf8}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.sticky{position:sticky}.top-0{top:0}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-auto{margin-top:auto}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-block{display:inline-block}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-4{height:calc(var(--spacing) * 4)}.h-8{height:calc(var(--spacing) * 8)}.h-full{height:100%}.max-h-56{max-height:calc(var(--spacing) * 56)}.max-h-72{max-height:calc(var(--spacing) * 72)}.min-h-screen{min-height:100vh}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-4{width:calc(var(--spacing) * 4)}.w-8{width:calc(var(--spacing) * 8)}.w-40{width:calc(var(--spacing) * 40)}.w-60{width:calc(var(--spacing) * 60)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-8{gap:calc(var(--spacing) * 8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-accent{border-color:var(--color-accent)}.border-accent\/30{border-color:#22d3ee4d}@supports (color:color-mix(in lab,red,red)){.border-accent\/30{border-color:color-mix(in oklab,var(--color-accent) 30%,transparent)}}.border-accent\/40{border-color:#22d3ee66}@supports (color:color-mix(in lab,red,red)){.border-accent\/40{border-color:color-mix(in oklab,var(--color-accent) 40%,transparent)}}.border-accent\/50{border-color:#22d3ee80}@supports (color:color-mix(in lab,red,red)){.border-accent\/50{border-color:color-mix(in oklab,var(--color-accent) 50%,transparent)}}.border-amber-500\/30{border-color:#f99c004d}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/30{border-color:color-mix(in oklab,var(--color-amber-500) 30%,transparent)}}.border-emerald-500\/30{border-color:#00bb7f4d}@supports (color:color-mix(in lab,red,red)){.border-emerald-500\/30{border-color:color-mix(in oklab,var(--color-emerald-500) 30%,transparent)}}.border-indigo-400\/40{border-color:#7d87ff66}@supports (color:color-mix(in lab,red,red)){.border-indigo-400\/40{border-color:color-mix(in oklab,var(--color-indigo-400) 40%,transparent)}}.border-line{border-color:var(--color-line)}.border-red-500\/30{border-color:#fb2c364d}@supports (color:color-mix(in lab,red,red)){.border-red-500\/30{border-color:color-mix(in oklab,var(--color-red-500) 30%,transparent)}}.border-red-500\/40{border-color:#fb2c3666}@supports (color:color-mix(in lab,red,red)){.border-red-500\/40{border-color:color-mix(in oklab,var(--color-red-500) 40%,transparent)}}.border-sky-500\/30{border-color:#00a5ef4d}@supports (color:color-mix(in lab,red,red)){.border-sky-500\/30{border-color:color-mix(in oklab,var(--color-sky-500) 30%,transparent)}}.border-transparent{border-color:#0000}.bg-accent{background-color:var(--color-accent)}.bg-accent-2{background-color:var(--color-accent-2)}.bg-accent\/10{background-color:#22d3ee1a}@supports (color:color-mix(in lab,red,red)){.bg-accent\/10{background-color:color-mix(in oklab,var(--color-accent) 10%,transparent)}}.bg-accent\/90{background-color:#22d3eee6}@supports (color:color-mix(in lab,red,red)){.bg-accent\/90{background-color:color-mix(in oklab,var(--color-accent) 90%,transparent)}}.bg-amber-500\/10{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/10{background-color:color-mix(in oklab,var(--color-amber-500) 10%,transparent)}}.bg-amber-500\/15{background-color:#f99c0026}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/15{background-color:color-mix(in oklab,var(--color-amber-500) 15%,transparent)}}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-emerald-500\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/10{background-color:color-mix(in oklab,var(--color-emerald-500) 10%,transparent)}}.bg-emerald-500\/15{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/15{background-color:color-mix(in oklab,var(--color-emerald-500) 15%,transparent)}}.bg-indigo-500\/15{background-color:#625fff26}@supports (color:color-mix(in lab,red,red)){.bg-indigo-500\/15{background-color:color-mix(in oklab,var(--color-indigo-500) 15%,transparent)}}.bg-panel{background-color:var(--color-panel)}.bg-panel-2{background-color:var(--color-panel-2)}.bg-panel\/60{background-color:#11182799}@supports (color:color-mix(in lab,red,red)){.bg-panel\/60{background-color:color-mix(in oklab,var(--color-panel) 60%,transparent)}}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/10{background-color:color-mix(in oklab,var(--color-red-500) 10%,transparent)}}.bg-red-500\/15{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/15{background-color:color-mix(in oklab,var(--color-red-500) 15%,transparent)}}.bg-sky-500\/15{background-color:#00a5ef26}@supports (color:color-mix(in lab,red,red)){.bg-sky-500\/15{background-color:color-mix(in oklab,var(--color-sky-500) 15%,transparent)}}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-accent{--tw-gradient-from:var(--color-accent);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-accent-2{--tw-gradient-to:var(--color-accent-2);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.fill-gray-400{fill:var(--color-gray-400)}.fill-white{fill:var(--color-white)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-accent{color:var(--color-accent)}.text-accent-2{color:var(--color-accent-2)}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-black{color:var(--color-black)}.text-emerald-300{color:var(--color-emerald-300)}.text-emerald-400{color:var(--color-emerald-400)}.text-gray-100{color:var(--color-gray-100)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-indigo-300{color:var(--color-indigo-300)}.text-red-300{color:var(--color-red-300)}.text-red-400{color:var(--color-red-400)}.text-sky-300{color:var(--color-sky-300)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.placeholder-gray-600::placeholder{color:var(--color-gray-600)}.accent-cyan-400{accent-color:var(--color-cyan-400)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}@media(hover:hover){.hover\:border-accent\/50:hover{border-color:#22d3ee80}@supports (color:color-mix(in lab,red,red)){.hover\:border-accent\/50:hover{border-color:color-mix(in oklab,var(--color-accent) 50%,transparent)}}.hover\:bg-accent\/10:hover{background-color:#22d3ee1a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-accent\/10:hover{background-color:color-mix(in oklab,var(--color-accent) 10%,transparent)}}.hover\:bg-panel-2:hover{background-color:var(--color-panel-2)}.hover\:bg-red-500\/10:hover{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-red-500\/10:hover{background-color:color-mix(in oklab,var(--color-red-500) 10%,transparent)}}.hover\:text-gray-100:hover{color:var(--color-gray-100)}.hover\:text-gray-200:hover{color:var(--color-gray-200)}.hover\:opacity-90:hover{opacity:.9}}.focus\:border-accent:focus{border-color:var(--color-accent)}.disabled\:opacity-40:disabled{opacity:.4}@media(min-width:40rem){.sm\:flex-row{flex-direction:row}}}body{background:var(--color-ink);color:#e5e7eb;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}::selection{background:#22d3ee4d}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}}
|