seo 0.2.21 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,7 +70,8 @@ through adding your own desktop OAuth client.
70
70
 
71
71
  Research providers are optional and connected separately. Start with the main
72
72
  report, then add DataForSEO when keyword estimates, live results, domain
73
- footprints, ranking pages, or competitor research would change the decision.
73
+ footprints, ranking pages, competitor research, or exact local result context
74
+ would change the decision.
74
75
 
75
76
  That is the normal path. The `seo` command is then available in every terminal,
76
77
  script, CI job, and local MCP client on the machine.
@@ -133,6 +134,8 @@ Use `seo help all` for the longer command list.
133
134
  ranks.
134
135
  - Detect repeated first-party and competitor page patterns, then return bounded
135
136
  data-source research briefs before proposing a new programmatic template.
137
+ - Find location-specific Search Console demand and repeated local page patterns,
138
+ then add a few exact local result snapshots when the decision needs them.
136
139
  - Give scripts and agents deterministic JSON, Markdown, stable rule IDs, and a
137
140
  compact local MCP surface.
138
141
 
@@ -209,6 +212,7 @@ commands:
209
212
  seo reports describe keyword-research --json
210
213
  seo reports describe domain-overview --json
211
214
  seo reports describe competitor-keyword-gap --json
215
+ seo reports describe local-search-demand --json
212
216
 
213
217
  seo reports run domain-overview \
214
218
  --params '{"domain":"example.com","countryCode":"GB","languageCode":"en"}' \
@@ -229,6 +233,9 @@ The research flow now covers:
229
233
  - `competitor-keyword-gap` for comparing up to three relevant domains with
230
234
  retained Search Console themes, the site's provider-observed rankings, and
231
235
  programmatic page patterns;
236
+ - `local-search-demand` for matching explicit place names, nearby phrases, and
237
+ postal codes in retained Search Console rows, reviewing repeated local page
238
+ patterns, and optionally adding up to three exact local result snapshots;
232
239
  - `link-evidence` for a current link summary, one representative backlink per
233
240
  referring domain, and linked-target checks against saved crawl and Search
234
241
  Console evidence.
@@ -238,6 +245,8 @@ are estimates. Search Console remains the evidence for measured search
238
245
  performance on a property you own. A live result snapshot remains the evidence
239
246
  for one query, market, device, and observation time. Reports can combine these
240
247
  sources without blending their meanings or turning a missing row into zero.
248
+ Local query wording does not establish the searcher's physical location, and an
249
+ observed local pack does not establish that a particular listing appeared.
241
250
 
242
251
  Read the [research provider guide](https://seoskill.dev/docs/research-providers)
243
252
  for setup, costs, caching, country-level limits, competitor classification, and
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
3
- import{$ as e,$n as t,$t as n,A as r,An as i,At as a,B as o,Bn as s,Bt as c,C as l,Cn as u,Ct as d,D as f,Dn as p,Dt as m,E as h,En as ee,Et as g,F as _,Fn as te,Ft as v,G as ne,Gn as re,Gt as ie,H as ae,Hn as oe,Ht as se,I as ce,In as le,It as ue,J as de,Jn as fe,Jt as pe,K as me,Kn as he,Kt as ge,L as _e,Ln as ve,Lt as ye,M as be,Mn as xe,Mt as Se,N as Ce,Nn as we,Nt as Te,O as Ee,On as De,Ot as Oe,P as ke,Pn as Ae,Pt as je,Q as Me,Qn as Ne,Qt as Pe,R as Fe,Rn as Ie,Rt as Le,S as Re,Sn as ze,St as Be,T as Ve,Tn as He,Tt as Ue,U as We,Un as Ge,Ut as Ke,V as qe,Vn as Je,Vt as Ye,W as Xe,Wn as Ze,Wt as Qe,X as $e,Xn as et,Xt as tt,Y as nt,Yn as rt,Yt as it,Z as at,Zn as ot,Zt as st,_ as ct,_n as lt,_t as ut,a as dt,an as ft,ar as pt,at as mt,b as ht,bn as gt,bt as _t,cn as vt,cr as yt,ct as bt,d as xt,dn as St,dt as Ct,en as wt,er as Tt,et as Et,f as Dt,fn as Ot,ft as kt,g as At,gn as jt,gt as Mt,h as Nt,hn as Pt,ht as Ft,i as It,in as Lt,ir as Rt,it as zt,j as Bt,jn as Vt,jt as Ht,k as Ut,kn as Wt,kt as Gt,l as Kt,ln as qt,lt as Jt,m as Yt,mn as Xt,mt as Zt,nn as Qt,nr as $t,nt as en,o as tn,on as nn,or as y,ot as rn,p as an,pn as on,pt as sn,q as cn,qn as ln,qt as un,r as dn,rn as fn,rr as pn,rt as mn,sn as hn,sr as gn,st as _n,t as vn,tn as yn,tr as bn,tt as xn,u as Sn,un as Cn,ut as wn,v as Tn,vn as En,vt as Dn,w as On,wn as kn,wt as An,x as jn,xn as Mn,xt as Nn,y as Pn,yn as Fn,yt as In,z as Ln,zn as Rn,zt as zn}from"./dist-B-zhNcyH.js";import{constants as Bn,copyFileSync as Vn,createWriteStream as Hn,existsSync as b,mkdirSync as Un,readFileSync as Wn,readdirSync as Gn,realpathSync as Kn,renameSync as qn,rmSync as Jn,statSync as Yn,writeFileSync as Xn}from"node:fs";import{homedir as Zn}from"node:os";import{basename as Qn,dirname as x,isAbsolute as $n,join as S,posix as er,relative as tr,resolve as nr,win32 as rr}from"node:path";import{randomUUID as ir}from"node:crypto";import{spawn as ar,spawnSync as or}from"node:child_process";import{fileURLToPath as sr}from"node:url";import{isDeepStrictEqual as cr,stripVTControlCharacters as lr}from"node:util";import{lstat as ur,mkdir as dr,mkdtemp as fr,readFile as pr,readdir as mr,rename as hr,rm as gr,stat as _r,unlink as vr,writeFile as yr}from"node:fs/promises";import{defineCommand as C,runCommand as br,runMain as xr}from"citty";import{cancel as Sr,confirm as Cr,intro as wr,isCancel as Tr,log as Er,multiselect as Dr,note as w,outro as Or,password as kr,select as Ar,text as jr}from"@clack/prompts";import Mr from"picocolors";import Nr from"string-width";import Pr from"cli-table3";import{applyEdits as Fr,modify as Ir,parse as Lr,printParseErrorCode as Rr}from"jsonc-parser";import{finished as zr}from"node:stream/promises";import Br from"update-notifier";function T(e){return e.replaceAll(`&`,`&amp;`).replaceAll(`<`,`&lt;`).replaceAll(`>`,`&gt;`).replaceAll(`"`,`&quot;`).replaceAll(`'`,`&#39;`)}function Vr(e){try{let t=new URL(e);return t.protocol===`http:`||t.protocol===`https:`?t.toString():void 0}catch{return}}function Hr(e){let t=Vr(e),n=T(e);return t?`<a href="${T(t)}" rel="noreferrer">${n}</a>`:n}function Ur(e){return e.length===0?`<p class="empty">No items to report.</p>`:`<ul>${e.map(e=>`<li>${T(e)}</li>`).join(``)}</ul>`}function Wr(e){return e.sections.map(e=>`<section class="panel">
3
+ import{$ as e,$n as t,$t as n,A as r,An as i,At as a,B as o,Bn as s,Bt as c,C as l,Cn as u,Ct as d,D as f,Dn as p,Dt as m,E as h,En as ee,Et as g,F as _,Fn as te,Ft as v,G as ne,Gn as re,Gt as ie,H as ae,Hn as oe,Ht as se,I as ce,In as le,It as ue,J as de,Jn as fe,Jt as pe,K as me,Kn as he,Kt as ge,L as _e,Ln as ve,Lt as ye,M as be,Mn as xe,Mt as Se,N as Ce,Nn as we,Nt as Te,O as Ee,On as De,Ot as Oe,P as ke,Pn as Ae,Pt as je,Q as Me,Qn as Ne,Qt as Pe,R as Fe,Rn as Ie,Rt as Le,S as Re,Sn as ze,St as Be,T as Ve,Tn as He,Tt as Ue,U as We,Un as Ge,Ut as Ke,V as qe,Vn as Je,Vt as Ye,W as Xe,Wn as Ze,Wt as Qe,X as $e,Xn as et,Xt as tt,Y as nt,Yn as rt,Yt as it,Z as at,Zn as ot,Zt as st,_ as ct,_n as lt,_t as ut,a as dt,an as ft,ar as pt,at as mt,b as ht,bn as gt,bt as _t,cn as vt,cr as yt,ct as bt,d as xt,dn as St,dt as Ct,en as wt,er as Tt,et as Et,f as Dt,fn as Ot,ft as kt,g as At,gn as jt,gt as Mt,h as Nt,hn as Pt,ht as Ft,i as It,in as Lt,ir as Rt,it as zt,j as Bt,jn as Vt,jt as Ht,k as Ut,kn as Wt,kt as Gt,l as Kt,ln as qt,lt as Jt,m as Yt,mn as Xt,mt as Zt,nn as Qt,nr as $t,nt as en,o as tn,on as nn,or as y,ot as rn,p as an,pn as on,pt as sn,q as cn,qn as ln,qt as un,r as dn,rn as fn,rr as pn,rt as mn,sn as hn,sr as gn,st as _n,t as vn,tn as yn,tr as bn,tt as xn,u as Sn,un as Cn,ut as wn,v as Tn,vn as En,vt as Dn,w as On,wn as kn,wt as An,x as jn,xn as Mn,xt as Nn,y as Pn,yn as Fn,yt as In,z as Ln,zn as Rn,zt as zn}from"./dist-DNsXiggv.js";import{constants as Bn,copyFileSync as Vn,createWriteStream as Hn,existsSync as b,mkdirSync as Un,readFileSync as Wn,readdirSync as Gn,realpathSync as Kn,renameSync as qn,rmSync as Jn,statSync as Yn,writeFileSync as Xn}from"node:fs";import{homedir as Zn}from"node:os";import{basename as Qn,dirname as x,isAbsolute as $n,join as S,posix as er,relative as tr,resolve as nr,win32 as rr}from"node:path";import{randomUUID as ir}from"node:crypto";import{spawn as ar,spawnSync as or}from"node:child_process";import{fileURLToPath as sr}from"node:url";import{isDeepStrictEqual as cr,stripVTControlCharacters as lr}from"node:util";import{lstat as ur,mkdir as dr,mkdtemp as fr,readFile as pr,readdir as mr,rename as hr,rm as gr,stat as _r,unlink as vr,writeFile as yr}from"node:fs/promises";import{defineCommand as C,runCommand as br,runMain as xr}from"citty";import{cancel as Sr,confirm as Cr,intro as wr,isCancel as Tr,log as Er,multiselect as Dr,note as w,outro as Or,password as kr,select as Ar,text as jr}from"@clack/prompts";import Mr from"picocolors";import Nr from"string-width";import Pr from"cli-table3";import{applyEdits as Fr,modify as Ir,parse as Lr,printParseErrorCode as Rr}from"jsonc-parser";import{finished as zr}from"node:stream/promises";import Br from"update-notifier";function T(e){return e.replaceAll(`&`,`&amp;`).replaceAll(`<`,`&lt;`).replaceAll(`>`,`&gt;`).replaceAll(`"`,`&quot;`).replaceAll(`'`,`&#39;`)}function Vr(e){try{let t=new URL(e);return t.protocol===`http:`||t.protocol===`https:`?t.toString():void 0}catch{return}}function Hr(e){let t=Vr(e),n=T(e);return t?`<a href="${T(t)}" rel="noreferrer">${n}</a>`:n}function Ur(e){return e.length===0?`<p class="empty">No items to report.</p>`:`<ul>${e.map(e=>`<li>${T(e)}</li>`).join(``)}</ul>`}function Wr(e){return e.sections.map(e=>`<section class="panel">
4
4
  <h2>${T(e.title)}</h2>
5
5
  ${Ur(e.bullets)}
6
6
  </section>`).join(``)}function Gr(e){return e.map(e=>`<section class="panel">
@@ -168,7 +168,7 @@ Google Analytics impact
168
168
  `),z([`Metric`,`Before`,`After`,`Delta`],[[`Sessions`,U(e.analytics.before.metrics.sessions),U(e.analytics.after.metrics.sessions),`${U(e.analytics.delta.sessions)} (${Rs(e.analytics.delta.sessionPct)})`],[`Conversions`,U(e.analytics.before.metrics.conversions),U(e.analytics.after.metrics.conversions),`${U(e.analytics.delta.conversions)} (${Rs(e.analytics.delta.conversionPct)})`],[`Revenue`,e.analytics.before.metrics.totalRevenue.toFixed(2),e.analytics.after.metrics.totalRevenue.toFixed(2),`${e.analytics.delta.totalRevenue.toFixed(2)} (${Rs(e.analytics.delta.revenuePct)})`]])),e.control&&(process.stdout.write(`
169
169
  Control comparison
170
170
  `),R([[`Control`,e.control.change.title],[`Adjusted clicks`,e.control.adjusted.clickDelta===null?`n/a`:U(e.control.adjusted.clickDelta)],[`Adjusted click %`,e.control.adjusted.clickPctPoints===null?`n/a`:`${e.control.adjusted.clickPctPoints.toFixed(1)} pts`],[`Note`,e.control.note]])),Y(`Warnings`,e.warnings),Y(`Caveats`,e.caveats)}const Us=C({meta:{name:`tests`,description:`Create and report local SEO tests from Search Console and Google Analytics data`},subCommands:{list:C({meta:{name:`list`,description:`List saved SEO tests`},args:{site:{type:`string`,description:`GSC property URL.`},client:{type:`string`,description:`Legacy alias for --project.`},project:{type:`string`,description:`Saved project id or name.`},limit:{type:`string`,description:`Maximum tests to list. Defaults to 20.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=N(e),n=D(e),r=se({site:(n||E(e.site)?await V({client:n,site:E(e.site),options:{json:t}}):void 0)?.site,limit:k(e.limit)});if(t){L({tests:r});return}z([`ID`,`Date`,`Scope`,`Target`,`Title`],r.map(e=>[e.id,e.changedAt,e.scope,e.target,e.title]))}}),create:C({meta:{name:`create`,description:`Create a local SEO test`},args:{site:{type:`string`,description:`GSC property URL.`},client:{type:`string`,description:`Legacy alias for --project.`},project:{type:`string`,description:`Saved project id or name.`},scope:{type:`string`,default:`page`,description:`Change scope: site, page, query, or group.`},target:{type:`string`,required:!0,description:`Changed URL, query, group pattern, or site marker.`},title:{type:`string`,required:!0,description:`Plain-English name for the test.`},description:{type:`string`,description:`Optional note about what changed.`},date:{type:`string`,description:`Change date in YYYY-MM-DD. Defaults to today.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=N(e),n=await V({client:D(e),site:E(e.site),options:{json:t}}),r=Ke({site:n.site,scope:Is(e.scope),target:E(e.target)??``,title:E(e.title)??``,description:E(e.description),changedAt:E(e.date)??Fs()});if(t){L(r);return}R([[`ID`,r.id],[`Date`,r.changedAt],[`Project`,n.client?.name??n.site],[`Scope`,r.scope],[`Target`,r.target],[`Title`,r.title]])}}),report:C({meta:{name:`report`,description:`Report an SEO test with equal finalized before/after windows`},args:{id:{type:`string`,description:`Saved SEO test id.`},site:{type:`string`,description:`GSC property URL.`},client:{type:`string`,description:`Legacy alias for --project.`},project:{type:`string`,description:`Saved project id or name.`},scope:{type:`string`,description:`Ad-hoc change scope: site, page, query, or group.`},target:{type:`string`,description:`Ad-hoc changed URL, query, group pattern, or site marker.`},title:{type:`string`,description:`Ad-hoc test title.`},date:{type:`string`,description:`Ad-hoc change date in YYYY-MM-DD.`},property:{type:`string`,description:`Google Analytics property ID.`},"control-scope":{type:`string`,description:`Control scope: site, page, query, or group.`},"control-target":{type:`string`,description:`Control URL, query, group pattern, or site marker.`},"control-title":{type:`string`,description:`Plain-English name for the control group.`},before:{type:`string`,description:`Days before the change date. Defaults to 28.`},after:{type:`string`,description:`Days after the change date. Defaults to 28.`},refresh:{type:`boolean`,default:!1,description:`Bypass the local Search Console and Google Analytics cache.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=N(e),n=E(e.id),r=!n||D(e)||E(e.site)?await V({client:D(e),site:E(e.site),options:{json:t,refresh:O(e.refresh)}}):void 0,i=await Be({id:n,site:n?void 0:r?.site,scope:n?void 0:Ls(e.scope),target:n?void 0:E(e.target),title:E(e.title),changedAt:n?void 0:E(e.date),googleAnalyticsPropertyId:E(e.property)??r?.client?.analytics.google?.propertyId,controlScope:Ls(e[`control-scope`]),controlTarget:E(e[`control-target`]),controlTitle:E(e[`control-title`]),beforeDays:A(e.before,`--before`),afterDays:A(e.after,`--after`),refresh:O(e.refresh)});if(t){L(i);return}Hs(i)}})}});function Ws(e){if(!e)return;let t=Date.now();return e=>{let n=((Date.now()-t)/1e3).toFixed(1);process.stderr.write(`[seo ${n}s] ${e}\n`)}}const Gs={site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},client:{type:`string`,description:`Legacy alias for --project.`},project:{type:`string`,description:`Saved project id or name.`}},Ks={...X([`includeBrand`,`verifyContent`,`verifyLimit`,`js`,`fetchConcurrency`,`fetchIntervalCap`,`fetchIntervalMs`,`refresh`]),json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}};function qs(e){return{includeBrand:O(e[`include-brand`]),verifyContent:O(e[`verify-content`]),verifyLimit:k(e[`verify-limit`])??3,js:O(e.js)?!0:void 0,rate:ui(e),refresh:O(e.refresh)}}function Js(e){return{client:D(e),site:E(e.site),refresh:O(e.refresh)}}const{json:Ys,...Xs}=Ks,Zs=Xs,Qs={client:{type:`string`,description:`Legacy alias for --project.`},project:{type:`string`,description:`Saved project id or name. Defaults to the configured default.`},site:{type:`string`,description:`GSC property URL/id when not using a saved project.`}};function $s(e){return{type:`string`,description:`Output folder. Defaults to ./seo-export/<project>-${e}-YYYY-MM-DD.`}}function ec(e){return e.toLowerCase().replace(/^sc-domain:/,``).replace(/^https?:\/\//,``).replace(/[^a-z0-9._-]+/g,`-`).replace(/^-+|-+$/g,``)}function tc(e){let t=new Date().toISOString().slice(0,10),n=ec(e.clientId??e.site);return nr(process.cwd(),`seo-export`,`${n}-${e.report}-${t}`)}async function nc(e,t){await dr(e,{recursive:!0});let n=[];for(let r of t){let t=nr(e,r.filename);await yr(t,Pt(r.rows,r.headers),`utf8`),n.push(t)}return n}function rc(e,t){process.stdout.write(`Wrote ${yn(t.length,`CSV file`)} to ${e}\n`);for(let e of t)process.stdout.write(`- ${e}\n`)}const ic=C({meta:{name:`diagnose`,description:`Export diagnosis tables: priorities, anomalies, movement, decay, cannibalisation, striking-distance, and quick wins.`},args:{...Qs,out:$s(`diagnose`),days:{type:`string`,description:`GSC lookback window. Defaults to 90.`},recent:{type:`string`,description:`Recent anomaly window in days.`},limit:{type:`string`,description:`Maximum rows per movement/opportunity table.`},"include-brand":{type:`boolean`,default:!1,description:`Include branded queries in opportunity reports.`},"verify-content":{type:`boolean`,default:!1,description:`Verify top opportunities against page title, meta, and content.`},"verify-limit":{type:`string`,description:`Maximum opportunity URLs to verify.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering for verified pages.`},"fetch-concurrency":{type:`string`,description:`Maximum concurrent page fetches per host. Defaults to 4.`},"fetch-interval-cap":{type:`string`,description:`Maximum page fetches per interval per host. Defaults to 4.`},"fetch-interval-ms":{type:`string`,description:`Fetch rate interval in milliseconds. Defaults to 1000.`},refresh:{type:`boolean`,default:!1,description:`Bypass local cache and fetch fresh data.`}},run:async({args:e})=>{let t=await V({client:D(e),site:E(e.site)}),n=await Le({site:t.site,days:k(e.days),recentDays:k(e.recent),limit:k(e.limit),brandTerms:t.client?.brandTerms,includeBrand:O(e[`include-brand`]),verifyContent:O(e[`verify-content`]),verifyLimit:k(e[`verify-limit`]),js:O(e.js)?!0:`auto`,rate:ui(e),refresh:O(e.refresh),progress:Ws(!0)}),r=E(e.out)??tc({report:`diagnose`,clientId:t.client?.id,site:t.site});rc(r,await nc(r,St(n)))}}),ac=`.seo-okf.json`,oc=`${JSON.stringify({generator:`seo`,format:`okf`,version:1})}\n`;function sc(e,t){let n=nr(e,t),r=tr(nr(e),n);if(!r||r.startsWith(`..`)||$n(r))throw new y(`INVALID_INPUT`,`Unsafe OKF file path: ${t}`);return n}async function cc(e,t){for(let n of t){let t=sc(e,n.path);await dr(x(t),{recursive:!0}),await yr(t,n.content)}await yr(S(e,ac),oc)}async function lc(e){let t=[];async function n(e,r=``){let i=await mr(e,{withFileTypes:!0});for(let a of i){let i=r?`${r}/${a.name}`:a.name,o=S(e,a.name);a.isDirectory()?await n(o,i):a.isFile()&&a.name.endsWith(`.md`)&&t.push({path:i,content:await pr(o,`utf8`)})}}return await n(e),t.sort((e,t)=>e.path<t.path?-1:+(e.path>t.path))}async function uc(e){try{let t=await ur(e);if(t.isSymbolicLink()||!t.isDirectory())throw new y(`INVALID_INPUT`,`OKF output must be a real directory: ${e}`);if(!(await mr(e)).length)return!0;try{if(await pr(S(e,ac),`utf8`)===oc)return!0}catch{}throw new y(`INVALID_INPUT`,`Refusing to replace non-empty unmanaged directory: ${e}`)}catch(e){if(e.code===`ENOENT`)return!1;throw e}}async function dc(e,t){if(!wt(t).valid)throw new y(`INVALID_INPUT`,`Generated bundle did not pass seo OKF checks and was not written.`);let n=nr(e),r=x(n);await dr(r,{recursive:!0});let i=await uc(n),a=await fr(S(r,`.${Qn(n)}-staging-`)),o=S(r,`.${Qn(n)}-backup-${ir()}`),s=!1;try{await cc(a,t);let e=wt(await lc(a));if(!e.valid)throw new y(`INTERNAL_ERROR`,`Staged OKF files failed validation after writing.`);return i&&(await hr(n,o),s=!0),await hr(a,n),s&&await gr(o,{recursive:!0,force:!0}),e}catch(e){throw await gr(a,{recursive:!0,force:!0}),s&&(await gr(n,{recursive:!0,force:!0}),await hr(o,n)),e}}async function fc(e,t){if(!e){process.stdout.write(t);return}await dr(x(e),{recursive:!0}),await yr(e,t),process.stdout.write(`Wrote ${e}\n`)}function pc(e){let t=E(e)??`okf`;if([`okf`,`markdown`,`json`].includes(t))return t;throw Error(`Format must be one of: okf, markdown, json.`)}function mc(e){return{reportId:e.id,url:e.config.url,generatedAt:e.generatedAt,crawl:{summary:e.summary,caveats:e.caveats,warnings:e.warnings,pages:e.pages.map(e=>({url:e.finalUrl,title:e.title,metaDescription:e.metaDescription,status:e.status,indexable:e.indexable,wordCount:e.wordCount,schemaTypes:e.schemaTypes,sameAs:e.schemaSameAs,internalInlinks:e.internalInlinkCount,contentSample:e.contentSample}))},aiReadiness:p(e),entityReadiness:De(e),llmsTxt:He(e)}}function hc(e){let t=mc(e);return`${[`# Site Knowledge Export`,``,`Source: ${e.config.url}`,`Crawl report: ${e.id}`,`Generated: ${t.generatedAt}`,``,`## Summary`,``,`- Documents: ${e.summary.totalPages}${e.requestEvidenceStatus===`available`?` from ${e.requests.length} URL requests`:e.requestEvidenceStatus===`partial`?` from ${e.requests.length} observed URL requests (request evidence is partial)`:` (request evidence unavailable for this legacy report)`}`,`- Indexable pages: ${e.summary.indexablePages}`,`- Crawl findings: ${e.issues.length} (${e.summary.highIssues} high, ${e.summary.mediumIssues} medium, ${e.summary.lowIssues} low)`,`- AI-search evidence: ${t.aiReadiness.dataStatus} (${t.aiReadiness.assessment})`,`- Entity evidence: ${t.entityReadiness.dataStatus} (${t.entityReadiness.assessment})`,``,`## Top AI Readiness Actions`,``,...t.aiReadiness.topActions.length?t.aiReadiness.topActions.map(e=>`- ${e.title}: ${e.action}`):[`- No AI-search technical actions in this crawl.`],``,`## Entity Signals`,``,`- sameAs profiles: ${t.entityReadiness.entities.sameAs.length}`,`- Social profiles: ${t.entityReadiness.entities.socialProfiles.length}`,`- Authors: ${t.entityReadiness.entities.authors.length}`,``,`## Important Pages`,``,...e.pages.filter(e=>e.indexable&&e.status<400).slice(0,100).map(e=>`- [${e.title??e.finalUrl}](${e.finalUrl}) - ${e.metaDescription??e.contentSample??`No summary available.`}`),``,`# Citations`,``,`- [Crawl start URL](${e.config.url})`,``].join(`
171
- `)}\n`}const gc=C({meta:{name:`knowledge`,description:`Export site knowledge from a saved crawl as OKF, Markdown, or JSON`},args:{"report-id":{type:`string`,description:`Saved crawl report id to export.`},site:{type:`string`,description:`GSC property URL for selecting the latest saved crawl.`},client:{type:`string`,description:`Legacy alias for --project.`},project:{type:`string`,description:`Saved project id or name.`},format:{type:`string`,description:`Output format: okf, markdown, or json. Defaults to okf.`},output:{type:`string`,description:`Output directory for OKF, or output file for Markdown/JSON.`},"max-concepts":{type:`string`,description:`Maximum OKF concept files. Defaults to 500.`},json:{type:`boolean`,default:!1,description:`Print command metadata as JSON.`}},run:async({args:e})=>{let t=N(e),r=pc(e.format),i=r===`okf`?n(A(e[`max-concepts`],`--max-concepts`)):void 0,a=await ca(e,{json:t}),o=E(e.output);if(r===`okf`){let e=st(a,{maxConcepts:i}),n=o??`./okf`,s=await dc(n,e.files);if(t){L({format:r,output:n,bundle:e,validation:s});return}process.stdout.write(`Wrote OKF bundle to ${n}\n\n`),R([[`Files`,String(e.files.length)],[`Concepts`,String(e.conceptCount)],[`Valid`,s.valid?`yes`:`no`]]);return}if(r===`json`){await fc(o,`${JSON.stringify(mc(a),null,2)}\n`);return}await fc(o,hc(a))}}),_c=C({meta:{name:`monthly`,description:`Export monthly report tables plus the underlying diagnosis detail CSVs.`},args:{...Qs,out:$s(`monthly`),month:{type:`string`,description:`Report month as YYYY-MM. Defaults to the latest month with final GSC data.`},...X([`limit`],{limit:{description:`Maximum rows per diagnostic section. Defaults to 10.`}}),...Zs},run:async({args:e})=>{let t=await V({client:D(e),site:E(e.site),options:{refresh:O(e.refresh)}}),n=await mn({site:t.site,month:E(e.month),limit:k(e.limit),brandTerms:t.client?.brandTerms,...qs(e),progress:Ws(!0)}),r=E(e.out)??tc({report:`monthly`,clientId:t.client?.id,site:t.site});rc(r,await nc(r,Ot(n)))}}),vc=C({meta:{name:`narrative`,description:`Export report narrative tables plus the underlying diagnosis detail CSVs.`},args:{...Qs,out:$s(`narrative`),...X([`days`,`recentDays`],{days:{description:`Diagnosis window length in days. Defaults to 90.`}}),"start-date":{type:`string`,description:`Optional report start date as YYYY-MM-DD.`},"end-date":{type:`string`,description:`Optional report end date as YYYY-MM-DD.`},...X([`limit`],{limit:{description:`Maximum rows per diagnostic section. Defaults to 10.`}}),"change-limit":{type:`string`,description:`Maximum saved changes to measure. Defaults to 5.`},...Zs},run:async({args:e})=>{let t=await V({client:D(e),site:E(e.site),options:{refresh:O(e.refresh)}}),n=await zt({site:t.site,days:k(e.days),recentDays:k(e.recent),startDate:E(e[`start-date`]),endDate:E(e[`end-date`]),limit:k(e.limit),changeLimit:k(e[`change-limit`]),brandTerms:t.client?.brandTerms,...qs(e),progress:Ws(!0)}),r=E(e.out)??tc({report:`narrative`,clientId:t.client?.id,site:t.site});rc(r,await nc(r,Ot(n)))}});function yc(e){return wn({days:A(e.days,`--days`),sitemaps:j(e.sitemap),maxSitemapUrls:A(e[`max-sitemap-urls`],`--max-sitemap-urls`),templateLimit:A(e.limit,`--limit`),minimumTemplateUrls:A(e[`minimum-template-urls`],`--minimum-template-urls`),minimumTemplateShare:A(e[`minimum-template-share`],`--minimum-template-share`),minimumTemplateImpressions:A(e[`minimum-template-impressions`],`--minimum-template-impressions`),crawlSamples:A(e[`crawl-samples`],`--crawl-samples`),inspectSamples:A(e[`inspect-samples`],`--inspect-samples`),brandTerms:j(e[`brand-terms`]),includeBrand:O(e[`include-brand`]),js:O(e.js)?!0:`auto`,refresh:O(e.refresh),rate:di(e)})}const bc=C({meta:{name:`pseo`,description:`Export pSEO audit tables: templates, demand patterns, top queries, sample coverage, crawl samples, and inspection samples.`},args:{...Qs,out:$s(`pseo`),days:{type:`string`,description:`GSC lookback window. Defaults to 28.`},limit:{type:`string`,description:`Maximum pSEO templates to export.`},sitemap:{type:`string`,description:`Comma-separated sitemap URLs. Can be passed once.`},"crawl-samples":{type:`string`,description:`Number of representative URLs to fetch per template. Defaults to 0.`},"inspect-samples":{type:`string`,description:`Number of representative URLs to inspect in GSC per template. Defaults to 0.`},"max-sitemap-urls":{type:`string`,description:`Maximum discovered URLs per sitemap. Defaults to 50000.`},"minimum-template-urls":{type:`string`,description:`Minimum repeated URLs required for a template. Defaults to 3.`},"minimum-template-share":{type:`string`,description:`Minimum share of discovered URLs from 0 to 1. Defaults to 0.`},"minimum-template-impressions":{type:`string`,description:`Minimum retained page impressions for a template. Defaults to 0.`},"brand-terms":{type:`string`,description:`Comma-separated brand terms to exclude from query evidence.`},"include-brand":{type:`boolean`,default:!1,description:`Include branded queries in pSEO demand analysis.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering for crawled samples.`},"fetch-concurrency":{type:`string`,description:`Maximum concurrent page fetches per host. Defaults to 4.`},"fetch-interval-cap":{type:`string`,description:`Maximum page fetches per interval per host. Defaults to 4.`},"fetch-interval-ms":{type:`string`,description:`Fetch rate interval in milliseconds. Defaults to 1000.`},refresh:{type:`boolean`,default:!1,description:`Bypass local cache and fetch fresh data.`}},run:async({args:e})=>{let t=yc(e),n=await V({client:D(e),site:E(e.site)}),r=await Ct({site:n.site,...t,brandTerms:t.brandTerms??n.client?.brandTerms,progress:Ws(!0)}),i=E(e.out)??tc({report:`pseo`,clientId:n.client?.id,site:n.site});rc(i,await nc(i,on(r)))}}),xc=C({meta:{name:`refresh-priorities`,description:`Export the ranked SEO priority queue, score breakdowns, grouped findings, and diagnosis detail CSVs.`},args:{...Qs,out:$s(`refresh-priorities`),...X([`days`,`recentDays`,`limit`,`includeBrand`,`refresh`],{days:{description:`Diagnosis window length in days. Defaults to 90.`},limit:{description:`Maximum queue items to export. Defaults to 25.`}}),"google-analytics-property":{type:`string`,description:`Google Analytics property ID to use for analytics value. Defaults from the selected project.`},"verify-content":ai(`Verify top opportunities against page title, meta, and content. Defaults to true.`,`Skip page content verification.`),"verify-limit":{type:`string`,description:`Maximum opportunity URLs to verify. Defaults to 5.`}},run:async({args:e})=>{let t=await V({client:D(e),site:E(e.site),options:{refresh:O(e.refresh)}}),n=await nt({site:t.site,days:k(e.days),recentDays:k(e.recent),limit:k(e.limit),brandTerms:t.client?.brandTerms,includeBrand:O(e[`include-brand`]),googleAnalyticsPropertyId:E(e[`google-analytics-property`])??t.client?.analytics.google?.propertyId,verifyContent:O(e[`verify-content`]),verifyLimit:k(e[`verify-limit`]),refresh:O(e.refresh)}),r=E(e.out)??tc({report:`refresh-priorities`,clientId:t.client?.id,site:t.site});rc(r,await nc(r,Xt(n)))}}),Sc=C({meta:{name:`export`,description:`Export full report data to CSV files`},subCommands:{diagnose:ic,knowledge:gc,narrative:vc,monthly:_c,"update-postmortem":C({meta:{name:`update-postmortem`,description:`Export update postmortem attribution, findings, template movement, and segment movers.`},args:{...Qs,out:$s(`update-postmortem`),...X([`days`,`recentDays`,`limit`,`includeBrand`,`refresh`],{days:{description:`Diagnosis window length in days. Defaults to 90.`},limit:{description:`Maximum winners/losers per segment. Defaults to 20.`}}),"known-change":{type:`string`,description:`Manual site-side change to treat as a confounder, for example pruning pages or blocking traffic.`},"ignore-change-log":{type:`boolean`,default:!1,description:`Do not use saved change-log entries as confounders.`}},run:async({args:e})=>{let t=await V({client:D(e),site:E(e.site),options:{refresh:O(e.refresh)}}),n=await cn({site:t.site,days:k(e.days),recentDays:k(e.recent),limit:k(e.limit),brandTerms:t.client?.brandTerms,includeBrand:O(e[`include-brand`]),knownConfounders:E(e[`known-change`])?[E(e[`known-change`])??``]:void 0,includeChangeLog:!O(e[`ignore-change-log`]),refresh:O(e.refresh)}),r=E(e.out)??tc({report:`update-postmortem`,clientId:t.client?.id,site:t.site});rc(r,await nc(r,Qr(n)))}}),"refresh-priorities":xc,pseo:bc}}),Cc={project:{type:`string`,description:`Saved project id or name.`},client:{type:`string`,description:`Legacy alias for --project.`},site:{type:`string`,description:`Site URL.`}};async function wc(e){let t=E(e.site);if(t)return t;let n=await H({project:D(e),options:{json:N(e)}});if(!n)throw new y(`INVALID_INPUT`,`Pass --site or select a saved project.`);return n.siteUrl}async function Tc(e){let t=E(e.url),n=j(e.urls)??[],r=E(e.file);if(!t&&n.length===0&&!r)throw new y(`INVALID_INPUT`,`Pass --url, comma-separated --urls, or a newline-delimited --file.`);let i=[...t?[t]:[],...n];if(r){let e=await _r(r).catch(()=>void 0);if(!e)throw new y(`INVALID_INPUT`,`IndexNow URL file not found: ${r}`);if(!e.isFile()||e.size>21e5)throw new y(`INVALID_INPUT`,`IndexNow URL files must be regular files no larger than 2.1 MB.`);i.push(...(await pr(r,`utf8`)).split(/\r?\n/).map(e=>e.trim()).filter(Boolean))}return i}const Ec=C({meta:{name:`indexnow`,description:`Set up and submit changed URLs with IndexNow`},subCommands:{setup:C({meta:{name:`setup`,description:`Generate an IndexNow key file for one site`},args:{...Cc,output:{type:`string`,description:`Public asset directory for the generated key file.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=E(e.output);if(!t)throw new y(`INVALID_INPUT`,`Pass --output with the public asset directory that deploys at the site root.`);let n=Ee({site:await wc(e)}),r=nr(t),i=S(r,`${n.key}.txt`);await dr(r,{recursive:!0});try{await yr(i,`${n.key}\n`,{encoding:`utf8`,flag:`wx`,mode:384})}catch(e){throw e.code===`EEXIST`?new y(`INVALID_INPUT`,`Refusing to overwrite the existing file at ${i}.`):e}let a;try{a=await be(n)}catch(e){throw await vr(i).catch(()=>void 0),e}let o={configured:!0,host:n.host,keyLocation:n.keyLocation,keyFile:i,credentialSource:a,nextStep:`Deploy ${i}, then run seo indexnow verify --site ${new URL(n.keyLocation).origin}`};N(e)?L(o):R([[`Host`,o.host],[`Key file`,o.keyFile],[`Public URL`,o.keyLocation],[`Saved in`,o.credentialSource],[`Next`,o.nextStep]])}}),status:C({meta:{name:`status`,description:`Show saved IndexNow hosts`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Ut(),n={configuredSites:t.length,sites:t};N(e)?L(n):z([`Host`,`Key location`,`Created`],t.map(e=>[e.host,e.keyLocation,e.createdAt]))}}),verify:C({meta:{name:`verify`,description:`Check the public IndexNow key file`},args:{...Cc,json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Bt({site:await wc(e)}),n={...await f({record:t.record}),credentialSource:t.source};N(e)?L(n):R([[`Verified`,n.verified?`yes`:`no`],[`HTTP status`,String(n.status)],[`Key location`,n.keyLocation],[`Credential`,n.credentialSource]]),n.verified||(process.exitCode=4)}}),submit:C({meta:{name:`submit`,description:`Notify IndexNow about changed URLs`},args:{...Cc,url:{type:`string`,description:`One changed URL.`},urls:{type:`string`,description:`Comma-separated changed URLs.`},file:{type:`string`,description:`Newline-delimited changed URL file.`},"dry-run":{type:`boolean`,default:!1,description:`Validate without notifying IndexNow.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Bt({site:await wc(e)}),n=await h({record:t.record,urls:await Tc(e),dryRun:O(e[`dry-run`])}),r={...n,credentialSource:t.source};if(N(e)){L(r);return}R([[`Host`,n.host],[`Status`,n.status],[`URLs`,String(n.submittedUrls)],[`Dry run`,n.dryRun?`yes`:`no`],[`Key location`,n.keyLocation]]);for(let e of n.caveats)process.stdout.write(`\n${e}\n`)}}),remove:C({meta:{name:`remove`,description:`Remove a saved IndexNow key`},args:{...Cc,json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await r(await wc(e)),n={removed:t,note:`The deployed key file was not removed.`};N(e)?L(n):process.stdout.write(`${t?`Saved key removed.`:`No saved key found.`} ${n.note}\n`)}})}}),Dc=C({...So,meta:{name:`init`,description:"Legacy alias for `seo start`."}}),Oc=C({meta:{name:`links`,description:`Review bounded referring-link evidence from DataForSEO, Bing or a file`},args:{project:{type:`string`,description:`Saved project id or name.`},client:{type:`string`,description:`Legacy alias for --project.`},site:{type:`string`,description:`Verified Bing Webmaster site URL.`},provider:{type:`string`,description:`Live link source: dataforseo or bing.`},target:{type:`string`,description:`Domain or absolute page URL for DataForSEO.`},scope:{type:`string`,description:`DataForSEO target scope: domain or page.`},"include-subdomains":{type:`boolean`,default:!0,description:`Include subdomains for a DataForSEO domain target.`},"search-site":{type:`string`,description:`Search Console property used to add target-page search evidence.`},file:{type:`string`,description:`Local CSV, JSON, JSONL, or NDJSON link export.`},format:{type:`string`,description:`Import format override: csv, json, or jsonl.`},"row-limit":{type:`string`,description:`Maximum source rows. Defaults: 100 DataForSEO, 500 Bing, 10000 files.`},"target-limit":{type:`string`,description:`Maximum Bing target pages to inspect. Defaults to 20.`},"detail-pages":{type:`string`,description:`Maximum Bing result pages per target. Defaults to 1.`},limit:{type:`string`,description:`Maximum link rows returned. Defaults to 100.`},days:{type:`string`,description:`Search Console lookback window for linked target pages. Defaults to 90.`},refresh:{type:`boolean`,default:!1,description:`Bypass local provider and Search Console caches.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=N(e),n=E(e.file),r=E(e.format),i=E(e.provider);if(i&&![`dataforseo`,`bing`].includes(i))throw new y(`INVALID_INPUT`,`--provider must be dataforseo or bing.`);if(n&&i)throw new y(`INVALID_INPUT`,`Do not pass --provider with a local link file.`);let a=E(e.scope);if(a&&a!==`domain`&&a!==`page`)throw new y(`INVALID_INPUT`,`--scope must be domain or page.`);if(r&&![`csv`,`json`,`jsonl`].includes(r))throw new y(`INVALID_INPUT`,`--format must be csv, json, or jsonl.`);let o=A(e[`row-limit`],`--row-limit`),s=E(e.target),c=E(e.site);if(n&&(c||s))throw new y(`INVALID_INPUT`,`Pass one link source: --file, --site for Bing, or --target for DataForSEO.`);if(c&&s)throw new y(`INVALID_INPUT`,`Pass --site for Bing or --target for DataForSEO, not both.`);if(s&&i===`bing`)throw new y(`INVALID_INPUT`,`Use --site with Bing or --target with DataForSEO.`);if(c&&i===`dataforseo`)throw new y(`INVALID_INPUT`,`Use --target with DataForSEO or --site with Bing.`);let u=D(e),d=u?await H({project:u,options:{json:t}}):void 0;!n&&!d&&!s&&(d=await H({options:{json:t}}));let f;if(n)f=await Re({file:n,format:r,rowLimit:o});else if(i===`dataforseo`||s){let t=s??d?.startUrl??d?.siteUrl.replace(/^sc-domain:/u,``);if(!t)throw new y(`INVALID_INPUT`,`Pass --target or use a saved project with a crawl URL or Search Console property.`);f=await l({target:t,scope:a,includeSubdomains:O(e[`include-subdomains`]),rowLimit:o,refresh:O(e.refresh),context:{projectId:d?.id,reportId:`link-evidence`,reportRunId:ir()}})}else{let t=c??ke(d);if(!t)throw new y(`INVALID_INPUT`,`Pass --site, connect Bing to a saved project, or import a link file with --file.`);f=await Ve({site:t,rowLimit:o,targetLimit:A(e[`target-limit`],`--target-limit`),detailPagesPerTarget:A(e[`detail-pages`],`--detail-pages`)})}let p=E(e[`search-site`])??d?.siteUrl,m=d||p||f.provenance.provider===`dataforseo`?await On({evidence:f,searchConsoleSite:p,crawlSite:d?.siteUrl??p??(f.provenance.provider===`dataforseo`?f.externalProvider?.summary.data.target:void 0),days:A(e.days,`--days`),refresh:O(e.refresh)}):void 0,h=jn({evidence:f,limit:A(e.limit,`--limit`),targetContext:m});if(t){L(h);return}let ee=h.providerEvidence?[h.providerEvidence.summary.cost.actualMicros,h.providerEvidence.backlinks.cost.actualMicros]:[],g=ee.every(e=>e!==null)?ee.reduce((e,t)=>e+t,0):null,_=h.providerEvidence?[h.providerEvidence.summary.cache.status,h.providerEvidence.backlinks.cache.status].every(e=>e===`hit`):!1;K({title:`Inbound link evidence`,target:h.provenance.provider,status:h.dataStatus===`complete`?`info`:`unknown`,summary:`${U(h.summary.observedLinks)} retained links from ${U(h.summary.referringDomains)} referring domains.`,metrics:[{label:`Evidence`,value:h.dataStatus},{label:`Observed links`,value:U(h.summary.observedLinks)},{label:`Referring domains`,value:U(h.summary.referringDomains)},{label:`Target pages`,value:U(h.summary.targetPages)},{label:`Returned rows`,value:`${U(h.selection.returnedRows)} of ${U(h.selection.availableRows)}`},...h.providerSummary?.backlinks.state===`observed`?[{label:`Provider backlinks`,value:U(h.providerSummary.backlinks.value)}]:[],...h.providerSummary?.referringDomains.state===`observed`?[{label:`Provider domains`,value:U(h.providerSummary.referringDomains.value)}]:[],...h.providerEvidence?[{label:`Provider cost`,value:g===null?`Unknown`:`$${(g/1e6).toFixed(4)}${_?` (cached)`:``}`}]:[],...h.providerSummary?.brokenBacklinks.state===`observed`&&h.providerSummary.brokenBacklinks.value>0?[{label:`Provider broken links`,value:U(h.providerSummary.brokenBacklinks.value)}]:[]]}),h.findings.length&&q([`Priority`,`Finding`,`Target`],h.findings.map(e=>[e.priority,e.code,G(e.targetUrl,72)])),h.links.length&&q([`Source`,`Target`,`Anchor`],h.links.map(e=>[G(e.sourceUrl,58),G(e.targetUrl,58),G(e.anchorText??``,36)])),Y(`Warnings`,h.warnings),Y(`Report caveats`,h.caveats)}});async function kc(e,t){if(!e){process.stdout.write(t);return}await dr(x(e),{recursive:!0}),await yr(e,t),process.stdout.write(`Wrote ${e}\n`)}const Ac={"report-id":{type:`string`,description:`Saved crawl report id to use.`},site:{type:`string`,description:`GSC property URL for selecting the latest saved crawl.`},client:{type:`string`,description:`Legacy alias for --project.`},project:{type:`string`,description:`Saved project id or name.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},jc=C({meta:{name:`llms`,description:`Inspect or generate optional llms.txt from saved crawl data`},subCommands:{audit:C({meta:{name:`audit`,description:`Inspect optional llms.txt presence from a saved crawl`},args:Ac,run:async({args:e})=>{let t=N(e),n=He(await ca(e,{json:t}));if(t){L(n);return}K({title:`llms.txt audit`,target:n.url,status:n.issues.length>0?`warning`:`pass`,summary:n.headline,metrics:[{label:`SEO impact`,value:n.googleSearchImpact},{label:`Optional`,value:n.optional?`Yes`:`No`},{label:`Found`,value:n.exists?`Yes`:`No`},{label:`URL`,value:n.llmsTxtUrl},{label:`HTTP status`,value:n.status?String(n.status):`Unavailable`}],diagnostics:[{title:`Issues`,items:n.issues.map(e=>({status:e.severity===`high`?`fail`:`warning`,title:e.title,explanation:e.plainEnglish,fix:e.action}))}]}),Y(`Recommended pages`,n.recommendedPages.slice(0,10).map(e=>`${e.section}: ${e.url}`))}}),generate:C({meta:{name:`generate`,description:`Generate an llms.txt draft from a saved crawl`},args:{...Ac,output:{type:`string`,description:`Write llms.txt to this path instead of stdout.`},"max-urls":{type:`string`,description:`Maximum URLs to include. Defaults to 250.`},"token-budget":{type:`string`,description:`Approximate token budget. Defaults to 12000.`},exclude:{type:`string`,description:`Comma-separated URL patterns to exclude.`},title:{type:`string`,description:`Override the llms.txt title.`},description:{type:`string`,description:`Override the llms.txt description.`}},run:async({args:e})=>{let t=N(e),n=ee(await ca(e,{json:t}),{maxUrls:k(e[`max-urls`]),tokenBudget:k(e[`token-budget`]),exclude:j(e.exclude),title:E(e.title),description:E(e.description)});if(t){L(n);return}await kc(E(e.output),n.content),E(e.output)&&R([[`URLs`,String(n.includedUrls)],[`Estimated tokens`,String(n.estimatedTokens)]])}})}}),Mc=C({meta:{name:`logs`,description:`Manage local log retention`},subCommands:{prune:C({meta:{name:`prune`,description:`Rotate large logs and remove old local logs`},args:{quiet:{type:`boolean`,default:!1,description:`Do not print a result.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=ht();if(N(e)){L(t);return}e.quiet||process.stdout.write(`Rotated ${t.rotated} and removed ${t.removed} log files. ${ra(t.sizeBytes)} retained.\n`)}})}}),Nc=[{client:`claude-desktop`,flag:`claude-desktop`},{client:`claude-code`,flag:`claude-code`},{client:`codex`,flag:`codex`},{client:`cursor`,flag:`cursor`}];function Pc(e,t){return O(e.all)?t:Nc.filter(({flag:t})=>O(e[t])).map(({client:e})=>e)}const Fc=C({meta:{name:`mcp`,description:`MCP server helpers`},subCommands:{serve:C({meta:{name:`serve`,description:`Run the SEO MCP server`},args:{test:{type:`boolean`,default:!1,description:`Start in test mode`}},run:async({args:e})=>{let{startMcpServer:t}=await import(`./dist-BITHJak_.js`);await t({test:O(e.test)})}}),install:C({meta:{name:`install`,description:`Install or remove SEO MCP config for local clients`},args:{uninstall:{type:`boolean`,default:!1,description:`Remove the SEO MCP config instead of adding it`},reinstall:{type:`boolean`,default:!1,description:`Rewrite an existing managed SEO MCP config`},"claude-desktop":{type:`boolean`,default:!1,description:`Update Claude Desktop`},"claude-code":{type:`boolean`,default:!1,description:`Update Claude Code`},codex:{type:`boolean`,default:!1,description:`Update Codex CLI, app, and IDE extension`},cursor:{type:`boolean`,default:!1,description:`Update Cursor`},all:{type:`boolean`,default:!1,description:`Update every detected MCP client`},json:{type:`boolean`,default:!1,description:`Print structured JSON output`}},run:async({args:e})=>{if(O(e.uninstall)&&O(e.reinstall))throw new y(`INVALID_INPUT`,`Use either --uninstall or --reinstall, not both.`);let t=La(),n=Ba({targets:t}),r=N(e),i=Pc(e,n.map(e=>e.client));if(i.length===0){if(!I({json:r}))throw new y(`INVALID_INPUT`,`Choose an MCP client with --claude-desktop, --claude-code, --codex, --cursor, or --all.`);let a=n.length>0?n:t;i=B(await Dr({message:e.uninstall?`Remove seo from which MCP clients?`:`Install seo into which MCP clients?`,options:a.map(e=>({value:e.client,label:e.label,hint:e.path})),initialValues:n.map(e=>e.client),required:!0}))}let a=i.filter(e=>!t.some(t=>t.client===e));if(a.length>0)throw new y(`INVALID_INPUT`,`MCP setup is not supported for ${a.join(`, `)} on ${process.platform}.`);let o=t.filter(e=>i.includes(e.client)).map(t=>O(e.uninstall)?ao(t):io(t,{reinstall:O(e.reinstall)}));if(r){L({operation:O(e.uninstall)?`uninstall`:`install`,results:o});return}for(let e of o)process.stdout.write(`${e.changed?`updated`:`skipped`} ${e.client} · ${e.path}\n`)}})}}),Ic=C({meta:{name:`crawl-diff`,description:`Crawl a site and diff technical/page changes vs last run`},args:{url:{type:`string`,required:!0,description:`Start URL to crawl.`},site:{type:`string`,description:`Optional GSC property to associate with the crawl.`},limit:{type:`string`,description:`Maximum pages to crawl. Defaults to 50.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`},refresh:{type:`boolean`,default:!1,description:`Bypass local HTTP cache.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering when Playwright is installed.`}},run:async({args:e})=>{let t=await _t({startUrl:E(e.url)??``,site:E(e.site),limit:k(e.limit),refresh:O(e.refresh),js:O(e.js)?!0:`auto`});if(N(e)){L(t);return}if(K({title:`Crawl history comparison`,target:t.run.startUrl,status:t.previousRun===void 0?`unknown`:t.summary.newErrors>0||t.summary.indexabilityFlips>0?`warning`:`pass`,summary:t.previousRun===void 0?`No previous crawl was available for comparison.`:`${t.summary.changed} changed, ${t.summary.added} added, and ${t.summary.removed} removed URLs.`,metrics:[{label:`Run`,value:t.run.id},{label:`Crawled`,value:t.summary.crawled},{label:`Previous run`,value:t.previousRun?.id??`None`},{label:`Added`,value:t.summary.added},{label:`Removed`,value:t.summary.removed},{label:`Changed`,value:t.summary.changed},{label:`New errors`,value:t.summary.newErrors},{label:`Indexability flips`,value:t.summary.indexabilityFlips},{label:`High priority`,value:t.summary.highPriorityRecommendations}]}),t.recommendations.length&&(process.stdout.write(`
171
+ `)}\n`}const gc=C({meta:{name:`knowledge`,description:`Export site knowledge from a saved crawl as OKF, Markdown, or JSON`},args:{"report-id":{type:`string`,description:`Saved crawl report id to export.`},site:{type:`string`,description:`GSC property URL for selecting the latest saved crawl.`},client:{type:`string`,description:`Legacy alias for --project.`},project:{type:`string`,description:`Saved project id or name.`},format:{type:`string`,description:`Output format: okf, markdown, or json. Defaults to okf.`},output:{type:`string`,description:`Output directory for OKF, or output file for Markdown/JSON.`},"max-concepts":{type:`string`,description:`Maximum OKF concept files. Defaults to 500.`},json:{type:`boolean`,default:!1,description:`Print command metadata as JSON.`}},run:async({args:e})=>{let t=N(e),r=pc(e.format),i=r===`okf`?n(A(e[`max-concepts`],`--max-concepts`)):void 0,a=await ca(e,{json:t}),o=E(e.output);if(r===`okf`){let e=st(a,{maxConcepts:i}),n=o??`./okf`,s=await dc(n,e.files);if(t){L({format:r,output:n,bundle:e,validation:s});return}process.stdout.write(`Wrote OKF bundle to ${n}\n\n`),R([[`Files`,String(e.files.length)],[`Concepts`,String(e.conceptCount)],[`Valid`,s.valid?`yes`:`no`]]);return}if(r===`json`){await fc(o,`${JSON.stringify(mc(a),null,2)}\n`);return}await fc(o,hc(a))}}),_c=C({meta:{name:`monthly`,description:`Export monthly report tables plus the underlying diagnosis detail CSVs.`},args:{...Qs,out:$s(`monthly`),month:{type:`string`,description:`Report month as YYYY-MM. Defaults to the latest month with final GSC data.`},...X([`limit`],{limit:{description:`Maximum rows per diagnostic section. Defaults to 10.`}}),...Zs},run:async({args:e})=>{let t=await V({client:D(e),site:E(e.site),options:{refresh:O(e.refresh)}}),n=await mn({site:t.site,month:E(e.month),limit:k(e.limit),brandTerms:t.client?.brandTerms,...qs(e),progress:Ws(!0)}),r=E(e.out)??tc({report:`monthly`,clientId:t.client?.id,site:t.site});rc(r,await nc(r,Ot(n)))}}),vc=C({meta:{name:`narrative`,description:`Export report narrative tables plus the underlying diagnosis detail CSVs.`},args:{...Qs,out:$s(`narrative`),...X([`days`,`recentDays`],{days:{description:`Diagnosis window length in days. Defaults to 90.`}}),"start-date":{type:`string`,description:`Optional report start date as YYYY-MM-DD.`},"end-date":{type:`string`,description:`Optional report end date as YYYY-MM-DD.`},...X([`limit`],{limit:{description:`Maximum rows per diagnostic section. Defaults to 10.`}}),"change-limit":{type:`string`,description:`Maximum saved changes to measure. Defaults to 5.`},...Zs},run:async({args:e})=>{let t=await V({client:D(e),site:E(e.site),options:{refresh:O(e.refresh)}}),n=await zt({site:t.site,days:k(e.days),recentDays:k(e.recent),startDate:E(e[`start-date`]),endDate:E(e[`end-date`]),limit:k(e.limit),changeLimit:k(e[`change-limit`]),brandTerms:t.client?.brandTerms,...qs(e),progress:Ws(!0)}),r=E(e.out)??tc({report:`narrative`,clientId:t.client?.id,site:t.site});rc(r,await nc(r,Ot(n)))}});function yc(e){return wn({days:A(e.days,`--days`),sitemaps:j(e.sitemap),maxSitemapUrls:A(e[`max-sitemap-urls`],`--max-sitemap-urls`),templateLimit:A(e.limit,`--limit`),minimumTemplateUrls:A(e[`minimum-template-urls`],`--minimum-template-urls`),minimumTemplateShare:A(e[`minimum-template-share`],`--minimum-template-share`),minimumTemplateImpressions:A(e[`minimum-template-impressions`],`--minimum-template-impressions`),crawlSamples:A(e[`crawl-samples`],`--crawl-samples`),inspectSamples:A(e[`inspect-samples`],`--inspect-samples`),brandTerms:j(e[`brand-terms`]),includeBrand:O(e[`include-brand`]),js:O(e.js)?!0:`auto`,refresh:O(e.refresh),rate:di(e)})}const bc=C({meta:{name:`pseo`,description:`Export pSEO audit tables: templates, demand patterns, top queries, sample coverage, crawl samples, and inspection samples.`},args:{...Qs,out:$s(`pseo`),days:{type:`string`,description:`GSC lookback window. Defaults to 28.`},limit:{type:`string`,description:`Maximum pSEO templates to export.`},sitemap:{type:`string`,description:`Comma-separated sitemap URLs. Can be passed once.`},"crawl-samples":{type:`string`,description:`Number of representative URLs to fetch per template. Defaults to 0.`},"inspect-samples":{type:`string`,description:`Number of representative URLs to inspect in GSC per template. Defaults to 0.`},"max-sitemap-urls":{type:`string`,description:`Maximum discovered URLs per sitemap. Defaults to 50000.`},"minimum-template-urls":{type:`string`,description:`Minimum repeated URLs required for a template. Defaults to 3.`},"minimum-template-share":{type:`string`,description:`Minimum share of discovered URLs from 0 to 1. Defaults to 0.`},"minimum-template-impressions":{type:`string`,description:`Minimum retained page impressions for a template. Defaults to 0.`},"brand-terms":{type:`string`,description:`Comma-separated brand terms to exclude from query evidence.`},"include-brand":{type:`boolean`,default:!1,description:`Include branded queries in pSEO demand analysis.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering for crawled samples.`},"fetch-concurrency":{type:`string`,description:`Maximum concurrent page fetches per host. Defaults to 4.`},"fetch-interval-cap":{type:`string`,description:`Maximum page fetches per interval per host. Defaults to 4.`},"fetch-interval-ms":{type:`string`,description:`Fetch rate interval in milliseconds. Defaults to 1000.`},refresh:{type:`boolean`,default:!1,description:`Bypass local cache and fetch fresh data.`}},run:async({args:e})=>{let t=yc(e),n=await V({client:D(e),site:E(e.site)}),r=await Ct({site:n.site,...t,brandTerms:t.brandTerms??n.client?.brandTerms,progress:Ws(!0)}),i=E(e.out)??tc({report:`pseo`,clientId:n.client?.id,site:n.site});rc(i,await nc(i,on(r)))}}),xc=C({meta:{name:`refresh-priorities`,description:`Export the ranked SEO priority queue, score breakdowns, grouped findings, and diagnosis detail CSVs.`},args:{...Qs,out:$s(`refresh-priorities`),...X([`days`,`recentDays`,`limit`,`includeBrand`,`refresh`],{days:{description:`Diagnosis window length in days. Defaults to 90.`},limit:{description:`Maximum queue items to export. Defaults to 25.`}}),"google-analytics-property":{type:`string`,description:`Google Analytics property ID to use for analytics value. Defaults from the selected project.`},"verify-content":ai(`Verify top opportunities against page title, meta, and content. Defaults to true.`,`Skip page content verification.`),"verify-limit":{type:`string`,description:`Maximum opportunity URLs to verify. Defaults to 5.`}},run:async({args:e})=>{let t=await V({client:D(e),site:E(e.site),options:{refresh:O(e.refresh)}}),n=await nt({site:t.site,days:k(e.days),recentDays:k(e.recent),limit:k(e.limit),brandTerms:t.client?.brandTerms,includeBrand:O(e[`include-brand`]),googleAnalyticsPropertyId:E(e[`google-analytics-property`])??t.client?.analytics.google?.propertyId,verifyContent:O(e[`verify-content`]),verifyLimit:k(e[`verify-limit`]),refresh:O(e.refresh)}),r=E(e.out)??tc({report:`refresh-priorities`,clientId:t.client?.id,site:t.site});rc(r,await nc(r,Xt(n)))}}),Sc=C({meta:{name:`export`,description:`Export full report data to CSV files`},subCommands:{diagnose:ic,knowledge:gc,narrative:vc,monthly:_c,"update-postmortem":C({meta:{name:`update-postmortem`,description:`Export update postmortem attribution, findings, template movement, and segment movers.`},args:{...Qs,out:$s(`update-postmortem`),...X([`days`,`recentDays`,`limit`,`includeBrand`,`refresh`],{days:{description:`Diagnosis window length in days. Defaults to 90.`},limit:{description:`Maximum winners/losers per segment. Defaults to 20.`}}),"known-change":{type:`string`,description:`Manual site-side change to treat as a confounder, for example pruning pages or blocking traffic.`},"ignore-change-log":{type:`boolean`,default:!1,description:`Do not use saved change-log entries as confounders.`}},run:async({args:e})=>{let t=await V({client:D(e),site:E(e.site),options:{refresh:O(e.refresh)}}),n=await cn({site:t.site,days:k(e.days),recentDays:k(e.recent),limit:k(e.limit),brandTerms:t.client?.brandTerms,includeBrand:O(e[`include-brand`]),knownConfounders:E(e[`known-change`])?[E(e[`known-change`])??``]:void 0,includeChangeLog:!O(e[`ignore-change-log`]),refresh:O(e.refresh)}),r=E(e.out)??tc({report:`update-postmortem`,clientId:t.client?.id,site:t.site});rc(r,await nc(r,Qr(n)))}}),"refresh-priorities":xc,pseo:bc}}),Cc={project:{type:`string`,description:`Saved project id or name.`},client:{type:`string`,description:`Legacy alias for --project.`},site:{type:`string`,description:`Site URL.`}};async function wc(e){let t=E(e.site);if(t)return t;let n=await H({project:D(e),options:{json:N(e)}});if(!n)throw new y(`INVALID_INPUT`,`Pass --site or select a saved project.`);return n.siteUrl}async function Tc(e){let t=E(e.url),n=j(e.urls)??[],r=E(e.file);if(!t&&n.length===0&&!r)throw new y(`INVALID_INPUT`,`Pass --url, comma-separated --urls, or a newline-delimited --file.`);let i=[...t?[t]:[],...n];if(r){let e=await _r(r).catch(()=>void 0);if(!e)throw new y(`INVALID_INPUT`,`IndexNow URL file not found: ${r}`);if(!e.isFile()||e.size>21e5)throw new y(`INVALID_INPUT`,`IndexNow URL files must be regular files no larger than 2.1 MB.`);i.push(...(await pr(r,`utf8`)).split(/\r?\n/).map(e=>e.trim()).filter(Boolean))}return i}const Ec=C({meta:{name:`indexnow`,description:`Set up and submit changed URLs with IndexNow`},subCommands:{setup:C({meta:{name:`setup`,description:`Generate an IndexNow key file for one site`},args:{...Cc,output:{type:`string`,description:`Public asset directory for the generated key file.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=E(e.output);if(!t)throw new y(`INVALID_INPUT`,`Pass --output with the public asset directory that deploys at the site root.`);let n=Ee({site:await wc(e)}),r=nr(t),i=S(r,`${n.key}.txt`);await dr(r,{recursive:!0});try{await yr(i,`${n.key}\n`,{encoding:`utf8`,flag:`wx`,mode:384})}catch(e){throw e.code===`EEXIST`?new y(`INVALID_INPUT`,`Refusing to overwrite the existing file at ${i}.`):e}let a;try{a=await be(n)}catch(e){throw await vr(i).catch(()=>void 0),e}let o={configured:!0,host:n.host,keyLocation:n.keyLocation,keyFile:i,credentialSource:a,nextStep:`Deploy ${i}, then run seo indexnow verify --site ${new URL(n.keyLocation).origin}`};N(e)?L(o):R([[`Host`,o.host],[`Key file`,o.keyFile],[`Public URL`,o.keyLocation],[`Saved in`,o.credentialSource],[`Next`,o.nextStep]])}}),status:C({meta:{name:`status`,description:`Show saved IndexNow hosts`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Ut(),n={configuredSites:t.length,sites:t};N(e)?L(n):z([`Host`,`Key location`,`Created`],t.map(e=>[e.host,e.keyLocation,e.createdAt]))}}),verify:C({meta:{name:`verify`,description:`Check the public IndexNow key file`},args:{...Cc,json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Bt({site:await wc(e)}),n={...await f({record:t.record}),credentialSource:t.source};N(e)?L(n):R([[`Verified`,n.verified?`yes`:`no`],[`HTTP status`,String(n.status)],[`Key location`,n.keyLocation],[`Credential`,n.credentialSource]]),n.verified||(process.exitCode=4)}}),submit:C({meta:{name:`submit`,description:`Notify IndexNow about changed URLs`},args:{...Cc,url:{type:`string`,description:`One changed URL.`},urls:{type:`string`,description:`Comma-separated changed URLs.`},file:{type:`string`,description:`Newline-delimited changed URL file.`},"dry-run":{type:`boolean`,default:!1,description:`Validate without notifying IndexNow.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Bt({site:await wc(e)}),n=await h({record:t.record,urls:await Tc(e),dryRun:O(e[`dry-run`])}),r={...n,credentialSource:t.source};if(N(e)){L(r);return}R([[`Host`,n.host],[`Status`,n.status],[`URLs`,String(n.submittedUrls)],[`Dry run`,n.dryRun?`yes`:`no`],[`Key location`,n.keyLocation]]);for(let e of n.caveats)process.stdout.write(`\n${e}\n`)}}),remove:C({meta:{name:`remove`,description:`Remove a saved IndexNow key`},args:{...Cc,json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await r(await wc(e)),n={removed:t,note:`The deployed key file was not removed.`};N(e)?L(n):process.stdout.write(`${t?`Saved key removed.`:`No saved key found.`} ${n.note}\n`)}})}}),Dc=C({...So,meta:{name:`init`,description:"Legacy alias for `seo start`."}}),Oc=C({meta:{name:`links`,description:`Review bounded referring-link evidence from DataForSEO, Bing or a file`},args:{project:{type:`string`,description:`Saved project id or name.`},client:{type:`string`,description:`Legacy alias for --project.`},site:{type:`string`,description:`Verified Bing Webmaster site URL.`},provider:{type:`string`,description:`Live link source: dataforseo or bing.`},target:{type:`string`,description:`Domain or absolute page URL for DataForSEO.`},scope:{type:`string`,description:`DataForSEO target scope: domain or page.`},"include-subdomains":{type:`boolean`,default:!0,description:`Include subdomains for a DataForSEO domain target.`},"search-site":{type:`string`,description:`Search Console property used to add target-page search evidence.`},file:{type:`string`,description:`Local CSV, JSON, JSONL, or NDJSON link export.`},format:{type:`string`,description:`Import format override: csv, json, or jsonl.`},"row-limit":{type:`string`,description:`Maximum source rows. Defaults: 100 DataForSEO, 500 Bing, 10000 files.`},"target-limit":{type:`string`,description:`Maximum Bing target pages to inspect. Defaults to 20.`},"detail-pages":{type:`string`,description:`Maximum Bing result pages per target. Defaults to 1.`},limit:{type:`string`,description:`Maximum link rows returned. Defaults to 100.`},days:{type:`string`,description:`Search Console lookback window for linked target pages. Defaults to 90.`},refresh:{type:`boolean`,default:!1,description:`Bypass local provider and Search Console caches.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=N(e),n=E(e.file),r=E(e.format),i=E(e.provider);if(i&&![`dataforseo`,`bing`].includes(i))throw new y(`INVALID_INPUT`,`--provider must be dataforseo or bing.`);if(n&&i)throw new y(`INVALID_INPUT`,`Do not pass --provider with a local link file.`);let a=E(e.scope);if(a&&a!==`domain`&&a!==`page`)throw new y(`INVALID_INPUT`,`--scope must be domain or page.`);if(r&&![`csv`,`json`,`jsonl`].includes(r))throw new y(`INVALID_INPUT`,`--format must be csv, json, or jsonl.`);let o=A(e[`row-limit`],`--row-limit`),s=E(e.target),c=E(e.site);if(n&&(c||s))throw new y(`INVALID_INPUT`,`Pass one link source: --file, --site for Bing, or --target for DataForSEO.`);if(c&&s)throw new y(`INVALID_INPUT`,`Pass --site for Bing or --target for DataForSEO, not both.`);if(s&&i===`bing`)throw new y(`INVALID_INPUT`,`Use --site with Bing or --target with DataForSEO.`);if(c&&i===`dataforseo`)throw new y(`INVALID_INPUT`,`Use --target with DataForSEO or --site with Bing.`);let u=D(e),d=u?await H({project:u,options:{json:t}}):void 0;!n&&!d&&!s&&(d=await H({options:{json:t}}));let f;if(n)f=await Re({file:n,format:r,rowLimit:o});else if(i===`dataforseo`||s){let t=s??d?.startUrl??d?.siteUrl.replace(/^sc-domain:/u,``);if(!t)throw new y(`INVALID_INPUT`,`Pass --target or use a saved project with a crawl URL or Search Console property.`);f=await l({target:t,scope:a,includeSubdomains:O(e[`include-subdomains`]),rowLimit:o,refresh:O(e.refresh),context:{projectId:d?.id,reportId:`link-evidence`,reportRunId:ir()}})}else{let t=c??ke(d);if(!t)throw new y(`INVALID_INPUT`,`Pass --site, connect Bing to a saved project, or import a link file with --file.`);f=await Ve({site:t,rowLimit:o,targetLimit:A(e[`target-limit`],`--target-limit`),detailPagesPerTarget:A(e[`detail-pages`],`--detail-pages`)})}let p=E(e[`search-site`])??d?.siteUrl,m=d||p||f.provenance.provider===`dataforseo`?await On({evidence:f,searchConsoleSite:p,crawlSite:d?.siteUrl??p??(f.provenance.provider===`dataforseo`?f.externalProvider?.summary.data.target:void 0),days:A(e.days,`--days`),refresh:O(e.refresh)}):void 0,h=jn({evidence:f,limit:A(e.limit,`--limit`),targetContext:m});if(t){L(h);return}let ee=h.providerEvidence?[h.providerEvidence.summary.cost.actualMicros,h.providerEvidence.backlinks.cost.actualMicros]:[],g=ee.every(e=>e!==null)?ee.reduce((e,t)=>e+t,0):null,_=h.providerEvidence?[h.providerEvidence.summary.cache.status,h.providerEvidence.backlinks.cache.status].every(e=>e===`hit`):!1;K({title:`Inbound link evidence`,target:h.provenance.provider,status:h.dataStatus===`complete`?`info`:`unknown`,summary:`${U(h.summary.observedLinks)} retained links from ${U(h.summary.referringDomains)} referring domains.`,metrics:[{label:`Evidence`,value:h.dataStatus},{label:`Observed links`,value:U(h.summary.observedLinks)},{label:`Referring domains`,value:U(h.summary.referringDomains)},{label:`Target pages`,value:U(h.summary.targetPages)},{label:`Returned rows`,value:`${U(h.selection.returnedRows)} of ${U(h.selection.availableRows)}`},...h.providerSummary?.backlinks.state===`observed`?[{label:`Provider backlinks`,value:U(h.providerSummary.backlinks.value)}]:[],...h.providerSummary?.referringDomains.state===`observed`?[{label:`Provider domains`,value:U(h.providerSummary.referringDomains.value)}]:[],...h.providerEvidence?[{label:`Provider cost`,value:g===null?`Unknown`:`$${(g/1e6).toFixed(4)}${_?` (cached)`:``}`}]:[],...h.providerSummary?.brokenBacklinks.state===`observed`&&h.providerSummary.brokenBacklinks.value>0?[{label:`Provider broken links`,value:U(h.providerSummary.brokenBacklinks.value)}]:[]]}),h.findings.length&&q([`Priority`,`Finding`,`Target`],h.findings.map(e=>[e.priority,e.code,G(e.targetUrl,72)])),h.links.length&&q([`Source`,`Target`,`Anchor`],h.links.map(e=>[G(e.sourceUrl,58),G(e.targetUrl,58),G(e.anchorText??``,36)])),Y(`Warnings`,h.warnings),Y(`Report caveats`,h.caveats)}});async function kc(e,t){if(!e){process.stdout.write(t);return}await dr(x(e),{recursive:!0}),await yr(e,t),process.stdout.write(`Wrote ${e}\n`)}const Ac={"report-id":{type:`string`,description:`Saved crawl report id to use.`},site:{type:`string`,description:`GSC property URL for selecting the latest saved crawl.`},client:{type:`string`,description:`Legacy alias for --project.`},project:{type:`string`,description:`Saved project id or name.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},jc=C({meta:{name:`llms`,description:`Inspect or generate optional llms.txt from saved crawl data`},subCommands:{audit:C({meta:{name:`audit`,description:`Inspect optional llms.txt presence from a saved crawl`},args:Ac,run:async({args:e})=>{let t=N(e),n=He(await ca(e,{json:t}));if(t){L(n);return}K({title:`llms.txt audit`,target:n.url,status:n.issues.length>0?`warning`:`pass`,summary:n.headline,metrics:[{label:`SEO impact`,value:n.googleSearchImpact},{label:`Optional`,value:n.optional?`Yes`:`No`},{label:`Found`,value:n.exists?`Yes`:`No`},{label:`URL`,value:n.llmsTxtUrl},{label:`HTTP status`,value:n.status?String(n.status):`Unavailable`}],diagnostics:[{title:`Issues`,items:n.issues.map(e=>({status:e.severity===`high`?`fail`:`warning`,title:e.title,explanation:e.plainEnglish,fix:e.action}))}]}),Y(`Recommended pages`,n.recommendedPages.slice(0,10).map(e=>`${e.section}: ${e.url}`))}}),generate:C({meta:{name:`generate`,description:`Generate an llms.txt draft from a saved crawl`},args:{...Ac,output:{type:`string`,description:`Write llms.txt to this path instead of stdout.`},"max-urls":{type:`string`,description:`Maximum URLs to include. Defaults to 250.`},"token-budget":{type:`string`,description:`Approximate token budget. Defaults to 12000.`},exclude:{type:`string`,description:`Comma-separated URL patterns to exclude.`},title:{type:`string`,description:`Override the llms.txt title.`},description:{type:`string`,description:`Override the llms.txt description.`}},run:async({args:e})=>{let t=N(e),n=ee(await ca(e,{json:t}),{maxUrls:k(e[`max-urls`]),tokenBudget:k(e[`token-budget`]),exclude:j(e.exclude),title:E(e.title),description:E(e.description)});if(t){L(n);return}await kc(E(e.output),n.content),E(e.output)&&R([[`URLs`,String(n.includedUrls)],[`Estimated tokens`,String(n.estimatedTokens)]])}})}}),Mc=C({meta:{name:`logs`,description:`Manage local log retention`},subCommands:{prune:C({meta:{name:`prune`,description:`Rotate large logs and remove old local logs`},args:{quiet:{type:`boolean`,default:!1,description:`Do not print a result.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=ht();if(N(e)){L(t);return}e.quiet||process.stdout.write(`Rotated ${t.rotated} and removed ${t.removed} log files. ${ra(t.sizeBytes)} retained.\n`)}})}}),Nc=[{client:`claude-desktop`,flag:`claude-desktop`},{client:`claude-code`,flag:`claude-code`},{client:`codex`,flag:`codex`},{client:`cursor`,flag:`cursor`}];function Pc(e,t){return O(e.all)?t:Nc.filter(({flag:t})=>O(e[t])).map(({client:e})=>e)}const Fc=C({meta:{name:`mcp`,description:`MCP server helpers`},subCommands:{serve:C({meta:{name:`serve`,description:`Run the SEO MCP server`},args:{test:{type:`boolean`,default:!1,description:`Start in test mode`}},run:async({args:e})=>{let{startMcpServer:t}=await import(`./dist-DhQXrgg2.js`);await t({test:O(e.test)})}}),install:C({meta:{name:`install`,description:`Install or remove SEO MCP config for local clients`},args:{uninstall:{type:`boolean`,default:!1,description:`Remove the SEO MCP config instead of adding it`},reinstall:{type:`boolean`,default:!1,description:`Rewrite an existing managed SEO MCP config`},"claude-desktop":{type:`boolean`,default:!1,description:`Update Claude Desktop`},"claude-code":{type:`boolean`,default:!1,description:`Update Claude Code`},codex:{type:`boolean`,default:!1,description:`Update Codex CLI, app, and IDE extension`},cursor:{type:`boolean`,default:!1,description:`Update Cursor`},all:{type:`boolean`,default:!1,description:`Update every detected MCP client`},json:{type:`boolean`,default:!1,description:`Print structured JSON output`}},run:async({args:e})=>{if(O(e.uninstall)&&O(e.reinstall))throw new y(`INVALID_INPUT`,`Use either --uninstall or --reinstall, not both.`);let t=La(),n=Ba({targets:t}),r=N(e),i=Pc(e,n.map(e=>e.client));if(i.length===0){if(!I({json:r}))throw new y(`INVALID_INPUT`,`Choose an MCP client with --claude-desktop, --claude-code, --codex, --cursor, or --all.`);let a=n.length>0?n:t;i=B(await Dr({message:e.uninstall?`Remove seo from which MCP clients?`:`Install seo into which MCP clients?`,options:a.map(e=>({value:e.client,label:e.label,hint:e.path})),initialValues:n.map(e=>e.client),required:!0}))}let a=i.filter(e=>!t.some(t=>t.client===e));if(a.length>0)throw new y(`INVALID_INPUT`,`MCP setup is not supported for ${a.join(`, `)} on ${process.platform}.`);let o=t.filter(e=>i.includes(e.client)).map(t=>O(e.uninstall)?ao(t):io(t,{reinstall:O(e.reinstall)}));if(r){L({operation:O(e.uninstall)?`uninstall`:`install`,results:o});return}for(let e of o)process.stdout.write(`${e.changed?`updated`:`skipped`} ${e.client} · ${e.path}\n`)}})}}),Ic=C({meta:{name:`crawl-diff`,description:`Crawl a site and diff technical/page changes vs last run`},args:{url:{type:`string`,required:!0,description:`Start URL to crawl.`},site:{type:`string`,description:`Optional GSC property to associate with the crawl.`},limit:{type:`string`,description:`Maximum pages to crawl. Defaults to 50.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`},refresh:{type:`boolean`,default:!1,description:`Bypass local HTTP cache.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering when Playwright is installed.`}},run:async({args:e})=>{let t=await _t({startUrl:E(e.url)??``,site:E(e.site),limit:k(e.limit),refresh:O(e.refresh),js:O(e.js)?!0:`auto`});if(N(e)){L(t);return}if(K({title:`Crawl history comparison`,target:t.run.startUrl,status:t.previousRun===void 0?`unknown`:t.summary.newErrors>0||t.summary.indexabilityFlips>0?`warning`:`pass`,summary:t.previousRun===void 0?`No previous crawl was available for comparison.`:`${t.summary.changed} changed, ${t.summary.added} added, and ${t.summary.removed} removed URLs.`,metrics:[{label:`Run`,value:t.run.id},{label:`Crawled`,value:t.summary.crawled},{label:`Previous run`,value:t.previousRun?.id??`None`},{label:`Added`,value:t.summary.added},{label:`Removed`,value:t.summary.removed},{label:`Changed`,value:t.summary.changed},{label:`New errors`,value:t.summary.newErrors},{label:`Indexability flips`,value:t.summary.indexabilityFlips},{label:`High priority`,value:t.summary.highPriorityRecommendations}]}),t.recommendations.length&&(process.stdout.write(`
172
172
  Recommended actions
173
173
  `),z([`Severity`,`Category`,`URL`,`Action`],t.recommendations.slice(0,10).map(e=>[e.severity,e.category,G(e.url,56),G(e.action,72)])),J(`Top crawl actions`,t.recommendations.map(e=>({label:e.category,context:`${e.severity}, ${G(e.url,96)}`,action:e.action})))),t.items.length&&(process.stdout.write(`
174
174
  Changed URLs