seo 0.17.1 → 0.17.2

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/dist/cli.js CHANGED
@@ -1,137 +1,137 @@
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,Cr as d,Ct as f,D as ee,Dn as p,Dt as m,E as h,En as te,Et as ne,F as re,Fn as ie,Ft as ae,G as oe,Gn as se,Gt as ce,H as le,Hn as ue,Ht as de,I as fe,In as pe,It as me,J as he,Jn as ge,Jt as _e,K as ve,Kn as ye,Kt as be,L as xe,Ln as Se,Lt as Ce,M as we,Mn as Te,Mt as Ee,N as De,Nn as Oe,Nt as ke,O as Ae,On as je,Ot as Me,P as Ne,Pn as Pe,Pt as Fe,Q as Ie,Qn as Le,Qt as Re,R as ze,Rn as Be,Rt as Ve,S as He,Sn as Ue,Sr as We,St as Ge,T as Ke,Tn as qe,Tt as Je,U as Ye,Un as Xe,Ut as Ze,V as Qe,Vn as $e,Vt as et,W as tt,Wn as nt,Wt as rt,X as it,Xn as at,Xt as ot,Y as st,Yn as ct,Yt as lt,Z as ut,Zn as dt,Zt as ft,_ as pt,_n as mt,_r as ht,_t as gt,a as _t,an as vt,ar as yt,at as bt,b as xt,bn as St,br as Ct,bt as wt,c as Tt,cn as Et,cr as Dt,ct as Ot,d as kt,dn as At,dr as jt,dt as Mt,en as Nt,er as Pt,et as Ft,f as It,fn as Lt,fr as Rt,ft as zt,g as Bt,gn as Vt,gr as Ht,gt as Ut,h as Wt,hn as Gt,hr as Kt,ht as qt,i as Jt,in as Yt,ir as Xt,it as Zt,j as Qt,jn as $t,jt as en,k as tn,kn as nn,kt as rn,l as an,ln as on,lr as sn,lt as cn,m as ln,mn as un,mr as dn,mt as fn,nn as pn,nr as mn,nt as hn,o as gn,on as _n,or as vn,ot as yn,p as bn,pn as xn,pr as Sn,pt as Cn,q as wn,qn as Tn,qt as En,rn as Dn,rr as On,rt as kn,sn as An,sr as jn,st as Mn,t as Nn,tn as Pn,tr as Fn,tt as In,u as Ln,un as Rn,ur as zn,ut as Bn,v as Vn,vn as Hn,vr as Un,vt as Wn,w as Gn,wn as Kn,wt as qn,x as Jn,xn as Yn,xr as Xn,xt as Zn,y as Qn,yn as $n,yr as g,z as er,zn as tr,zt as nr}from"./dist-BjB7Lfd6.js";import{randomBytes as rr}from"node:crypto";import{arch as ir,homedir as ar,platform as or}from"node:os";import{basename as sr,dirname as cr,join as _,posix as lr,relative as ur,resolve as dr,sep as fr,win32 as pr}from"node:path";import{constants as mr,copyFileSync as hr,createWriteStream as gr,existsSync as v,mkdirSync as _r,readFileSync as vr,readdirSync as yr,realpathSync as br,renameSync as xr,rmSync as Sr,statSync as Cr,writeFileSync as wr}from"node:fs";import{fileURLToPath as Tr}from"node:url";import{cp as Er,mkdir as Dr,open as Or,readFile as y,readdir as kr,rename as Ar,stat as jr,writeFile as Mr}from"node:fs/promises";import{isDeepStrictEqual as Nr,stripVTControlCharacters as Pr}from"node:util";import{spawn as Fr,spawnSync as Ir}from"node:child_process";import{defineCommand as b,runCommand as Lr,runMain as Rr}from"citty";import{cancel as zr,confirm as Br,intro as Vr,isCancel as Hr,log as Ur,multiselect as Wr,note as x,outro as S,password as Gr,select as Kr,text as qr}from"@clack/prompts";import Jr from"picocolors";import Yr from"string-width";import Xr from"cli-table3";import{finished as Zr}from"node:stream/promises";import{applyEdits as Qr,modify as $r,parse as ei,printParseErrorCode as ti}from"jsonc-parser";import ni from"update-notifier";const C={summary:/* @__PURE__ */ `workflow.site.generated_at.summary.attribution.confidence.classification.updates_matched.known_confounders.page_segment_status.page_segment_eligible_rows.page_segment_returned_rows.page_segment_limited_rows.page_segment_result_limit.page_segment_source_row_limit.page_segment_source_possibly_truncated.page_segment_warnings.query_segment_status.query_segment_eligible_rows.query_segment_returned_rows.query_segment_limited_rows.query_segment_result_limit.query_segment_source_row_limit.query_segment_source_possibly_truncated.query_segment_warnings.device_segment_status.device_segment_eligible_rows.device_segment_returned_rows.device_segment_limited_rows.device_segment_result_limit.device_segment_source_row_limit.device_segment_source_possibly_truncated.device_segment_warnings.country_segment_status.country_segment_eligible_rows.country_segment_returned_rows.country_segment_limited_rows.country_segment_result_limit.country_segment_source_row_limit.country_segment_source_possibly_truncated.country_segment_warnings`.split(`.`),steps:[`rank`,`tool`,`status`,`summary`],actions:[`rank`,`title`,`confidence`,`action`],findings:[`dimension`,`data_status`,`eligible_rows`,`returned_rows`,`limited_rows`,`result_limit`,`unmatched_rows`,`summary`,`winner_key`,`winner_click_delta`,`winner_impression_delta`,`winner_position_delta`,`loser_key`,`loser_click_delta`,`loser_impression_delta`,`loser_position_delta`],templates:[`rank`,`direction`,`template`,`confidence`,`url_count`,`click_delta`,`impression_delta`,`movement_share`,`movement_share_scope`,`confidence_scope`,`eligible_rows`,`returned_rows`,`limited_rows`,`result_limit`,`source_possibly_truncated`,`common_terms`,`sample_urls`,`summary`],evidence:[`rank`,`evidence`],confounders:[`rank`,`source`,`title`,`date`,`scope`,`target`,`description`],segment:[`rank`,`dimension`,`data_status`,`evidence_scope`,`key`,`before_clicks`,`after_clicks`,`click_delta`,`before_impressions`,`after_impressions`,`impression_delta`,`before_position`,`after_position`,`position_delta`],unmatchedSegment:[`rank`,`dimension`,`data_status`,`key`,`retained_in`,`clicks`,`impressions`,`position`,`reason`]};function ri(e,t){return[...e.output.segments[t].winners,...e.output.segments[t].losers].map((n,r)=>({rank:r+1,dimension:t,data_status:e.output.segments[t].dataStatus,evidence_scope:n.evidenceScope,key:n.key,before_clicks:n.beforeClicks,after_clicks:n.afterClicks,click_delta:n.clickDelta,before_impressions:n.beforeImpressions,after_impressions:n.afterImpressions,impression_delta:n.impressionDelta,before_position:n.beforePosition,after_position:n.afterPosition,position_delta:n.positionDelta}))}function ii(e,t){return e.output.segments[t].unmatchedSegments.map((n,r)=>({rank:r+1,dimension:t,data_status:e.output.segments[t].dataStatus,key:n.key,retained_in:n.retainedIn,clicks:n.clicks,impressions:n.impressions,position:n.position,reason:n.reason}))}function ai(e){return[{filename:`postmortem-summary.csv`,headers:[...C.summary],rows:[{workflow:e.workflow,site:e.site,generated_at:e.generatedAt,summary:e.summary,attribution:e.output.update.attribution,confidence:e.output.update.confidence,classification:e.output.update.classification,updates_matched:e.output.update.overlappingUpdates.length,known_confounders:e.output.update.confounders.length,page_segment_status:e.output.segments.page.dataStatus,page_segment_eligible_rows:e.output.segments.page.coverage.eligibleRows,page_segment_returned_rows:e.output.segments.page.coverage.returnedRows,page_segment_limited_rows:e.output.segments.page.coverage.limitedRows,page_segment_result_limit:e.output.segments.page.coverage.resultLimit,page_segment_source_row_limit:e.output.segments.page.coverage.sourceRowLimit,page_segment_source_possibly_truncated:e.output.segments.page.coverage.sourcePossiblyTruncated,page_segment_warnings:e.output.segments.page.warnings.join(`; `),query_segment_status:e.output.segments.query.dataStatus,query_segment_eligible_rows:e.output.segments.query.coverage.eligibleRows,query_segment_returned_rows:e.output.segments.query.coverage.returnedRows,query_segment_limited_rows:e.output.segments.query.coverage.limitedRows,query_segment_result_limit:e.output.segments.query.coverage.resultLimit,query_segment_source_row_limit:e.output.segments.query.coverage.sourceRowLimit,query_segment_source_possibly_truncated:e.output.segments.query.coverage.sourcePossiblyTruncated,query_segment_warnings:e.output.segments.query.warnings.join(`; `),device_segment_status:e.output.segments.device.dataStatus,device_segment_eligible_rows:e.output.segments.device.coverage.eligibleRows,device_segment_returned_rows:e.output.segments.device.coverage.returnedRows,device_segment_limited_rows:e.output.segments.device.coverage.limitedRows,device_segment_result_limit:e.output.segments.device.coverage.resultLimit,device_segment_source_row_limit:e.output.segments.device.coverage.sourceRowLimit,device_segment_source_possibly_truncated:e.output.segments.device.coverage.sourcePossiblyTruncated,device_segment_warnings:e.output.segments.device.warnings.join(`; `),country_segment_status:e.output.segments.country.dataStatus,country_segment_eligible_rows:e.output.segments.country.coverage.eligibleRows,country_segment_returned_rows:e.output.segments.country.coverage.returnedRows,country_segment_limited_rows:e.output.segments.country.coverage.limitedRows,country_segment_result_limit:e.output.segments.country.coverage.resultLimit,country_segment_source_row_limit:e.output.segments.country.coverage.sourceRowLimit,country_segment_source_possibly_truncated:e.output.segments.country.coverage.sourcePossiblyTruncated,country_segment_warnings:e.output.segments.country.warnings.join(`; `)}]},{filename:`workflow-steps.csv`,headers:[...C.steps],rows:e.steps.map((e,t)=>({rank:t+1,tool:e.tool,status:e.status,summary:e.summary}))},{filename:`workflow-actions.csv`,headers:[...C.actions],rows:e.actions.map((e,t)=>({rank:t+1,title:e.title,confidence:e.confidence,action:e.action}))},{filename:`postmortem-findings.csv`,headers:[...C.findings],rows:e.output.insights.map(e=>({dimension:e.dimension,data_status:e.dataStatus,eligible_rows:e.coverage.eligibleRows,returned_rows:e.coverage.returnedRows,limited_rows:e.coverage.limitedRows,result_limit:e.coverage.resultLimit,unmatched_rows:e.unmatchedRows,summary:e.summary,winner_key:e.winner?.key,winner_click_delta:e.winner?.clickDelta,winner_impression_delta:e.winner?.impressionDelta,winner_position_delta:e.winner?.positionDelta,loser_key:e.loser?.key,loser_click_delta:e.loser?.clickDelta,loser_impression_delta:e.loser?.impressionDelta,loser_position_delta:e.loser?.positionDelta}))},{filename:`postmortem-template-movement.csv`,headers:[...C.templates],rows:e.output.templateMovement.map((e,t)=>({rank:t+1,direction:e.direction,template:e.signature,confidence:e.confidence,url_count:e.urlCount,click_delta:e.clickDelta,impression_delta:e.impressionDelta,movement_share:e.movementShare,movement_share_scope:e.movementShareScope,confidence_scope:e.confidenceScope,eligible_rows:e.segmentCoverage.eligibleRows,returned_rows:e.segmentCoverage.returnedRows,limited_rows:e.segmentCoverage.limitedRows,result_limit:e.segmentCoverage.resultLimit,source_possibly_truncated:e.segmentCoverage.sourcePossiblyTruncated,common_terms:e.commonTerms.join(`; `),sample_urls:e.sampleUrls.join(`; `),summary:e.summary}))},{filename:`postmortem-update-evidence.csv`,headers:[...C.evidence],rows:e.output.update.evidence.map((e,t)=>({rank:t+1,evidence:e}))},{filename:`postmortem-confounders.csv`,headers:[...C.confounders],rows:e.output.update.confounders.map((e,t)=>({rank:t+1,source:e.source,title:e.title,date:e.date,scope:e.scope,target:e.target,description:e.description}))},...[`page`,`query`,`device`,`country`].map(t=>({filename:`postmortem-segment-${t}.csv`,headers:[...C.segment],rows:ri(e,t)})),...[`page`,`query`,`device`,`country`].map(t=>({filename:`postmortem-segment-${t}-unmatched.csv`,headers:[...C.unmatchedSegment],rows:ii(e,t)}))]}function oi(e){return v(e)?yr(e,{withFileTypes:!0}).filter(e=>e.isFile()&&e.name.endsWith(`.log`)).map(t=>{let n=_(e,t.name),r=Cr(n);return{path:n,size:r.size,modifiedAt:r.mtimeMs}}):[]}function si(e,t){let n=cr(e),r=sr(e,`.log`),i=0;for(;;){let e=_(n,`${r}.${t}${i?`-${i}`:``}.log`);if(!v(e))return e;i+=1}}function ci(e={}){let t=e.directory??Sn().logDir,n=e.now??Date.now(),r=e.maxFileBytes??8388608,i=e.maxTotalBytes??67108864,a=e.maxAgeMs??12096e5;_r(t,{recursive:!0,mode:448});let o=0,s=0;for(let e of oi(t))e.size<=r||(xr(e.path,si(e.path,n)),o+=1);for(let e of oi(t))e.modifiedAt>=n-a||(Sr(e.path,{force:!0}),s+=1);let c=oi(t).sort((e,t)=>e.modifiedAt-t.modifiedAt||(e.path<t.path?-1:+(e.path>t.path))),l=c.reduce((e,t)=>e+t.size,0);for(let e of c){if(l<=i)break;Sr(e.path,{force:!0}),l-=e.size,s+=1}return{rotated:o,removed:s,sizeBytes:l}}function li(e){let t=e-Date.now();if(t<=0)return`expired`;let n=Math.round(t/6e4);if(n<60)return`in ${n}m`;let r=Math.floor(n/60),i=n%60;return i?`in ${r}h ${i}m`:`in ${r}h`}function ui(e,t){return{type:`boolean`,default:!0,description:e,negativeDescription:t}}function w(e){return typeof e==`string`?e:void 0}function T(e){return typeof e==`boolean`?e:void 0}function di(e){let t=w(e.rendering);if(t!==void 0&&t!==`auto`&&t!==`on`&&t!==`off`)throw new g(`INVALID_INPUT`,`--rendering must be auto, on, or off.`);if(T(e.js)===!0&&t&&t!==`on`)throw new g(`INVALID_INPUT`,`Use either --js or --rendering on, not both with different modes.`);return t??(T(e.js)?`on`:`auto`)}function E(e,t){let n=T(e[t]);if(n===!1)return!0;if(n===!0)return!1;let r=T(e[fi(t)]);if(r===!1)return!0;if(r===!0)return!1;if(T(e[`no-${t}`])===!0||T(e[`no${pi(t)}`])===!0)return!0}function fi(e){return e.replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}function pi(e){let t=fi(e);return t?`${t[0]?.toUpperCase()}${t.slice(1)}`:``}function D(e){if(typeof e==`number`)return e;if(typeof e!=`string`||!e.trim())return;let t=Number(e);return Number.isFinite(t)?t:void 0}function O(e,t){if(e===void 0||e===``)return;let n=D(e);if(n===void 0)throw new g(`INVALID_INPUT`,`${t} must be a number.`);return n}function k(e){if(typeof e!=`string`)return;let t=e.split(`,`).map(e=>e.trim()).filter(Boolean);return t.length?t:void 0}function mi(e){return k(e)??[]}function hi(e){let t={concurrency:D(e[`fetch-concurrency`]),intervalCap:D(e[`fetch-interval-cap`]),intervalMs:D(e[`fetch-interval-ms`])};return Object.values(t).some(e=>e!==void 0)?t:void 0}function A(e){return e.json===!0}async function gi(e,t){let n=w(e),r=w(t);if(n&&r)throw Error(`Use either --body or --body-file, not both.`);if(!n&&!r)return;let i=n??await y(r??``,`utf8`),a=JSON.parse(i);if(!a||typeof a!=`object`||Array.isArray(a))throw Error(`JSON body must be an object.`);return a}function j(e){if(e)return e;throw new g(`INVALID_INPUT`,"No site given. Pass --site with a domain, URL or sc-domain: property. Run `seo sites` to list your Search Console properties.")}async function _i(e,t={}){let n=j(e),r=(await $n(t.refresh,t.account)).map(e=>e.siteUrl),i=Kt(r,n);if(!i)throw new g(`PROPERTY_NOT_FOUND`,`This Google login has no Search Console property for ${n}. Run \`seo sites\` to list the properties it can read.`);return i}function vi(e){if(e)return e;throw new g(`INVALID_INPUT`,"No Google Analytics property given. Pass --property with the numeric property id. Run `seo analytics google properties` to list them.")}function yi(e){if(typeof e==`number`)return Number.isFinite(e)?Math.round(e):void 0;if(!e)return;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:void 0}function bi(e={}){let t=e.env??process.env,n=yi(e.columns)??yi(e.streamColumns)??yi(t.COLUMNS)??80;return Math.min(200,Math.max(40,n))}function M(e={}){let t=e.env??process.env,n=e.isTTY??!!process.stdout.isTTY,r=e.color??!!(n&&t.NO_COLOR===void 0);return{columns:bi({...e,env:t,streamColumns:e.streamColumns??process.stdout.columns}),colors:Jr.createColors(r),hasColor:r,isInteractive:n&&!t.CI}}function N(e){return Yr(Pr(e))}function xi(e,t){let n=[],r=``;for(let i of e)r&&N(`${r}${i}`)>t?(n.push(r),r=i):r+=i;return r&&n.push(r),n}function Si(e,t){if(!e)return[``];let n=e.trim().split(/\s+/u),r=[],i=``;for(let e of n){let n=N(e)>t?xi(e,t):[e];for(let e of n)i?N(`${i} ${e}`)<=t?i+=` ${e}`:(r.push(i),i=e):i=e}return i&&r.push(i),r.length>0?r:[``]}function P(e,t){let n=Math.max(1,t);return e.replaceAll(`\r
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,Cr as d,Ct as f,D as ee,Dn as p,Dt as te,E as m,En as h,Et as ne,F as re,Fn as ie,Ft as ae,G as oe,Gn as se,Gt as ce,H as le,Hn as ue,Ht as de,I as fe,In as pe,It as me,J as he,Jn as ge,Jt as _e,K as ve,Kn as ye,Kt as be,L as xe,Ln as Se,Lt as Ce,M as we,Mn as Te,Mt as Ee,N as De,Nn as Oe,Nt as ke,O as Ae,On as je,Ot as Me,P as Ne,Pn as Pe,Pt as Fe,Q as Ie,Qn as Le,Qt as Re,R as ze,Rn as Be,Rt as Ve,S as He,Sn as Ue,Sr as We,St as Ge,T as Ke,Tn as qe,Tt as Je,U as Ye,Un as Xe,Ut as Ze,V as Qe,Vn as $e,Vt as et,W as tt,Wn as nt,Wt as rt,X as it,Xn as at,Xt as ot,Y as st,Yn as ct,Yt as lt,Z as ut,Zn as dt,Zt as ft,_ as pt,_n as mt,_r as ht,_t as gt,a as _t,an as vt,ar as yt,at as bt,b as xt,bn as St,br as g,c as Ct,cn as wt,cr as Tt,ct as Et,d as Dt,dn as Ot,dr as kt,dt as At,en as jt,er as Mt,et as Nt,f as Pt,fn as Ft,fr as It,ft as Lt,g as Rt,gn as zt,gr as Bt,gt as Vt,h as Ht,hn as Ut,hr as Wt,ht as Gt,i as Kt,in as qt,ir as Jt,it as Yt,j as Xt,jn as Zt,jt as Qt,k as $t,kn as en,kt as tn,l as nn,ln as rn,lr as an,lt as on,m as sn,mn as cn,mr as ln,mt as un,nn as dn,nr as fn,nt as pn,o as mn,on as hn,or as gn,ot as _n,p as vn,pn as yn,pr as bn,pt as xn,q as Sn,qn as Cn,qt as wn,rn as Tn,rr as En,rt as Dn,sn as On,sr as kn,st as An,t as jn,tn as Mn,tr as Nn,tt as Pn,u as Fn,un as In,ur as Ln,ut as Rn,v as zn,vn as Bn,vr as Vn,vt as Hn,w as Un,wn as Wn,wr as Gn,wt as Kn,x as qn,xn as Jn,xr as Yn,xt as Xn,y as Zn,yn as Qn,yr as $n,yt as er,z as tr,zn as nr,zt as rr}from"./dist-CW8OlPkK.js";import{randomBytes as ir}from"node:crypto";import{arch as ar,homedir as or,platform as sr}from"node:os";import{basename as cr,dirname as lr,join as _,posix as ur,relative as dr,resolve as fr,sep as pr,win32 as mr}from"node:path";import{constants as hr,copyFileSync as gr,createWriteStream as _r,existsSync as v,mkdirSync as vr,readFileSync as yr,readdirSync as br,realpathSync as xr,renameSync as Sr,rmSync as Cr,statSync as wr,writeFileSync as Tr}from"node:fs";import{fileURLToPath as Er}from"node:url";import{cp as Dr,mkdir as Or,open as kr,readFile as y,readdir as Ar,rename as jr,stat as Mr,writeFile as Nr}from"node:fs/promises";import{isDeepStrictEqual as Pr,stripVTControlCharacters as Fr}from"node:util";import{spawn as Ir,spawnSync as Lr}from"node:child_process";import{defineCommand as b,runCommand as Rr,runMain as zr}from"citty";import{cancel as Br,confirm as Vr,intro as x,isCancel as Hr,log as Ur,multiselect as Wr,note as S,outro as C,password as Gr,select as Kr,text as qr}from"@clack/prompts";import Jr from"picocolors";import Yr from"string-width";import Xr from"cli-table3";import{finished as Zr}from"node:stream/promises";import{applyEdits as Qr,modify as $r,parse as ei,printParseErrorCode as ti}from"jsonc-parser";import ni from"update-notifier";const w={summary:/* @__PURE__ */ `workflow.site.generated_at.summary.attribution.confidence.classification.updates_matched.known_confounders.page_segment_status.page_segment_eligible_rows.page_segment_returned_rows.page_segment_limited_rows.page_segment_result_limit.page_segment_source_row_limit.page_segment_source_possibly_truncated.page_segment_warnings.query_segment_status.query_segment_eligible_rows.query_segment_returned_rows.query_segment_limited_rows.query_segment_result_limit.query_segment_source_row_limit.query_segment_source_possibly_truncated.query_segment_warnings.device_segment_status.device_segment_eligible_rows.device_segment_returned_rows.device_segment_limited_rows.device_segment_result_limit.device_segment_source_row_limit.device_segment_source_possibly_truncated.device_segment_warnings.country_segment_status.country_segment_eligible_rows.country_segment_returned_rows.country_segment_limited_rows.country_segment_result_limit.country_segment_source_row_limit.country_segment_source_possibly_truncated.country_segment_warnings`.split(`.`),steps:[`rank`,`tool`,`status`,`summary`],actions:[`rank`,`title`,`confidence`,`action`],findings:[`dimension`,`data_status`,`eligible_rows`,`returned_rows`,`limited_rows`,`result_limit`,`unmatched_rows`,`summary`,`winner_key`,`winner_click_delta`,`winner_impression_delta`,`winner_position_delta`,`loser_key`,`loser_click_delta`,`loser_impression_delta`,`loser_position_delta`],templates:[`rank`,`direction`,`template`,`confidence`,`url_count`,`click_delta`,`impression_delta`,`movement_share`,`movement_share_scope`,`confidence_scope`,`eligible_rows`,`returned_rows`,`limited_rows`,`result_limit`,`source_possibly_truncated`,`common_terms`,`sample_urls`,`summary`],evidence:[`rank`,`evidence`],confounders:[`rank`,`source`,`title`,`date`,`scope`,`target`,`description`],segment:[`rank`,`dimension`,`data_status`,`evidence_scope`,`key`,`before_clicks`,`after_clicks`,`click_delta`,`before_impressions`,`after_impressions`,`impression_delta`,`before_position`,`after_position`,`position_delta`],unmatchedSegment:[`rank`,`dimension`,`data_status`,`key`,`retained_in`,`clicks`,`impressions`,`position`,`reason`]};function ri(e,t){return[...e.output.segments[t].winners,...e.output.segments[t].losers].map((n,r)=>({rank:r+1,dimension:t,data_status:e.output.segments[t].dataStatus,evidence_scope:n.evidenceScope,key:n.key,before_clicks:n.beforeClicks,after_clicks:n.afterClicks,click_delta:n.clickDelta,before_impressions:n.beforeImpressions,after_impressions:n.afterImpressions,impression_delta:n.impressionDelta,before_position:n.beforePosition,after_position:n.afterPosition,position_delta:n.positionDelta}))}function ii(e,t){return e.output.segments[t].unmatchedSegments.map((n,r)=>({rank:r+1,dimension:t,data_status:e.output.segments[t].dataStatus,key:n.key,retained_in:n.retainedIn,clicks:n.clicks,impressions:n.impressions,position:n.position,reason:n.reason}))}function ai(e){return[{filename:`postmortem-summary.csv`,headers:[...w.summary],rows:[{workflow:e.workflow,site:e.site,generated_at:e.generatedAt,summary:e.summary,attribution:e.output.update.attribution,confidence:e.output.update.confidence,classification:e.output.update.classification,updates_matched:e.output.update.overlappingUpdates.length,known_confounders:e.output.update.confounders.length,page_segment_status:e.output.segments.page.dataStatus,page_segment_eligible_rows:e.output.segments.page.coverage.eligibleRows,page_segment_returned_rows:e.output.segments.page.coverage.returnedRows,page_segment_limited_rows:e.output.segments.page.coverage.limitedRows,page_segment_result_limit:e.output.segments.page.coverage.resultLimit,page_segment_source_row_limit:e.output.segments.page.coverage.sourceRowLimit,page_segment_source_possibly_truncated:e.output.segments.page.coverage.sourcePossiblyTruncated,page_segment_warnings:e.output.segments.page.warnings.join(`; `),query_segment_status:e.output.segments.query.dataStatus,query_segment_eligible_rows:e.output.segments.query.coverage.eligibleRows,query_segment_returned_rows:e.output.segments.query.coverage.returnedRows,query_segment_limited_rows:e.output.segments.query.coverage.limitedRows,query_segment_result_limit:e.output.segments.query.coverage.resultLimit,query_segment_source_row_limit:e.output.segments.query.coverage.sourceRowLimit,query_segment_source_possibly_truncated:e.output.segments.query.coverage.sourcePossiblyTruncated,query_segment_warnings:e.output.segments.query.warnings.join(`; `),device_segment_status:e.output.segments.device.dataStatus,device_segment_eligible_rows:e.output.segments.device.coverage.eligibleRows,device_segment_returned_rows:e.output.segments.device.coverage.returnedRows,device_segment_limited_rows:e.output.segments.device.coverage.limitedRows,device_segment_result_limit:e.output.segments.device.coverage.resultLimit,device_segment_source_row_limit:e.output.segments.device.coverage.sourceRowLimit,device_segment_source_possibly_truncated:e.output.segments.device.coverage.sourcePossiblyTruncated,device_segment_warnings:e.output.segments.device.warnings.join(`; `),country_segment_status:e.output.segments.country.dataStatus,country_segment_eligible_rows:e.output.segments.country.coverage.eligibleRows,country_segment_returned_rows:e.output.segments.country.coverage.returnedRows,country_segment_limited_rows:e.output.segments.country.coverage.limitedRows,country_segment_result_limit:e.output.segments.country.coverage.resultLimit,country_segment_source_row_limit:e.output.segments.country.coverage.sourceRowLimit,country_segment_source_possibly_truncated:e.output.segments.country.coverage.sourcePossiblyTruncated,country_segment_warnings:e.output.segments.country.warnings.join(`; `)}]},{filename:`workflow-steps.csv`,headers:[...w.steps],rows:e.steps.map((e,t)=>({rank:t+1,tool:e.tool,status:e.status,summary:e.summary}))},{filename:`workflow-actions.csv`,headers:[...w.actions],rows:e.actions.map((e,t)=>({rank:t+1,title:e.title,confidence:e.confidence,action:e.action}))},{filename:`postmortem-findings.csv`,headers:[...w.findings],rows:e.output.insights.map(e=>({dimension:e.dimension,data_status:e.dataStatus,eligible_rows:e.coverage.eligibleRows,returned_rows:e.coverage.returnedRows,limited_rows:e.coverage.limitedRows,result_limit:e.coverage.resultLimit,unmatched_rows:e.unmatchedRows,summary:e.summary,winner_key:e.winner?.key,winner_click_delta:e.winner?.clickDelta,winner_impression_delta:e.winner?.impressionDelta,winner_position_delta:e.winner?.positionDelta,loser_key:e.loser?.key,loser_click_delta:e.loser?.clickDelta,loser_impression_delta:e.loser?.impressionDelta,loser_position_delta:e.loser?.positionDelta}))},{filename:`postmortem-template-movement.csv`,headers:[...w.templates],rows:e.output.templateMovement.map((e,t)=>({rank:t+1,direction:e.direction,template:e.signature,confidence:e.confidence,url_count:e.urlCount,click_delta:e.clickDelta,impression_delta:e.impressionDelta,movement_share:e.movementShare,movement_share_scope:e.movementShareScope,confidence_scope:e.confidenceScope,eligible_rows:e.segmentCoverage.eligibleRows,returned_rows:e.segmentCoverage.returnedRows,limited_rows:e.segmentCoverage.limitedRows,result_limit:e.segmentCoverage.resultLimit,source_possibly_truncated:e.segmentCoverage.sourcePossiblyTruncated,common_terms:e.commonTerms.join(`; `),sample_urls:e.sampleUrls.join(`; `),summary:e.summary}))},{filename:`postmortem-update-evidence.csv`,headers:[...w.evidence],rows:e.output.update.evidence.map((e,t)=>({rank:t+1,evidence:e}))},{filename:`postmortem-confounders.csv`,headers:[...w.confounders],rows:e.output.update.confounders.map((e,t)=>({rank:t+1,source:e.source,title:e.title,date:e.date,scope:e.scope,target:e.target,description:e.description}))},...[`page`,`query`,`device`,`country`].map(t=>({filename:`postmortem-segment-${t}.csv`,headers:[...w.segment],rows:ri(e,t)})),...[`page`,`query`,`device`,`country`].map(t=>({filename:`postmortem-segment-${t}-unmatched.csv`,headers:[...w.unmatchedSegment],rows:ii(e,t)}))]}function oi(e){return v(e)?br(e,{withFileTypes:!0}).filter(e=>e.isFile()&&e.name.endsWith(`.log`)).map(t=>{let n=_(e,t.name),r=wr(n);return{path:n,size:r.size,modifiedAt:r.mtimeMs}}):[]}function si(e,t){let n=lr(e),r=cr(e,`.log`),i=0;for(;;){let e=_(n,`${r}.${t}${i?`-${i}`:``}.log`);if(!v(e))return e;i+=1}}function ci(e={}){let t=e.directory??ln().logDir,n=e.now??Date.now(),r=e.maxFileBytes??8388608,i=e.maxTotalBytes??67108864,a=e.maxAgeMs??12096e5;vr(t,{recursive:!0,mode:448});let o=0,s=0;for(let e of oi(t))e.size<=r||(Sr(e.path,si(e.path,n)),o+=1);for(let e of oi(t))e.modifiedAt>=n-a||(Cr(e.path,{force:!0}),s+=1);let c=oi(t).sort((e,t)=>e.modifiedAt-t.modifiedAt||(e.path<t.path?-1:+(e.path>t.path))),l=c.reduce((e,t)=>e+t.size,0);for(let e of c){if(l<=i)break;Cr(e.path,{force:!0}),l-=e.size,s+=1}return{rotated:o,removed:s,sizeBytes:l}}function li(e){let t=e-Date.now();if(t<=0)return`expired`;let n=Math.round(t/6e4);if(n<60)return`in ${n}m`;let r=Math.floor(n/60),i=n%60;return i?`in ${r}h ${i}m`:`in ${r}h`}function ui(e,t){return{type:`boolean`,default:!0,description:e,negativeDescription:t}}function T(e){return typeof e==`string`?e:void 0}function E(e){return typeof e==`boolean`?e:void 0}function di(e){let t=T(e.rendering);if(t!==void 0&&t!==`auto`&&t!==`on`&&t!==`off`)throw new g(`INVALID_INPUT`,`--rendering must be auto, on, or off.`);if(E(e.js)===!0&&t&&t!==`on`)throw new g(`INVALID_INPUT`,`Use either --js or --rendering on, not both with different modes.`);return t??(E(e.js)?`on`:`auto`)}function D(e,t){let n=E(e[t]);if(n===!1)return!0;if(n===!0)return!1;let r=E(e[fi(t)]);if(r===!1)return!0;if(r===!0)return!1;if(E(e[`no-${t}`])===!0||E(e[`no${pi(t)}`])===!0)return!0}function fi(e){return e.replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}function pi(e){let t=fi(e);return t?`${t[0]?.toUpperCase()}${t.slice(1)}`:``}function O(e){if(typeof e==`number`)return e;if(typeof e!=`string`||!e.trim())return;let t=Number(e);return Number.isFinite(t)?t:void 0}function k(e,t){if(e===void 0||e===``)return;let n=O(e);if(n===void 0)throw new g(`INVALID_INPUT`,`${t} must be a number.`);return n}function A(e){if(typeof e!=`string`)return;let t=e.split(`,`).map(e=>e.trim()).filter(Boolean);return t.length?t:void 0}function mi(e){return A(e)??[]}function hi(e){let t={concurrency:O(e[`fetch-concurrency`]),intervalCap:O(e[`fetch-interval-cap`]),intervalMs:O(e[`fetch-interval-ms`])};return Object.values(t).some(e=>e!==void 0)?t:void 0}function j(e){return e.json===!0}async function gi(e,t){let n=T(e),r=T(t);if(n&&r)throw Error(`Use either --body or --body-file, not both.`);if(!n&&!r)return;let i=n??await y(r??``,`utf8`),a=JSON.parse(i);if(!a||typeof a!=`object`||Array.isArray(a))throw Error(`JSON body must be an object.`);return a}function M(e){if(e)return e;throw new g(`INVALID_INPUT`,"No site given. Pass --site with a domain, URL or sc-domain: property. Run `seo sites` to list your Search Console properties.")}async function _i(e,t={}){let n=M(e),r=(await St(t.refresh,t.account)).map(e=>e.siteUrl),i=Bt(r,n);if(!i)throw new g(`PROPERTY_NOT_FOUND`,`This Google login has no Search Console property for ${n}. Run \`seo sites\` to list the properties it can read.`);return i}function vi(e){if(e)return e;throw new g(`INVALID_INPUT`,"No Google Analytics property given. Pass --property with the numeric property id. Run `seo analytics google properties` to list them.")}function yi(e){if(typeof e==`number`)return Number.isFinite(e)?Math.round(e):void 0;if(!e)return;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:void 0}function bi(e={}){let t=e.env??process.env,n=yi(e.columns)??yi(e.streamColumns)??yi(t.COLUMNS)??80;return Math.min(200,Math.max(40,n))}function N(e={}){let t=e.env??process.env,n=e.isTTY??!!process.stdout.isTTY,r=e.color??!!(n&&t.NO_COLOR===void 0);return{columns:bi({...e,env:t,streamColumns:e.streamColumns??process.stdout.columns}),colors:Jr.createColors(r),hasColor:r,isInteractive:n&&!t.CI}}function P(e){return Yr(Fr(e))}function xi(e,t){let n=[],r=``;for(let i of e)r&&P(`${r}${i}`)>t?(n.push(r),r=i):r+=i;return r&&n.push(r),n}function Si(e,t){if(!e)return[``];let n=e.trim().split(/\s+/u),r=[],i=``;for(let e of n){let n=P(e)>t?xi(e,t):[e];for(let e of n)i?P(`${i} ${e}`)<=t?i+=` ${e}`:(r.push(i),i=e):i=e}return i&&r.push(i),r.length>0?r:[``]}function F(e,t){let n=Math.max(1,t);return e.replaceAll(`\r
4
4
  `,`
5
5
  `).split(`
6
- `).flatMap(e=>Si(e,n))}function Ci(e,t){if(N(e)<=t)return e;let n=Math.max(1,t-N(`...`)),r=``;for(let t of e){if(N(`${r}${t}`)>n)break;r+=t}return`${r}...`}const wi={top:``,"top-mid":``,"top-left":``,"top-right":``,bottom:``,"bottom-mid":``,"bottom-left":``,"bottom-right":``,left:``,"left-mid":``,mid:``,"mid-mid":``,right:``,"right-mid":``,middle:``};function Ti(e){return e===void 0?``:String(e)}function Ei(e,t){return e.map((e,n)=>{let r=t.map(e=>Ti(e[n]));return Math.min(42,Math.max(N(e),...r.map(N),4))})}function Di(e,t,n){return e.length===0||e.length>4||!e.map((e,n)=>t.every(e=>/^-?(?:\d+(?:\.\d+)?|\d+(?:\.\d+)?%)$/u.test(Ti(e[n]).trim()))).some(Boolean)?!1:Ei(e,t).reduce((e,t)=>e+t+2,0)<=n.columns}function Oi(e,t,n){if(!/(?:status|state|assessment|data|allowed)/iu.test(e))return t;let r=t.trim().toLowerCase();return[`pass`,`passed`,`completed`,`allowed`,`yes`].includes(r)?n.colors.green(t):[`warn`,`warning`,`review`,`partial`].includes(r)?n.colors.yellow(t):[`fail`,`failed`,`blocked`,`no`].includes(r)?n.colors.red(t):[`unknown`,`unavailable`].includes(r)?n.colors.blue(t):t}function ki(e,t,n){let r=Ei(e,t).map(e=>e+2),i=new Xr({chars:wi,colWidths:r,head:e.map(e=>n.colors.bold(e)),style:{border:[],head:[],"padding-left":0,"padding-right":2},wordWrap:!1});for(let a of t)i.push(e.map((e,t)=>P(Oi(e,Ti(a[t]),n),Math.max(1,(r[t]??3)-2)).join(`
7
- `)));return i.toString()}function Ai(e,t,n,r){let i=r.columns-n-2;return i<24?[r.colors.dim(e),...P(t,r.columns-2).map(e=>` ${e}`)]:P(t,i).map((t,i)=>{let a=i===0?e.padEnd(n):``.padEnd(n);return`${r.colors.dim(a)}${` `.repeat(2)}${t}`})}function ji(e,t,n,r={}){let i=Math.min(28,Math.max(...e.map(e=>N(e)),0)),a=r.rowNumbers??!1,o=String(t.length).length;return t.map((t,r)=>{let s=[];a&&s.push(n.colors.bold(String(r+1).padStart(o,`0`)));for(let[r,a]of e.entries())s.push(...Ai(a,Oi(a,Ti(t[r]),n),i,n));return s.join(`
6
+ `).flatMap(e=>Si(e,n))}function Ci(e,t){if(P(e)<=t)return e;let n=Math.max(1,t-P(`...`)),r=``;for(let t of e){if(P(`${r}${t}`)>n)break;r+=t}return`${r}...`}const wi={top:``,"top-mid":``,"top-left":``,"top-right":``,bottom:``,"bottom-mid":``,"bottom-left":``,"bottom-right":``,left:``,"left-mid":``,mid:``,"mid-mid":``,right:``,"right-mid":``,middle:``};function Ti(e){return e===void 0?``:String(e)}function Ei(e,t){return e.map((e,n)=>{let r=t.map(e=>Ti(e[n]));return Math.min(42,Math.max(P(e),...r.map(P),4))})}function Di(e,t,n){return e.length===0||e.length>4||!e.map((e,n)=>t.every(e=>/^-?(?:\d+(?:\.\d+)?|\d+(?:\.\d+)?%)$/u.test(Ti(e[n]).trim()))).some(Boolean)?!1:Ei(e,t).reduce((e,t)=>e+t+2,0)<=n.columns}function Oi(e,t,n){if(!/(?:status|state|assessment|data|allowed)/iu.test(e))return t;let r=t.trim().toLowerCase();return[`pass`,`passed`,`completed`,`allowed`,`yes`].includes(r)?n.colors.green(t):[`warn`,`warning`,`review`,`partial`].includes(r)?n.colors.yellow(t):[`fail`,`failed`,`blocked`,`no`].includes(r)?n.colors.red(t):[`unknown`,`unavailable`].includes(r)?n.colors.blue(t):t}function ki(e,t,n){let r=Ei(e,t).map(e=>e+2),i=new Xr({chars:wi,colWidths:r,head:e.map(e=>n.colors.bold(e)),style:{border:[],head:[],"padding-left":0,"padding-right":2},wordWrap:!1});for(let a of t)i.push(e.map((e,t)=>F(Oi(e,Ti(a[t]),n),Math.max(1,(r[t]??3)-2)).join(`
7
+ `)));return i.toString()}function Ai(e,t,n,r){let i=r.columns-n-2;return i<24?[r.colors.dim(e),...F(t,r.columns-2).map(e=>` ${e}`)]:F(t,i).map((t,i)=>{let a=i===0?e.padEnd(n):``.padEnd(n);return`${r.colors.dim(a)}${` `.repeat(2)}${t}`})}function ji(e,t,n,r={}){let i=Math.min(28,Math.max(...e.map(e=>P(e)),0)),a=r.rowNumbers??!1,o=String(t.length).length;return t.map((t,r)=>{let s=[];a&&s.push(n.colors.bold(String(r+1).padStart(o,`0`)));for(let[r,a]of e.entries())s.push(...Ai(a,Oi(a,Ti(t[r]),n),i,n));return s.join(`
8
8
  `)}).join(`
9
9
 
10
- `)}function Mi(e,t,n,r={}){return t.length===0?n.colors.dim(`No results.`):r.layout===`table`||r.layout!==`records`&&Di(e,t,n)?ki(e,t,n):ji(e,t,n,r)}function Ni(e,t){if(e.length===0)return``;let n=Math.min(28,Math.max(...e.map(([e])=>N(e))));return e.flatMap(([e,r])=>{let i=Ai(e,r,n,t);return i.length>0&&(i[0]=(i[0]??``).replace(e,t.colors.bold(e))),i}).join(`
11
- `)}function Pi(e,t){let n=[t.colors.bold(e.title)];return e.body&&n.push(...P(e.body,t.columns)),e.command&&n.push(t.colors.cyan(Ci(e.command,t.columns))),n.join(`
12
- `)}function Fi(e,t){return e===`pass`?t.colors.green:e===`warning`?t.colors.yellow:e===`fail`?t.colors.red:e===`unknown`?t.colors.blue:e===`info`?t.colors.cyan:t.colors.dim}function Ii(e){return e===`warning`?`WARN`:e===`notApplicable`?`N/A`:e.toUpperCase()}function Li(e,t,n){let r=` `.repeat(t);return P(e,Math.max(1,n-t)).map(e=>`${r}${e}`)}function Ri(e,t){let n=Fi(e.status,t)(`[${Ii(e.status)}]`.padEnd(10)),r=Math.max(1,t.columns-10),i=P(e.title,r).map((e,r)=>`${r===0?n:` `.repeat(10)}${t.colors.bold(e)}`);i.push(...Li(e.explanation,10,t.columns));for(let n of e.evidence??[]){let e=P(n,Math.max(1,t.columns-20));i.push(...e.map((e,n)=>`${` `.repeat(10)}${n===0?`${t.colors.dim(`Evidence`)} `:` `}${e}`))}if(e.fix){let n=P(e.fix,Math.max(1,t.columns-15));i.push(...n.map((e,n)=>`${` `.repeat(10)}${n===0?`${t.colors.cyan(t.colors.bold(`Fix`))} `:` `}${e}`))}return i.join(`
13
- `)}function zi(e,t){return P(e.map(e=>{let n=String(e.value),r=e.status?Fi(e.status,t)(n):t.colors.bold(n);return`${e.label} ${r}`}).join(t.colors.dim(` · `)),t.columns).join(`
14
- `)}function Bi(e,t){let n=Fi(e.status,t),r=[t.colors.bold(e.title)];e.target&&r.push(t.colors.dim(e.target)),r.push(``),r.push(`${n(Ii(e.status))} ${P(e.summary,Math.max(1,t.columns-6)).join(`
10
+ `)}function Mi(e,t,n,r={}){return t.length===0?n.colors.dim(`No results.`):r.layout===`table`||r.layout!==`records`&&Di(e,t,n)?ki(e,t,n):ji(e,t,n,r)}function Ni(e,t){if(e.length===0)return``;let n=Math.min(28,Math.max(...e.map(([e])=>P(e))));return e.flatMap(([e,r])=>{let i=Ai(e,r,n,t);return i.length>0&&(i[0]=(i[0]??``).replace(e,t.colors.bold(e))),i}).join(`
11
+ `)}function Pi(e,t){let n=[t.colors.bold(e.title)];return e.body&&n.push(...F(e.body,t.columns)),e.command&&n.push(t.colors.cyan(Ci(e.command,t.columns))),n.join(`
12
+ `)}function Fi(e,t){return e===`pass`?t.colors.green:e===`warning`?t.colors.yellow:e===`fail`?t.colors.red:e===`unknown`?t.colors.blue:e===`info`?t.colors.cyan:t.colors.dim}function Ii(e){return e===`warning`?`WARN`:e===`notApplicable`?`N/A`:e.toUpperCase()}function Li(e,t,n){let r=` `.repeat(t);return F(e,Math.max(1,n-t)).map(e=>`${r}${e}`)}function Ri(e,t){let n=Fi(e.status,t)(`[${Ii(e.status)}]`.padEnd(10)),r=Math.max(1,t.columns-10),i=F(e.title,r).map((e,r)=>`${r===0?n:` `.repeat(10)}${t.colors.bold(e)}`);i.push(...Li(e.explanation,10,t.columns));for(let n of e.evidence??[]){let e=F(n,Math.max(1,t.columns-20));i.push(...e.map((e,n)=>`${` `.repeat(10)}${n===0?`${t.colors.dim(`Evidence`)} `:` `}${e}`))}if(e.fix){let n=F(e.fix,Math.max(1,t.columns-15));i.push(...n.map((e,n)=>`${` `.repeat(10)}${n===0?`${t.colors.cyan(t.colors.bold(`Fix`))} `:` `}${e}`))}return i.join(`
13
+ `)}function zi(e,t){return F(e.map(e=>{let n=String(e.value),r=e.status?Fi(e.status,t)(n):t.colors.bold(n);return`${e.label} ${r}`}).join(t.colors.dim(` · `)),t.columns).join(`
14
+ `)}function Bi(e,t){let n=Fi(e.status,t),r=[t.colors.bold(e.title)];e.target&&r.push(t.colors.dim(e.target)),r.push(``),r.push(`${n(Ii(e.status))} ${F(e.summary,Math.max(1,t.columns-6)).join(`
15
15
  `)}`),e.metrics?.length&&r.push(zi(e.metrics,t));for(let n of e.sections.filter(e=>e.diagnostics.length>0))r.push(``,t.colors.bold(n.title)),r.push(n.diagnostics.map(e=>Ri(e,t)).join(`
16
16
 
17
- `));if(e.notes?.length){r.push(``,t.colors.bold(`Notes`));for(let n of e.notes){let e=P(n,Math.max(1,t.columns-2));r.push(...e.map((e,t)=>`${t===0?`- `:` `}${e}`))}}return r.join(`
18
- `)}function Vi(e,t,n=`${t}s`){return e===1?t:n}function Hi(e,t,n){let r=` `.repeat(t);return P(e,Math.max(1,n-t)).map(e=>`${r}${e}`)}function Ui(e,t,n){return[t.colors.bold(e),...n?P(n,t.columns):[]].join(`
19
- `)}function Wi(e,t){let n={pass:t.colors.green,warn:t.colors.yellow,fail:t.colors.red};return e.map(e=>{let r=[`${n[e.status](e.status.toUpperCase().padEnd(4))} ${t.colors.bold(e.label)}`];if(r.push(...Hi(e.detail,6,t.columns)),e.fix){let n=P(e.fix,Math.max(1,t.columns-11));r.push(...n.map((e,n)=>`${` `.repeat(6)}${n===0?`${t.colors.bold(`Fix`)} `:` `}${e}`))}return r.join(`
17
+ `));if(e.notes?.length){r.push(``,t.colors.bold(`Notes`));for(let n of e.notes){let e=F(n,Math.max(1,t.columns-2));r.push(...e.map((e,t)=>`${t===0?`- `:` `}${e}`))}}return r.join(`
18
+ `)}function Vi(e,t,n=`${t}s`){return e===1?t:n}function Hi(e,t,n){let r=` `.repeat(t);return F(e,Math.max(1,n-t)).map(e=>`${r}${e}`)}function Ui(e,t,n){return[t.colors.bold(e),...n?F(n,t.columns):[]].join(`
19
+ `)}function Wi(e,t){let n={pass:t.colors.green,warn:t.colors.yellow,fail:t.colors.red};return e.map(e=>{let r=[`${n[e.status](e.status.toUpperCase().padEnd(4))} ${t.colors.bold(e.label)}`];if(r.push(...Hi(e.detail,6,t.columns)),e.fix){let n=F(e.fix,Math.max(1,t.columns-11));r.push(...n.map((e,n)=>`${` `.repeat(6)}${n===0?`${t.colors.bold(`Fix`)} `:` `}${e}`))}return r.join(`
20
20
  `)}).join(`
21
21
 
22
- `)}function Gi(e){let t=e.filter(e=>e.status===`pass`).length,n=e.filter(e=>e.status===`warn`).length,r=e.filter(e=>e.status===`fail`).length;return`${t} ${Vi(t,`check`)} passed, ${n} ${Vi(n,`warning`)}, ${r} failed.`}function Ki(e,t,n){let r=n.columns-t-2-2;return r<24||N(e.id)>t?[...Hi(e.id,2,n.columns).map(e=>n.colors.cyan(e)),...Hi(e.name,4,n.columns)].join(`
23
- `):P(e.name,r).map((r,i)=>{let a=i===0?e.id.padEnd(t):``.padEnd(t);return` ${n.colors.cyan(a)}${` `.repeat(2)}${r}`}).join(`
24
- `)}function qi(e){let t=e.replaceAll(`-`,` `);return t.charAt(0).toUpperCase()+t.slice(1)}function Ji(e,t,n){let r=/* @__PURE__ */ new Map;for(let t of e){let e=r.get(t.category)??[];e.push(t),r.set(t.category,e)}let i=`${e.length} ${Vi(e.length,n.noun)} across ${r.size} ${Vi(r.size,`category`,`categories`)}.`,a=Math.min(30,Math.max(...e.map(e=>N(e.id)),0)),o=Object.keys(n.categoryLabels??{}).filter(e=>r.has(e)),s=[...r.keys()].filter(e=>!o.includes(e));return[i,...[...o,...s].map(e=>{let i=r.get(e)??[],o=n.categoryLabels?.[e]??qi(e);return[t.colors.bold(`${o} (${i.length})`),...i.map(e=>Ki(e,a,t))].join(`
22
+ `)}function Gi(e){let t=e.filter(e=>e.status===`pass`).length,n=e.filter(e=>e.status===`warn`).length,r=e.filter(e=>e.status===`fail`).length;return`${t} ${Vi(t,`check`)} passed, ${n} ${Vi(n,`warning`)}, ${r} failed.`}function Ki(e,t,n){let r=n.columns-t-2-2;return r<24||P(e.id)>t?[...Hi(e.id,2,n.columns).map(e=>n.colors.cyan(e)),...Hi(e.name,4,n.columns)].join(`
23
+ `):F(e.name,r).map((r,i)=>{let a=i===0?e.id.padEnd(t):``.padEnd(t);return` ${n.colors.cyan(a)}${` `.repeat(2)}${r}`}).join(`
24
+ `)}function qi(e){let t=e.replaceAll(`-`,` `);return t.charAt(0).toUpperCase()+t.slice(1)}function Ji(e,t,n){let r=/* @__PURE__ */ new Map;for(let t of e){let e=r.get(t.category)??[];e.push(t),r.set(t.category,e)}let i=`${e.length} ${Vi(e.length,n.noun)} across ${r.size} ${Vi(r.size,`category`,`categories`)}.`,a=Math.min(30,Math.max(...e.map(e=>P(e.id)),0)),o=Object.keys(n.categoryLabels??{}).filter(e=>r.has(e)),s=[...r.keys()].filter(e=>!o.includes(e));return[i,...[...o,...s].map(e=>{let i=r.get(e)??[],o=n.categoryLabels?.[e]??qi(e);return[t.colors.bold(`${o} (${i.length})`),...i.map(e=>Ki(e,a,t))].join(`
25
25
  `)})].join(`
26
26
 
27
27
  `)}function Yi(e,t,n){return e.length===0?t.colors.dim(n.empty):e.map(e=>{let n=[t.colors.bold(e.title)];e.description&&n.push(...Hi(e.description,2,t.columns));let r=e.meta?.filter(Boolean).join(` · `);return r&&n.push(...Hi(r,2,t.columns).map(e=>t.colors.dim(e))),n.join(`
28
28
  `)}).join(`
29
29
 
30
- `)}function Xi(e,t,n){let r=t.filter(e=>e.trim().length>0).flatMap(e=>P(e,n.columns));return[n.colors.bold(e),...r].join(`
31
- `)}function Zi(e,t,n){let r=t.flatMap(e=>P(e,Math.max(1,n.columns-2)).map((e,t)=>`${t===0?`- `:` `}${e}`));return[n.colors.bold(e),...r].join(`
32
- `)}function Qi(e,t){return e.length===0?t.colors.dim(`No parameters.`):Yi(e.map(e=>({title:e.name,description:e.description,meta:[e.type,e.required?`required`:`optional`]})),t,{empty:`No parameters.`})}function F(e={}){return!(e.json||!process.stdin.isTTY||!process.stdout.isTTY||process.env.CI)}function I(e){let t=e&&typeof e==`object`&&!Array.isArray(e)?an():void 0,n=t?{...e,updateNotice:t}:e;process.stdout.write(`${JSON.stringify(n,null,2)}\n`)}function L(e){let t=Ni(e,M());t&&process.stdout.write(`${t}\n`)}function R(e,t){process.stdout.write(`${Mi(e,t,M())}\n`)}function z(e){process.stdout.write(`${Pi(e,M())}\n`)}function $i(e,t){process.stdout.write(`${Ui(e,M(),t)}\n`)}function ea(e){process.stdout.write(`${Wi(e,M())}\n`)}function ta(e){process.stdout.write(`${Bi(e,M())}\n`)}function na(e,t){process.stdout.write(`${Ji(e,M(),t)}\n`)}function ra(e,t){process.stdout.write(`${Yi(e,M(),t)}\n`)}function ia(e,...t){process.stdout.write(`${Xi(e,t,M())}\n`)}function aa(e,t){process.stdout.write(`${Zi(e,t,M())}\n`)}function oa(e){process.stdout.write(`${Qi(e,M())}\n`)}function sa(e){return e<1024?`${e} B`:e<1048576?`${(e/1024).toFixed(1)} KB`:`${(e/1048576).toFixed(1)} MB`}function B(e){return typeof e==`symbol`&&process.exit(1),e}function ca(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 la={site:`--site`,url:`--url`,section:`--section`,date:`--date`,pages:`--pages`,dimensions:`--dimensions`,detail:`--detail`,offset:`--offset`,max_urls:`--max-urls`,mark:`--mark`,issues:`--issues`,note:`--note`,end_date:`--end-date`},ua=e=>/^[\w:/.,<>@#?=&%+-]+$/.test(String(e))?String(e):`"${e}"`,da=(e,t)=>[`seo ${e}`,...Object.entries(t??{}).filter(([,e])=>e!==void 0).map(([e,t])=>`${la[e]??`--${e}`} ${ua(t??``)}`)].join(` `),fa={account:{type:`string`,description:`Saved Google account email.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`},refresh:{type:`boolean`,default:!1,description:`Bypass the local cache and fetch fresh data.`}},pa={"start-date":{type:`string`,description:`Optional YYYY-MM-DD.`},"end-date":{type:`string`,description:`Optional YYYY-MM-DD.`}},ma={detail:{type:`string`,description:`summary (default), rows for the next page, or full.`},offset:{type:`string`,description:`rows only: the first row.`}};function V(e){let t=w(e.detail)??`summary`;if(![`summary`,`rows`,`full`].includes(t))throw new g(`INVALID_INPUT`,`--detail is summary, rows or full.`);return t}function ha(e,t){return _i(t,{refresh:T(e.refresh),account:w(e.account)})}async function H(e,t){try{let n=await t({...Qt({call:da,account:w(e.account),refresh:T(e.refresh)}),progress:ca(!A(e))});A(e)?I(n.data):process.stdout.write(`${n.text}\n`)}catch(e){throw h(e)}}const ga=b({meta:{name:`report`,description:"Start here. No --site: last week across your audited sites (beta, after `seo login`). --site: overview and every section. Add --section for one"},args:{site:{type:`string`,description:"Site, URL or Search Console property. Leave it out for the summary of every audited site (beta, after `seo login`)."},section:{type:`string`,description:`Optional, needs --site. One of ${[...e,Zt,hn].join(`, `)}. issues keeps a list of what was found for the site, on this machine. ${hn} lays the latest Google ranking update (or the latest that started by --end-date) over the site.`},mark:{type:`string`,description:`With --site and --issues: ${bt.join(`, `)}. Records what the person decided; runs no checks.`},issues:{type:`string`,description:`With --mark: issue ids from --section issues, comma-separated.`},note:{type:`string`,description:`Optional, with --mark: why.`},...pa,...ma,...fa},run:async({args:t})=>{let n=w(t.section),r=[...e,Zt,hn];if(n&&!r.includes(n))throw new g(`INVALID_INPUT`,`--section is one of ${r.join(`, `)}.`);let i=w(t.site),a=w(t.mark),o=w(t.issues);if(a||o){if(a&&!bt.includes(a))throw new g(`INVALID_INPUT`,`--mark is one of ${bt.join(`, `)}.`);let e=await ha(t,j(i));await H(t,r=>In({site:e,section:n,mark:a,issues:o?.split(`,`),note:w(t.note)},r));return}if(!i&&!n&&ye()){await H(t,e=>oe(e));return}let s=n===`bing`?await ha(t,i).catch(()=>j(i)):await ha(t,i).catch(async e=>{if(n||!i)throw e;return await H(t,n=>Ft({site:j(i),startDate:w(t[`start-date`]),endDate:w(t[`end-date`])},n).catch(()=>{throw e})),null});s!==null&&await H(t,e=>In({site:s,section:n,startDate:w(t[`start-date`]),endDate:w(t[`end-date`]),detail:V(t),offset:D(t.offset)},e))}}),_a=b({meta:{name:`page`,description:`One URL: live page facts, Google index status and its queries`},args:{url:{type:`string`,description:`The page URL.`,required:!0},...pa,...ma,...fa},run:async({args:e})=>{let t=w(e.url)??``;if(!/^https?:\/\/\S+$/.test(t))throw new g(`INVALID_INPUT`,`--url must be a full http(s) URL.`);let n=await ha(e,t);await H(e,r=>st({site:n,url:t,startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),detail:V(e),offset:D(e.offset)},r))}}),va=b({meta:{name:`compare`,description:`Equal periods before and after a date, with the pages and queries that moved`},args:{site:{type:`string`,description:`Site, URL or Search Console property.`},date:{type:`string`,description:`YYYY-MM-DD: the first day after the change.`,required:!0},pages:{type:`string`,description:`Optional URL text, such as /blog/.`},...fa},run:async({args:e})=>{let t=await ha(e,w(e.site));await H(e,n=>wn({site:t,date:w(e.date)??``,pages:w(e.pages)},n))}}),ya=b({meta:{name:`index`,description:`Google indexing reasons for known URLs, checked live, and pages that lost most of their clicks`},args:{site:{type:`string`,description:`Site, URL or Search Console property.`},"max-urls":{type:`string`,description:`How many URLs to ask Google about, 100 by default. Ask for more when you want a fuller picture; it stops at the URL Inspection quota left today.`},...pa,...ma,...fa},run:async({args:e})=>{let t=await ha(e,w(e.site)),n=D(e[`max-urls`]);if(n!==void 0&&(!Number.isInteger(n)||n<1||n>2e3))throw new g(`INVALID_INPUT`,`--max-urls is a whole number from 1 to ${Mn}.`);await H(e,r=>he({site:t,startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),detail:V(e),offset:D(e.offset),maxUrls:n},r))}}),ba=b({meta:{name:`data`,description:`Raw Search Console or Bing rows beside the previous period, or Clicky landing pages`},args:{site:{type:`string`,description:`Site, URL or Search Console property.`},source:{type:`string`,description:`search_console (default), bing or clicky. Bing takes the first dimension, query or page. Clicky takes the site or its Clicky site ID.`},dimensions:{type:`string`,description:`Comma-separated: ${fn.join(`, `)}. Default query.`},page:{type:`string`,description:`Optional: only rows for pages whose URL contains this.`},query:{type:`string`,description:`Optional: only rows for queries that contain this.`},...pa,...ma,...fa},run:async({args:e})=>{let t=k(e.dimensions)??[`query`],n=t.filter(e=>!fn.includes(e));if(n.length)throw new g(`INVALID_INPUT`,`Unknown dimensions: ${n.join(`, `)}. Use ${fn.join(`, `)}.`);let r=w(e.source)??`search_console`;if(![`search_console`,`bing`,`clicky`].includes(r))throw new g(`INVALID_INPUT`,`--source is search_console, bing or clicky.`);if(r===`clicky`){let t=j(w(e.site));await H(e,async()=>cn({site:t,startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),detail:V(e),offset:D(e.offset)},await tn(),{connect:"save it at https://seoskill.dev/account#keys and run `seo login`, or run `seo analytics clicky connect` to keep it on this machine only.",refresh:Ae,call:e=>da(`data`,{source:`clicky`,site:t,detail:`rows`,offset:e})}));return}if(r===`bing`){let n=j(w(e.site));await H(e,r=>Bn({site:n,dimension:t[0]===`page`?`page`:`query`,startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),detail:V(e),offset:D(e.offset)},r));return}let i=await ha(e,w(e.site)),a=w(e.page),o=w(e.query);await H(e,n=>qt({site:i,dimensions:t,filters:[...a?[{dimension:`page`,operator:`contains`,expression:a}]:[],...o?[{dimension:`query`,operator:`contains`,expression:o}]:[]],startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),detail:V(e),offset:D(e.offset)},n))}}),xa=b({meta:{name:`research`,description:`Keyword, SERP, competitor, backlink and AI answer data from your DataForSEO or Ahrefs key`},args:{action:{type:`positional`,required:!0,description:it.join(`, `)},provider:{type:`string`,description:`${ut.join(`, `)}. Default: the saved key, DataForSEO when both are saved.`},scope:{type:`string`,description:`${Ut.join(`, `)}. Default: from the target. A domain is the whole site, a path ending in / is every URL under it (example.com/blog/), any other path is one page.`},keywords:{type:`string`,description:`Comma-separated keywords.`},tasks:{type:`string`,description:`ai_answers only: comma-separated tasks an earlier run returned as not ready.`},also:{type:`string`,description:`ai_answers only: the brand's other domains, comma-separated, such as claude.ai beside claude.com. A citation to any of them counts as citing the target.`},brand:{type:`string`,description:`ai_answers only: comma-separated brand names that count as a mention, such as Datadog for datadoghq.com. Default: each domain's name.`},target:{type:`string`,description:`Domain, subfolder ending in / or page URL.`},competitors:{type:`string`,description:`link_gap only: 2 to 5 comma-separated competitor domains.`},country:{type:`string`,description:`Two-letter country code. Default us.`},depth:{type:`string`,description:`serp only: results per keyword, 1 to 100. Default 20. Deeper costs more.`},...ma,account:fa.account,json:fa.json,refresh:{type:`boolean`,default:!1,description:`Ask the provider again instead of reading the result saved in the last day.`}},run:async({args:e})=>{let t=w(e.action);if(!it.includes(t))throw new g(`INVALID_INPUT`,`The action is one of ${it.join(`, `)}.`);let n=await tn(),i=w(e.provider);if(i&&!ut.includes(i))throw new g(`INVALID_INPUT`,`--provider is one of ${ut.join(`, `)}.`);let a=w(e.scope);if(a&&!Ut.includes(a))throw new g(`INVALID_INPUT`,`--scope is one of ${Ut.join(`, `)}.`);let o=k(e.keywords)??[],s=w(e.target),c=k(e.competitors)??[],l=w(e.country);if(t===`ai_answers`){if(i===`ahrefs`)throw new g(`INVALID_INPUT`,`ai_answers uses dataforseo.`);let r=k(e.tasks)??[],a=k(e.also),c=k(e.brand),u=o.length||r.length?s??``:await ha(e,s);await H(e,i=>Cn({site:u,country:l??`us`,detail:V(e),offset:D(e.offset)??0,questions:o,tasks:r,also:a,brand:c,waitSeconds:240},n,i,(e,n)=>da(`research`,{action:t,target:s,also:a?.join(`,`),brand:c?.join(`,`),tasks:n.join(`,`)||void 0,country:l,detail:`rows`,offset:e}).replace(`--action `,``)))}else await H(e,()=>Ie({provider:i,scope:a,depth:D(e.depth),action:t,keywords:o,target:s,competitors:c,country:l,detail:V(e),offset:D(e.offset)},n,{store:r(T(e.refresh)),call:n=>da(`research`,{action:t,provider:i,scope:a,depth:D(e.depth),keywords:o.join(`,`)||void 0,target:s,competitors:c.join(`,`)||void 0,country:l,detail:`rows`,offset:n}).replace(`--action `,``)}))}}),Sa=b({meta:{name:`lint`,description:`Check copy for patterns common in AI-written text, weak copy and title and description length`},args:{paths:{type:`positional`,required:!1,description:"Files, folders or globs: .md, .mdx, .html, .astro or .txt. Up to 100 files. `seo lint rules` lists the rules instead (add --only ai for the AI-pattern rules)."},url:{type:`string`,description:`Instead of files: a live page. Checks its main content as Markdown (what seo page --detail full shows), with the blog preset unless you set one.`},only:{type:`string`,description:`Check only these categories, comma-separated: ${gt.join(`, `)}. ai is patterns common in AI-written text; copy is weak copy and house style such as dashes; seo is titles, descriptions and headings.`},preset:{type:`string`,description:`${Zn.join(`, `)}. Default blog for Markdown and text, landing for HTML and Astro. meta reads one title or description per line of a .txt file; start a line with "title:" or "description:" to say which.`},"meta-kind":{type:`string`,description:`With --preset meta: title or description, what a line with no "title:" or "description:" label is. Default: a line up to 70 characters is a title, a longer one a description.`},format:{type:`string`,description:`${Wn.join(`, `)}. Default: from each file name.`},"fail-on":{type:`string`,description:`error (default) or warning: exit with code 1 when a finding at that level or above is found. A file or page that could not be checked in full also exits with code 1.`},ignore:{type:`string`,description:`Rule ids or review lenses to skip, comma-separated.`},"ignore-phrase":{type:`string`,description:`A phrase to allow, such as a product name a rule keeps matching. Repeat it for more.`},json:fa.json},run:async({args:e})=>{let t=w(e.preset);if(t&&!Zn.includes(t))throw new g(`INVALID_INPUT`,`--preset is one of ${Zn.join(`, `)}.`);let n=w(e[`meta-kind`]);if(n&&!wt.includes(n))throw new g(`INVALID_INPUT`,`--meta-kind is ${wt.join(` or `)}.`);let r=w(e.format);if(r&&!Wn.includes(r))throw new g(`INVALID_INPUT`,`--format is one of ${Wn.join(`, `)}.`);let i=w(e[`fail-on`])??`error`;if(i!==`error`&&i!==`warning`)throw new g(`INVALID_INPUT`,`--fail-on is error or warning.`);let a=(e._??[]).filter(e=>typeof e==`string`),o=w(e.url),s=!o&&a.length===1&&a[0]===`rules`&&!v(`rules`);if(!s&&!!o==a.length>0)throw new g(`INVALID_INPUT`,`Give files, folders or globs to check, or --url with a live page, but not both.`);let c=k(e.only);if(c?.filter(e=>!gt.includes(e))?.length)throw new g(`INVALID_INPUT`,`--only takes ${gt.join(`, `)}, comma-separated.`);let u=[].concat(e[`ignore-phrase`]??[]).filter(e=>typeof e==`string`);try{if(s){let t=await kn({rules:!0,only:c});A(e)?I(t.data):process.stdout.write(`${t.text}\n`);return}let{pageMarkdown:d}=Qt({call:da}),f=await kn({paths:o?void 0:a,url:o,preset:t,format:r,ignoreRules:k(e.ignore),ignorePhrases:u,only:c,metaKind:n},e=>l(e),d&&(e=>d(e,2e5)));A(e)?I(f.data):process.stdout.write(`${f.text}\n`);let{errors:ee,warnings:p,skipped:m,partial:h}=f.data;(ee>0||i===`warning`&&p>0||m>0||h>0)&&(process.exitCode=1)}catch(e){throw h(e)}}});function Ca(e){return e===`keychain`?`system keychain`:e===`file`?`private local file`:e===`environment`?Nt:e??`missing`}function wa(e){if(e&&qn(e))return e.trim();throw new g(`INVALID_INPUT`,`No Clicky site given. Pass --site-id with the numeric Clicky site ID.`)}async function Ta(e,t){return f({siteId:e,sitekey:t}).catch(e=>{throw new g(e instanceof Ge&&e.code===`auth`?`AUTH_REQUIRED`:`PROVIDER_UNAVAILABLE`,e instanceof Error?e.message:String(e))})}const Ea={type:`boolean`,default:!1,description:`Print machine-readable JSON.`},Da={type:`string`,description:`Numeric Clicky site ID.`},Oa=b({meta:{name:`connect`,description:`Check and save a Clicky site on this machine`},args:{"site-id":Da,json:Ea},run:async({args:e})=>{let t=A(e),n=F({json:t});n&&Vr(`Connect Clicky`);let r=wa(w(e[`site-id`])??(n?B(await qr({message:`Clicky site ID`,validate:e=>qn(e??``)?void 0:`Enter the numeric site ID from Clicky`})):void 0)),i=process.env.SEO_CLICKY_SITEKEY?.trim()||(n?B(await Gr({message:`Clicky sitekey`,validate:e=>Je(e??``)?void 0:`Enter the sitekey from Clicky (Preferences, Info)`})):void 0);if(!i)throw new g(`AUTH_REQUIRED`,`Run this command in a terminal or set ${Nt}.`);let a=await Ta(r,i.trim()),o=await Yt(r,i.trim()),s={siteId:r,domain:a,connected:!0,credentialSource:o};if(t){I(s);return}let c=`Clicky site ${r}${a?` (${a})`:``} saved in the ${Ca(o)}. Next: seo analytics clicky report --site ${r}`;n?S(c):process.stdout.write(`${c}\n`)}}),ka=b({meta:{name:`status`,description:`Show a Clicky site saved on this machine`},args:{"site-id":Da,check:{type:`boolean`,default:!1,description:`Check the saved sitekey with Clicky.`},json:Ea},run:async({args:e})=>{let t=wa(w(e[`site-id`])),n=await Dn(t),r=T(e.check);r&&n&&await Ta(t,n.siteKey);let i={siteId:t,connected:!!n,credentialSource:n?.source,liveCheck:r?n?`passed`:`unavailable`:`not-requested`};A(e)?I(i):L([[`Site ID`,t],[`Connected`,i.connected?`yes`:`no`],[`Credential`,Ca(i.credentialSource)],[`Live check`,i.liveCheck]])}}),Aa=b({meta:{name:`report`,description:`Clicky landing pages, traffic sources and search engines`},args:{site:{type:`string`,description:`The site, or its numeric Clicky site ID.`},...pa,...ma,json:Ea},run:async({args:e})=>{let t=j(w(e.site));await H(e,async()=>cn({site:t,startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),detail:V(e),offset:D(e.offset)},await tn(),{connect:"save it at https://seoskill.dev/account#keys and run `seo login`, or run `seo analytics clicky connect` to keep it on this machine only.",refresh:Ae,call:e=>`seo analytics clicky report --site ${t} --detail rows --offset ${e}`}))}}),ja=b({meta:{name:`disconnect`,description:`Remove a Clicky site saved on this machine`},args:{"site-id":Da,json:Ea},run:async({args:e})=>{let t=wa(w(e[`site-id`]));await Pn(t);let n=!!process.env[Nt],r={siteId:t,savedCredentialRemoved:!0,environmentCredential:n?`active`:`missing`};A(e)?I(r):process.stdout.write(n?`Saved sitekey removed. ${Nt} is still active.\n`:`Clicky is disconnected for this site.
33
- `)}}),Ma=b({meta:{name:`clicky`,description:`Read Clicky, or save a Clicky site on this machine only`},subCommands:{connect:Oa,status:ka,report:Aa,disconnect:ja}}),Na=b({meta:{name:`properties`,description:`List Google Analytics accounts and properties`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`},account:{type:`string`,description:`Saved Google account email.`}},run:async({args:e})=>{let t=await nn(w(e.account)),n=t.flatMap(e=>e.propertySummaries.map(t=>({account:e.displayName??e.account,property:je(t.property),displayName:t.displayName??t.property})));A(e)?I({accountSummaries:t,properties:n}):R([`Property`,`Name`,`Account`],n.map(e=>[e.property,e.displayName,e.account]))}}),Pa=b({meta:{name:`report`,description:`Google Analytics landing pages for visits from search`},args:{property:{type:`string`,description:"Numeric Google Analytics property ID. Run `seo analytics google properties` to list them."},...pa,...ma,json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=vi(w(e.property));await H(e,async()=>zt({property:t,startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),detail:V(e),offset:D(e.offset)},await Mt(await Ke(),t,Te.signIn),{signIn:Te.signIn,call:e=>`seo analytics google report --property ${t} --detail rows --offset ${e}`}))}}),Fa=b({meta:{name:`google`,description:`Read Google Analytics properties and reports`},subCommands:{properties:Na,report:Pa}}),Ia=b({meta:{name:`analytics`,description:`Read analytics data from connected providers`},subCommands:{clicky:Ma,google:Fa}});function La(){return`Your browser will open. Choose your Google account and select all permission boxes for read-only access. Keep this terminal open and finish the browser steps within five minutes.`}async function Ra(){return x(La(),`Google sign-in`),Oe()}function za(e){return e?e===`environment-json`?`SEO_GOOGLE_SERVICE_ACCOUNT_JSON`:e===`environment-file`?`SEO_GOOGLE_SERVICE_ACCOUNT_FILE`:`GOOGLE_APPLICATION_CREDENTIALS`:`invalid or conflicting configuration`}function Ba(e){return e.active===`keychain`?`system keychain`:e.configured===`keychain`?`private file fallback`:`private file`}function Va(e){return e===`shared`?`SEO Skill app`:`Local OAuth config`}function Ha(e){let t=[[`Account`,e.accountEmail]],n=new Set(e.scope.split(/\s+/u).filter(Boolean));return n.has(`https://www.googleapis.com/auth/webmasters.readonly`)?t.push([`Search Console`,`Read only`]):t.push([`Search Console`,`Not granted`]),n.has(`https://www.googleapis.com/auth/analytics.readonly`)?t.push([`Google Analytics`,`Read only`]):t.push([`Google Analytics`,`Not granted`]),t.push([`OAuth app`,Va(e.clientSource)],[`Expires`,li(e.expiresAt)]),t}const Ua=b({meta:{name:`auth`,description:`Authentication commands`},subCommands:{login:b({meta:{name:`login`,description:`Run Google OAuth flow`},run:async()=>{if((await Pe()).activeMode===`service-account`)throw new g(`AUTH_CONFIG_REQUIRED`,`A service account is active from the environment. Unset its credential variable before running browser OAuth.`);let e=await Ra();L(Ha({accountEmail:e.account_email,clientSource:e.client_source,expiresAt:e.expires_at,scope:e.scope}))}}),logout:b({meta:{name:`logout`,description:`Delete a local Google login`},args:{account:{type:`string`,description:`Delete the login for this Google account.`},all:{type:`boolean`,default:!1,description:`Delete every saved Google login.`}},run:async({args:e})=>{let t=await Pe(),n=w(e.account);if(n&&e.all)throw new g(`INVALID_INPUT`,`Use either --account or --all, not both.`);let r=e.all?void 0:n??t.accounts.find(e=>e.active)?.accountEmail;await pe(r),t.activeMode===`service-account`?process.stdout.write(`${e.all?`Deleted all local Google logins.`:r?`Deleted the local login for ${r}.`:`No local Google login was found.`} Service account credentials remain available through the environment.\n`):process.stdout.write(`${e.all?`Deleted all local Google logins.`:r?`Deleted the local login for ${r}.`:`No local Google login was found.`}\nRevoke at https://myaccount.google.com/permissions if you also want Google to forget the grant.\n`)}}),whoami:b({meta:{name:`whoami`,description:`Show the signed-in Google account`},args:{account:{type:`string`,description:`Show this saved Google account.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Pe();if(t.activeMode===`service-account`){let n={mode:`service-account`,account:t.identity??null,source:za(t.serviceAccount.source)};if(A(e)){I(n);return}L([[`Account type`,`Service account`],[`Account`,t.identity??`Invalid configuration`],[`Credentials`,n.source],[`Access`,`Search Console and Google Analytics, read only`]]);return}let n=w(e.account),r=n?await s(n):t.tokens;if(n&&!r)throw new g(`AUTH_REQUIRED`,`Google account ${n} is not connected.`);if(r)A(e)?I({mode:`oauth`,account:r.account_email,scopes:r.scope.split(/\s+/u).filter(Boolean),clientSource:r.client_source,expiresAt:r.expires_at}):L(Ha({accountEmail:r.account_email,clientSource:r.client_source,expiresAt:r.expires_at,scope:r.scope}));else{if(A(e)){I({mode:`oauth`,account:null});return}process.stdout.write(`Not logged in.
34
- `)}}}),status:b({meta:{name:`status`,description:`Show local Google auth status`},args:{account:{type:`string`,description:`Show status for this saved Google account.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Pe(),n=w(e.account),r=n?await s(n):t.tokens;if(n&&!r&&t.activeMode!==`service-account`)throw new g(`AUTH_REQUIRED`,`Google account ${n} is not connected.`);let i=t.activeMode===`oauth`?await Be():void 0;if(A(e)){I({authenticated:t.activeMode===`oauth`?!!r:t.serviceAccount.configured,mode:t.activeMode,identity:t.identity,account:t.activeMode===`oauth`?r?.account_email:void 0,scopes:t.activeMode===`oauth`?r?.scope:void 0,clientSource:t.activeMode===`oauth`?r?.client_source:void 0,expiresAt:t.activeMode===`oauth`?r?.expires_at:void 0,sharedConfigured:t.sharedConfigured,byoConfigured:t.byoConfigured,serviceAccount:{configured:t.serviceAccount.configured,identity:t.serviceAccount.identity,source:t.serviceAccount.source,error:t.serviceAccount.error},accounts:t.accounts,...i?{tokenStorage:i}:{}});return}if(t.activeMode===`service-account`){let e=[[`Mode`,`service account`],[`Identity`,t.identity??`unavailable`],[`Source`,za(t.serviceAccount.source)],[`Scopes`,`Search Console readonly, Google Analytics readonly`],[`Storage`,`credentials stay in the environment`]];t.serviceAccount.error&&e.push([`Status`,t.serviceAccount.error]),L(e);return}if(!r){let e=t.serviceAccount.error?t.serviceAccount.error:t.sharedConfigured?`Shared seo app available`:t.byoConfigured?`Local OAuth config available`:`No OAuth client configured`;process.stdout.write(`Not logged in. ${e}. Run \`seo auth login\` to connect Google.\n`);return}let a=i??await Be();L([[`Account`,r.account_email],[`Connected accounts`,String(t.accounts.length)],[`Scopes`,r.scope],[`Client`,Va(r.client_source)],[`Expires`,li(r.expires_at)],[`Tokens file`,Sn().tokensFile],[`Token storage`,Ba(a)],...a.reason?[[`Storage note`,a.reason]]:[],[`OAuth mode`,t.sharedConfigured?`SEO Skill app available`:t.byoConfigured?`Local OAuth config available`:`missing`],[`Revoke at`,`https://myaccount.google.com/permissions`]])}}),storage:b({meta:{name:`storage`,description:`Show or change local OAuth token storage`},args:{keychain:{type:`boolean`,default:!1,description:`Store OAuth tokens in the system keychain when available.`},file:{type:`boolean`,default:!1,description:`Store OAuth tokens in the private local file.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{if(e.keychain&&e.file)throw new g(`INVALID_INPUT`,`Choose either --keychain or --file, not both.`);let t=e.keychain||e.file?await ue(e.keychain?`keychain`:`file`):await Be();A(e)?I(t):L([[`Configured storage`,t.configured],[`Active storage`,Ba(t)],...t.reason?[[`Note`,t.reason]]:[]])}}),refresh:b({meta:{name:`refresh`,description:`Refresh the Google OAuth token`},args:{account:{type:`string`,description:`Refresh this saved Google account.`}},run:async({args:e})=>{if((await Pe()).activeMode===`service-account`){process.stdout.write(`Service accounts request short-lived Google tokens automatically. No local refresh token is stored.
35
- `);return}let t=await ie(w(e.account));process.stdout.write(`Refreshed. New expiry ${new Date(t.expires_at).toISOString()}.\n`)}}),accounts:b({meta:{name:`accounts`,description:`List saved Google accounts`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:({args:e})=>{let t=tr();A(e)?I({accounts:t}):t.length===0?process.stdout.write(`No Google accounts are connected.
36
- `):L(t.map(e=>[e.active?`Active account`:`Account`,e.accountEmail]))}}),use:b({meta:{name:`use`,description:`Select the default Google account`},args:{account:{type:`positional`,required:!0,description:`Saved Google account email.`}},run:({args:e})=>{let t=w(e.account);if(!t)throw new g(`INVALID_INPUT`,`Google account is required.`);$e(t),process.stdout.write(`Default Google account: ${t}\n`)}}),"setup-client":b({meta:{name:`setup-client`,description:`Save your own Google Desktop OAuth client`},args:{json:{type:`boolean`,default:!1,description:`Return a structured error instead of prompting.`}},run:async({args:e})=>{if(!F({json:A(e)}))throw new g(`INVALID_INPUT`,"`seo auth setup-client` needs an interactive terminal. Run it without --json, or set SEO_GOOGLE_CLIENT_ID and SEO_GOOGLE_CLIENT_SECRET.");Vr(`Local Google OAuth client`),x(`This is the advanced path.
30
+ `)}function Xi(e,t,n){let r=t.filter(e=>e.trim().length>0).flatMap(e=>F(e,n.columns));return[n.colors.bold(e),...r].join(`
31
+ `)}function Zi(e,t,n){let r=t.flatMap(e=>F(e,Math.max(1,n.columns-2)).map((e,t)=>`${t===0?`- `:` `}${e}`));return[n.colors.bold(e),...r].join(`
32
+ `)}function Qi(e,t){return e.length===0?t.colors.dim(`No parameters.`):Yi(e.map(e=>({title:e.name,description:e.description,meta:[e.type,e.required?`required`:`optional`]})),t,{empty:`No parameters.`})}function I(e={}){return!(e.json||!process.stdin.isTTY||!process.stdout.isTTY||process.env.CI)}function L(e){let t=e&&typeof e==`object`&&!Array.isArray(e)?nn():void 0,n=t?{...e,updateNotice:t}:e;process.stdout.write(`${JSON.stringify(n,null,2)}\n`)}function R(e){let t=Ni(e,N());t&&process.stdout.write(`${t}\n`)}function z(e,t){process.stdout.write(`${Mi(e,t,N())}\n`)}function $i(e){process.stdout.write(`${Pi(e,N())}\n`)}function ea(e,t){process.stdout.write(`${Ui(e,N(),t)}\n`)}function ta(e){process.stdout.write(`${Wi(e,N())}\n`)}function na(e){process.stdout.write(`${Bi(e,N())}\n`)}function ra(e,t){process.stdout.write(`${Ji(e,N(),t)}\n`)}function ia(e,t){process.stdout.write(`${Yi(e,N(),t)}\n`)}function aa(e,...t){process.stdout.write(`${Xi(e,t,N())}\n`)}function oa(e,t){process.stdout.write(`${Zi(e,t,N())}\n`)}function sa(e){process.stdout.write(`${Qi(e,N())}\n`)}function ca(e){return e<1024?`${e} B`:e<1048576?`${(e/1024).toFixed(1)} KB`:`${(e/1048576).toFixed(1)} MB`}function B(e){return typeof e==`symbol`&&process.exit(1),e}function la(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 ua={site:`--site`,url:`--url`,section:`--section`,date:`--date`,pages:`--pages`,dimensions:`--dimensions`,detail:`--detail`,offset:`--offset`,max_urls:`--max-urls`,mark:`--mark`,issues:`--issues`,note:`--note`,end_date:`--end-date`},da=e=>/^[\w:/.,<>@#?=&%+-]+$/.test(String(e))?String(e):`"${e}"`,fa=(e,t)=>[`seo ${e}`,...Object.entries(t??{}).filter(([,e])=>e!==void 0).map(([e,t])=>`${ua[e]??`--${e}`} ${da(t??``)}`)].join(` `),pa={account:{type:`string`,description:`Saved Google account email.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`},refresh:{type:`boolean`,default:!1,description:`Bypass the local cache and fetch fresh data.`}},ma={"start-date":{type:`string`,description:`Optional YYYY-MM-DD.`},"end-date":{type:`string`,description:`Optional YYYY-MM-DD.`}},ha={detail:{type:`string`,description:`summary (default), rows for the next page, or full.`},offset:{type:`string`,description:`rows only: the first row.`}};function V(e){let t=T(e.detail)??`summary`;if(![`summary`,`rows`,`full`].includes(t))throw new g(`INVALID_INPUT`,`--detail is summary, rows or full.`);return t}function ga(e,t){return _i(t,{refresh:E(e.refresh),account:T(e.account)})}async function H(e,t){try{let n=await t({...Xt({call:fa,account:T(e.account),refresh:E(e.refresh)}),progress:la(!j(e))});j(e)?L(n.data):process.stdout.write(`${n.text}\n`)}catch(e){throw m(e)}}const _a=b({meta:{name:`report`,description:"Start here. No --site: last week across your audited sites (beta, after `seo login`). --site: overview and every section. Add --section for one"},args:{site:{type:`string`,description:"Site, URL or Search Console property. Leave it out for the summary of every audited site (beta, after `seo login`)."},section:{type:`string`,description:`Optional, needs --site. One of ${[...e,Yt,pn].join(`, `)}. issues keeps a list of what was found for the site, on this machine. ${pn} lays the latest Google ranking update (or the latest that started by --end-date) over the site.`},mark:{type:`string`,description:`With --site and --issues: ${bt.join(`, `)}. Records what the person decided; runs no checks.`},issues:{type:`string`,description:`With --mark: issue ids from --section issues, comma-separated.`},note:{type:`string`,description:`Optional, with --mark: why.`},...ma,...ha,...pa},run:async({args:t})=>{let n=T(t.section),r=[...e,Yt,pn];if(n&&!r.includes(n))throw new g(`INVALID_INPUT`,`--section is one of ${r.join(`, `)}.`);let i=T(t.site),a=T(t.mark),o=T(t.issues);if(a||o){if(a&&!bt.includes(a))throw new g(`INVALID_INPUT`,`--mark is one of ${bt.join(`, `)}.`);let e=await ga(t,M(i));await H(t,r=>Pn({site:e,section:n,mark:a,issues:o?.split(`,`),note:T(t.note)},r));return}if(!i&&!n&&Cn()){await H(t,e=>oe(e));return}let s=n===`bing`?await ga(t,i).catch(()=>M(i)):await ga(t,i).catch(async e=>{if(n||!i)throw e;return await H(t,n=>Nt({site:M(i),startDate:T(t[`start-date`]),endDate:T(t[`end-date`])},n).catch(()=>{throw e})),null});s!==null&&await H(t,e=>Pn({site:s,section:n,startDate:T(t[`start-date`]),endDate:T(t[`end-date`]),detail:V(t),offset:O(t.offset)},e))}}),va=b({meta:{name:`page`,description:`One URL: live page facts, Google index status and its queries`},args:{url:{type:`string`,description:`The page URL.`,required:!0},...ma,...ha,...pa},run:async({args:e})=>{let t=T(e.url)??``;if(!/^https?:\/\/\S+$/.test(t))throw new g(`INVALID_INPUT`,`--url must be a full http(s) URL.`);let n=await ga(e,t);await H(e,r=>st({site:n,url:t,startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),detail:V(e),offset:O(e.offset)},r))}}),ya=b({meta:{name:`compare`,description:`Equal periods before and after a date, with the pages and queries that moved`},args:{site:{type:`string`,description:`Site, URL or Search Console property.`},date:{type:`string`,description:`YYYY-MM-DD: the first day after the change.`,required:!0},pages:{type:`string`,description:`Optional URL text, such as /blog/.`},...pa},run:async({args:e})=>{let t=await ga(e,T(e.site));await H(e,n=>Sn({site:t,date:T(e.date)??``,pages:T(e.pages)},n))}}),ba=b({meta:{name:`index`,description:`Google indexing reasons for known URLs, checked live, and pages that lost most of their clicks`},args:{site:{type:`string`,description:`Site, URL or Search Console property.`},"max-urls":{type:`string`,description:`How many URLs to ask Google about, 100 by default. Ask for more when you want a fuller picture; it stops at the URL Inspection quota left today.`},...ma,...ha,...pa},run:async({args:e})=>{let t=await ga(e,T(e.site)),n=O(e[`max-urls`]);if(n!==void 0&&(!Number.isInteger(n)||n<1||n>2e3))throw new g(`INVALID_INPUT`,`--max-urls is a whole number from 1 to ${An}.`);await H(e,r=>he({site:t,startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),detail:V(e),offset:O(e.offset),maxUrls:n},r))}}),xa=b({meta:{name:`data`,description:`Raw Search Console or Bing rows beside the previous period, or Clicky landing pages`},args:{site:{type:`string`,description:`Site, URL or Search Console property.`},source:{type:`string`,description:`search_console (default), bing or clicky. Bing takes the first dimension, query or page. Clicky takes the site or its Clicky site ID.`},dimensions:{type:`string`,description:`Comma-separated: ${un.join(`, `)}. Default query.`},page:{type:`string`,description:`Optional: only rows for pages whose URL contains this.`},query:{type:`string`,description:`Optional: only rows for queries that contain this.`},...ma,...ha,...pa},run:async({args:e})=>{let t=A(e.dimensions)??[`query`],n=t.filter(e=>!un.includes(e));if(n.length)throw new g(`INVALID_INPUT`,`Unknown dimensions: ${n.join(`, `)}. Use ${un.join(`, `)}.`);let r=T(e.source)??`search_console`;if(![`search_console`,`bing`,`clicky`].includes(r))throw new g(`INVALID_INPUT`,`--source is search_console, bing or clicky.`);if(r===`clicky`){let t=M(T(e.site));await H(e,async()=>on({site:t,startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),detail:V(e),offset:O(e.offset)},await $t(),{connect:"save it at https://seoskill.dev/account#keys and run `seo login`, or run `seo analytics clicky connect` to keep it on this machine only.",refresh:Ae,call:e=>fa(`data`,{source:`clicky`,site:t,detail:`rows`,offset:e})}));return}if(r===`bing`){let n=M(T(e.site));await H(e,r=>Rn({site:n,dimension:t[0]===`page`?`page`:`query`,startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),detail:V(e),offset:O(e.offset)},r));return}let i=await ga(e,T(e.site)),a=T(e.page),o=T(e.query);await H(e,n=>Gt({site:i,dimensions:t,filters:[...a?[{dimension:`page`,operator:`contains`,expression:a}]:[],...o?[{dimension:`query`,operator:`contains`,expression:o}]:[]],startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),detail:V(e),offset:O(e.offset)},n))}}),Sa=b({meta:{name:`research`,description:`Keyword, SERP, competitor, backlink and AI answer data from your DataForSEO or Ahrefs key`},args:{action:{type:`positional`,required:!0,description:it.join(`, `)},provider:{type:`string`,description:`${ut.join(`, `)}. Default: the saved key, DataForSEO when both are saved.`},scope:{type:`string`,description:`${gt.join(`, `)}. Default: from the target. A domain is the whole site, a path ending in / is every URL under it (example.com/blog/), any other path is one page.`},keywords:{type:`string`,description:`Comma-separated keywords.`},tasks:{type:`string`,description:`ai_answers only: comma-separated tasks an earlier run returned as not ready.`},also:{type:`string`,description:`ai_answers only: the brand's other domains, comma-separated, such as claude.ai beside claude.com. A citation to any of them counts as citing the target.`},brand:{type:`string`,description:`ai_answers only: comma-separated brand names that count as a mention, such as Datadog for datadoghq.com. Default: each domain's name.`},target:{type:`string`,description:`Domain, subfolder ending in / or page URL.`},competitors:{type:`string`,description:`link_gap only: 2 to 5 comma-separated competitor domains.`},country:{type:`string`,description:`Two-letter country code. Default us.`},depth:{type:`string`,description:`serp only: results per keyword, 1 to 100. Default 20. Deeper costs more.`},device:{type:`string`,description:`serp only: desktop (default) or mobile. Google ranks pages differently on each. DataForSEO only.`},...ha,account:pa.account,json:pa.json,refresh:{type:`boolean`,default:!1,description:`Ask the provider again instead of reading the result saved in the last day.`}},run:async({args:e})=>{let t=T(e.action);if(!it.includes(t))throw new g(`INVALID_INPUT`,`The action is one of ${it.join(`, `)}.`);let n=await $t(),i=T(e.provider);if(i&&!ut.includes(i))throw new g(`INVALID_INPUT`,`--provider is one of ${ut.join(`, `)}.`);let a=T(e.scope);if(a&&!gt.includes(a))throw new g(`INVALID_INPUT`,`--scope is one of ${gt.join(`, `)}.`);let o=T(e.device);if(o&&!Vt.includes(o))throw new g(`INVALID_INPUT`,`--device is one of ${Vt.join(`, `)}.`);let s=A(e.keywords)??[],c=T(e.target),l=A(e.competitors)??[],u=T(e.country);if(t===`ai_answers`){if(i===`ahrefs`)throw new g(`INVALID_INPUT`,`ai_answers uses dataforseo.`);let r=A(e.tasks)??[],a=A(e.also),o=A(e.brand),l=s.length||r.length?c??``:await ga(e,c);await H(e,i=>xn({site:l,country:u??`us`,detail:V(e),offset:O(e.offset)??0,questions:s,tasks:r,also:a,brand:o,waitSeconds:240},n,i,(e,n)=>fa(`research`,{action:t,target:c,also:a?.join(`,`),brand:o?.join(`,`),tasks:n.join(`,`)||void 0,country:u,detail:`rows`,offset:e}).replace(`--action `,``)))}else await H(e,()=>Ie({provider:i,scope:a,depth:O(e.depth),device:o,action:t,keywords:s,target:c,competitors:l,country:u,detail:V(e),offset:O(e.offset)},n,{store:r(E(e.refresh)),call:n=>fa(`research`,{action:t,provider:i,scope:a,depth:O(e.depth),device:o,keywords:s.join(`,`)||void 0,target:c,competitors:l.join(`,`)||void 0,country:u,detail:`rows`,offset:n}).replace(`--action `,``)}))}}),Ca=b({meta:{name:`lint`,description:`Check copy for patterns common in AI-written text, weak copy and title and description length`},args:{paths:{type:`positional`,required:!1,description:"Files, folders or globs: .md, .mdx, .html, .astro or .txt. Up to 100 files. `seo lint rules` lists the rules instead (add --only ai for the AI-pattern rules)."},url:{type:`string`,description:`Instead of files: a live page. Checks its main content as Markdown (what seo page --detail full shows), with the blog preset unless you set one.`},only:{type:`string`,description:`Check only these categories, comma-separated: ${Hn.join(`, `)}. ai is patterns common in AI-written text; copy is weak copy and house style such as dashes; seo is titles, descriptions and headings.`},preset:{type:`string`,description:`${Ge.join(`, `)}. Default blog for Markdown and text, landing for HTML and Astro. meta reads one title or description per line of a .txt file; start a line with "title:" or "description:" to say which.`},"meta-kind":{type:`string`,description:`With --preset meta: title or description, what a line with no "title:" or "description:" label is. Default: a line up to 70 characters is a title, a longer one a description.`},format:{type:`string`,description:`${er.join(`, `)}. Default: from each file name.`},"fail-on":{type:`string`,description:`error (default) or warning: exit with code 1 when a finding at that level or above is found. A file or page that could not be checked in full also exits with code 1.`},ignore:{type:`string`,description:`Rule ids or review lenses to skip, comma-separated.`},"ignore-phrase":{type:`string`,description:`A phrase to allow, such as a product name a rule keeps matching. Repeat it for more.`},json:pa.json},run:async({args:e})=>{let t=T(e.preset);if(t&&!Ge.includes(t))throw new g(`INVALID_INPUT`,`--preset is one of ${Ge.join(`, `)}.`);let n=T(e[`meta-kind`]);if(n&&!Xn.includes(n))throw new g(`INVALID_INPUT`,`--meta-kind is ${Xn.join(` or `)}.`);let r=T(e.format);if(r&&!er.includes(r))throw new g(`INVALID_INPUT`,`--format is one of ${er.join(`, `)}.`);let i=T(e[`fail-on`])??`error`;if(i!==`error`&&i!==`warning`)throw new g(`INVALID_INPUT`,`--fail-on is error or warning.`);let a=(e._??[]).filter(e=>typeof e==`string`),o=T(e.url),s=!o&&a.length===1&&a[0]===`rules`&&!v(`rules`);if(!s&&!!o==a.length>0)throw new g(`INVALID_INPUT`,`Give files, folders or globs to check, or --url with a live page, but not both.`);let c=A(e.only);if(c?.filter(e=>!Hn.includes(e))?.length)throw new g(`INVALID_INPUT`,`--only takes ${Hn.join(`, `)}, comma-separated.`);let u=[].concat(e[`ignore-phrase`]??[]).filter(e=>typeof e==`string`);try{if(s){let t=await Dn({rules:!0,only:c});j(e)?L(t.data):process.stdout.write(`${t.text}\n`);return}let{pageMarkdown:d}=Xt({call:fa}),f=await Dn({paths:o?void 0:a,url:o,preset:t,format:r,ignoreRules:A(e.ignore),ignorePhrases:u,only:c,metaKind:n},e=>l(e),d&&(e=>d(e,2e5)));j(e)?L(f.data):process.stdout.write(`${f.text}\n`);let{errors:ee,warnings:p,skipped:te,partial:m}=f.data;(ee>0||i===`warning`&&p>0||te>0||m>0)&&(process.exitCode=1)}catch(e){throw m(e)}}});function wa(e){return e===`keychain`?`system keychain`:e===`file`?`private local file`:e===`environment`?Mn:e??`missing`}function Ta(e){if(e&&Je(e))return e.trim();throw new g(`INVALID_INPUT`,`No Clicky site given. Pass --site-id with the numeric Clicky site ID.`)}async function Ea(e,t){return Kn({siteId:e,sitekey:t}).catch(e=>{throw new g(e instanceof f&&e.code===`auth`?`AUTH_REQUIRED`:`PROVIDER_UNAVAILABLE`,e instanceof Error?e.message:String(e))})}const Da={type:`boolean`,default:!1,description:`Print machine-readable JSON.`},Oa={type:`string`,description:`Numeric Clicky site ID.`},ka=b({meta:{name:`connect`,description:`Check and save a Clicky site on this machine`},args:{"site-id":Oa,json:Da},run:async({args:e})=>{let t=j(e),n=I({json:t});n&&x(`Connect Clicky`);let r=Ta(T(e[`site-id`])??(n?B(await qr({message:`Clicky site ID`,validate:e=>Je(e??``)?void 0:`Enter the numeric site ID from Clicky`})):void 0)),i=process.env.SEO_CLICKY_SITEKEY?.trim()||(n?B(await Gr({message:`Clicky sitekey`,validate:e=>ne(e??``)?void 0:`Enter the sitekey from Clicky (Preferences, Info)`})):void 0);if(!i)throw new g(`AUTH_REQUIRED`,`Run this command in a terminal or set ${Mn}.`);let a=await Ea(r,i.trim()),o=await vt(r,i.trim()),s={siteId:r,domain:a,connected:!0,credentialSource:o};if(t){L(s);return}let c=`Clicky site ${r}${a?` (${a})`:``} saved in the ${wa(o)}. Next: seo analytics clicky report --site ${r}`;n?C(c):process.stdout.write(`${c}\n`)}}),Aa=b({meta:{name:`status`,description:`Show a Clicky site saved on this machine`},args:{"site-id":Oa,check:{type:`boolean`,default:!1,description:`Check the saved sitekey with Clicky.`},json:Da},run:async({args:e})=>{let t=Ta(T(e[`site-id`])),n=await qt(t),r=E(e.check);r&&n&&await Ea(t,n.siteKey);let i={siteId:t,connected:!!n,credentialSource:n?.source,liveCheck:r?n?`passed`:`unavailable`:`not-requested`};j(e)?L(i):R([[`Site ID`,t],[`Connected`,i.connected?`yes`:`no`],[`Credential`,wa(i.credentialSource)],[`Live check`,i.liveCheck]])}}),ja=b({meta:{name:`report`,description:`Clicky landing pages, traffic sources and search engines`},args:{site:{type:`string`,description:`The site, or its numeric Clicky site ID.`},...ma,...ha,json:Da},run:async({args:e})=>{let t=M(T(e.site));await H(e,async()=>on({site:t,startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),detail:V(e),offset:O(e.offset)},await $t(),{connect:"save it at https://seoskill.dev/account#keys and run `seo login`, or run `seo analytics clicky connect` to keep it on this machine only.",refresh:Ae,call:e=>`seo analytics clicky report --site ${t} --detail rows --offset ${e}`}))}}),Ma=b({meta:{name:`disconnect`,description:`Remove a Clicky site saved on this machine`},args:{"site-id":Oa,json:Da},run:async({args:e})=>{let t=Ta(T(e[`site-id`]));await dn(t);let n=!!process.env[Mn],r={siteId:t,savedCredentialRemoved:!0,environmentCredential:n?`active`:`missing`};j(e)?L(r):process.stdout.write(n?`Saved sitekey removed. ${Mn} is still active.\n`:`Clicky is disconnected for this site.
33
+ `)}}),Na=b({meta:{name:`clicky`,description:`Read Clicky, or save a Clicky site on this machine only`},subCommands:{connect:ka,status:Aa,report:ja,disconnect:Ma}}),Pa=b({meta:{name:`properties`,description:`List Google Analytics accounts and properties`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`},account:{type:`string`,description:`Saved Google account email.`}},run:async({args:e})=>{let t=await i(T(e.account)),n=t.flatMap(e=>e.propertySummaries.map(t=>({account:e.displayName??e.account,property:en(t.property),displayName:t.displayName??t.property})));j(e)?L({accountSummaries:t,properties:n}):z([`Property`,`Name`,`Account`],n.map(e=>[e.property,e.displayName,e.account]))}}),Fa=b({meta:{name:`report`,description:`Google Analytics landing pages for visits from search`},args:{property:{type:`string`,description:"Numeric Google Analytics property ID. Run `seo analytics google properties` to list them."},...ma,...ha,json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=vi(T(e.property));await H(e,async()=>Lt({property:t,startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),detail:V(e),offset:O(e.offset)},await At(await Ke(),t,Oe.signIn),{signIn:Oe.signIn,call:e=>`seo analytics google report --property ${t} --detail rows --offset ${e}`}))}}),Ia=b({meta:{name:`google`,description:`Read Google Analytics properties and reports`},subCommands:{properties:Pa,report:Fa}}),La=b({meta:{name:`analytics`,description:`Read analytics data from connected providers`},subCommands:{clicky:Na,google:Ia}});function Ra(){return`Your browser will open. Choose your Google account and select all permission boxes for read-only access. Keep this terminal open and finish the browser steps within five minutes.`}async function za(){return S(Ra(),`Google sign-in`),Pe()}function Ba(e){return e?e===`environment-json`?`SEO_GOOGLE_SERVICE_ACCOUNT_JSON`:e===`environment-file`?`SEO_GOOGLE_SERVICE_ACCOUNT_FILE`:`GOOGLE_APPLICATION_CREDENTIALS`:`invalid or conflicting configuration`}function Va(e){return e.active===`keychain`?`system keychain`:e.configured===`keychain`?`private file fallback`:`private file`}function Ha(e){return e===`shared`?`SEO Skill app`:`Local OAuth config`}function Ua(e){let t=[[`Account`,e.accountEmail]],n=new Set(e.scope.split(/\s+/u).filter(Boolean));return n.has(`https://www.googleapis.com/auth/webmasters.readonly`)?t.push([`Search Console`,`Read only`]):t.push([`Search Console`,`Not granted`]),n.has(`https://www.googleapis.com/auth/analytics.readonly`)?t.push([`Google Analytics`,`Read only`]):t.push([`Google Analytics`,`Not granted`]),t.push([`OAuth app`,Ha(e.clientSource)],[`Expires`,li(e.expiresAt)]),t}const Wa=b({meta:{name:`auth`,description:`Authentication commands`},subCommands:{login:b({meta:{name:`login`,description:`Run Google OAuth flow`},run:async()=>{if((await ie()).activeMode===`service-account`)throw new g(`AUTH_CONFIG_REQUIRED`,`A service account is active from the environment. Unset its credential variable before running browser OAuth.`);let e=await za();R(Ua({accountEmail:e.account_email,clientSource:e.client_source,expiresAt:e.expires_at,scope:e.scope}))}}),logout:b({meta:{name:`logout`,description:`Delete a local Google login`},args:{account:{type:`string`,description:`Delete the login for this Google account.`},all:{type:`boolean`,default:!1,description:`Delete every saved Google login.`}},run:async({args:e})=>{let t=await ie(),n=T(e.account);if(n&&e.all)throw new g(`INVALID_INPUT`,`Use either --account or --all, not both.`);let r=e.all?void 0:n??t.accounts.find(e=>e.active)?.accountEmail;await Se(r),t.activeMode===`service-account`?process.stdout.write(`${e.all?`Deleted all local Google logins.`:r?`Deleted the local login for ${r}.`:`No local Google login was found.`} Service account credentials remain available through the environment.\n`):process.stdout.write(`${e.all?`Deleted all local Google logins.`:r?`Deleted the local login for ${r}.`:`No local Google login was found.`}\nRevoke at https://myaccount.google.com/permissions if you also want Google to forget the grant.\n`)}}),whoami:b({meta:{name:`whoami`,description:`Show the signed-in Google account`},args:{account:{type:`string`,description:`Show this saved Google account.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await ie();if(t.activeMode===`service-account`){let n={mode:`service-account`,account:t.identity??null,source:Ba(t.serviceAccount.source)};if(j(e)){L(n);return}R([[`Account type`,`Service account`],[`Account`,t.identity??`Invalid configuration`],[`Credentials`,n.source],[`Access`,`Search Console and Google Analytics, read only`]]);return}let n=T(e.account),r=n?await $e(n):t.tokens;if(n&&!r)throw new g(`AUTH_REQUIRED`,`Google account ${n} is not connected.`);if(r)j(e)?L({mode:`oauth`,account:r.account_email,scopes:r.scope.split(/\s+/u).filter(Boolean),clientSource:r.client_source,expiresAt:r.expires_at}):R(Ua({accountEmail:r.account_email,clientSource:r.client_source,expiresAt:r.expires_at,scope:r.scope}));else{if(j(e)){L({mode:`oauth`,account:null});return}process.stdout.write(`Not logged in.
34
+ `)}}}),status:b({meta:{name:`status`,description:`Show local Google auth status`},args:{account:{type:`string`,description:`Show status for this saved Google account.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await ie(),n=T(e.account),r=n?await $e(n):t.tokens;if(n&&!r&&t.activeMode!==`service-account`)throw new g(`AUTH_REQUIRED`,`Google account ${n} is not connected.`);let i=t.activeMode===`oauth`?await nr():void 0;if(j(e)){L({authenticated:t.activeMode===`oauth`?!!r:t.serviceAccount.configured,mode:t.activeMode,identity:t.identity,account:t.activeMode===`oauth`?r?.account_email:void 0,scopes:t.activeMode===`oauth`?r?.scope:void 0,clientSource:t.activeMode===`oauth`?r?.client_source:void 0,expiresAt:t.activeMode===`oauth`?r?.expires_at:void 0,sharedConfigured:t.sharedConfigured,byoConfigured:t.byoConfigured,serviceAccount:{configured:t.serviceAccount.configured,identity:t.serviceAccount.identity,source:t.serviceAccount.source,error:t.serviceAccount.error},accounts:t.accounts,...i?{tokenStorage:i}:{}});return}if(t.activeMode===`service-account`){let e=[[`Mode`,`service account`],[`Identity`,t.identity??`unavailable`],[`Source`,Ba(t.serviceAccount.source)],[`Scopes`,`Search Console readonly, Google Analytics readonly`],[`Storage`,`credentials stay in the environment`]];t.serviceAccount.error&&e.push([`Status`,t.serviceAccount.error]),R(e);return}if(!r){let e=t.serviceAccount.error?t.serviceAccount.error:t.sharedConfigured?`Shared seo app available`:t.byoConfigured?`Local OAuth config available`:`No OAuth client configured`;process.stdout.write(`Not logged in. ${e}. Run \`seo auth login\` to connect Google.\n`);return}let a=i??await nr();R([[`Account`,r.account_email],[`Connected accounts`,String(t.accounts.length)],[`Scopes`,r.scope],[`Client`,Ha(r.client_source)],[`Expires`,li(r.expires_at)],[`Tokens file`,ln().tokensFile],[`Token storage`,Va(a)],...a.reason?[[`Storage note`,a.reason]]:[],[`OAuth mode`,t.sharedConfigured?`SEO Skill app available`:t.byoConfigured?`Local OAuth config available`:`missing`],[`Revoke at`,`https://myaccount.google.com/permissions`]])}}),storage:b({meta:{name:`storage`,description:`Show or change local OAuth token storage`},args:{keychain:{type:`boolean`,default:!1,description:`Store OAuth tokens in the system keychain when available.`},file:{type:`boolean`,default:!1,description:`Store OAuth tokens in the private local file.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{if(e.keychain&&e.file)throw new g(`INVALID_INPUT`,`Choose either --keychain or --file, not both.`);let t=e.keychain||e.file?await Xe(e.keychain?`keychain`:`file`):await nr();j(e)?L(t):R([[`Configured storage`,t.configured],[`Active storage`,Va(t)],...t.reason?[[`Note`,t.reason]]:[]])}}),refresh:b({meta:{name:`refresh`,description:`Refresh the Google OAuth token`},args:{account:{type:`string`,description:`Refresh this saved Google account.`}},run:async({args:e})=>{if((await ie()).activeMode===`service-account`){process.stdout.write(`Service accounts request short-lived Google tokens automatically. No local refresh token is stored.
35
+ `);return}let t=await pe(T(e.account));process.stdout.write(`Refreshed. New expiry ${new Date(t.expires_at).toISOString()}.\n`)}}),accounts:b({meta:{name:`accounts`,description:`List saved Google accounts`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:({args:e})=>{let t=s();j(e)?L({accounts:t}):t.length===0?process.stdout.write(`No Google accounts are connected.
36
+ `):R(t.map(e=>[e.active?`Active account`:`Account`,e.accountEmail]))}}),use:b({meta:{name:`use`,description:`Select the default Google account`},args:{account:{type:`positional`,required:!0,description:`Saved Google account email.`}},run:({args:e})=>{let t=T(e.account);if(!t)throw new g(`INVALID_INPUT`,`Google account is required.`);ue(t),process.stdout.write(`Default Google account: ${t}\n`)}}),"setup-client":b({meta:{name:`setup-client`,description:`Save your own Google Desktop OAuth client`},args:{json:{type:`boolean`,default:!1,description:`Return a structured error instead of prompting.`}},run:async({args:e})=>{if(!I({json:j(e)}))throw new g(`INVALID_INPUT`,"`seo auth setup-client` needs an interactive terminal. Run it without --json, or set SEO_GOOGLE_CLIENT_ID and SEO_GOOGLE_CLIENT_SECRET.");x(`Local Google OAuth client`),S(`This is the advanced path.
37
37
  Most users should use the shared seo Google app.
38
- Your actual sensitive data is the local refresh token, not the desktop app client secret.`,`Advanced`);let t=B(await qr({message:`Google Desktop OAuth client ID`,validate:e=>e?void 0:`Client ID is required`})),n=B(await Gr({message:`Google Desktop OAuth client secret`,validate:e=>e?void 0:`Client secret is required`}));Xe({clientId:t,clientSecret:n}),S(`Saved local OAuth config to ${Sn().oauthClientFile}`)}})}}),Wa=[`gsc`,`google-analytics`,`dataforseo`,`ahrefs`,`clicky`,`http`];function Ga(e){if(e!==void 0){if(typeof e==`string`&&Wa.includes(e))return e;throw new g(`INVALID_INPUT`,`--provider must be one of: ${Wa.join(`, `)}.`)}}const Ka=b({meta:{name:`cache`,description:`Cache helpers`},subCommands:{stats:b({meta:{name:`stats`,description:`Show local cache size and row counts`},run:async()=>{let e=$t();L([[`DB`,e.dbPath],[`Size`,sa(e.sizeBytes)],[`Cached data`,`${sa(e.logicalSizeBytes)} of ${sa(e.maxSizeBytes)} automatic limit`],[`Sites`,String(e.counts.sites??0)],[`Search Console`,String(e.counts.gsc_cache??0)],[`URL Inspection`,String(e.counts.url_inspection_cache??0)],[`Google Analytics`,String(e.counts.google_analytics_cache??0)],[`DataForSEO`,String(e.counts.provider_cache??0)],[`Ahrefs`,String(e.counts.ahrefs_cache??0)],[`Clicky`,String(e.counts.clicky_cache??0)],[`HTTP`,String(e.counts.http_cache??0)]])}}),clear:b({meta:{name:`clear`,description:`Clear cached API and HTTP data`},args:{provider:{type:`string`,description:`Optional cache provider: gsc, google-analytics, dataforseo, ahrefs, clicky, or http`}},run:async({args:e})=>{let t=i(Ga(e.provider));process.stdout.write(`Removed ${t} cached rows.\n`)}})}});function U(e){return Math.round(e).toLocaleString(`en-GB`)}function qa(e){return e.toFixed(1)}function W(e){return`${(e*100).toFixed(1)}%`}function G(e,t=72){return Ci(e,t)}function Ja(e){return e.verification?.requested?`${e.verification.verified} checked, ${e.verification.failed} failed`:`off`}function Ya(e){return{title:e.title,target:e.target,status:e.status,summary:e.summary,metrics:e.metrics,sections:(e.diagnostics??[]).map(e=>({title:e.title,diagnostics:e.items})),notes:e.notes}}function K(e){ta(Ya(e))}function q(e,t){R(e,t.slice(0,25)),t.length>25&&process.stdout.write(`Showing 25 of ${t.length}. Use --json for full data.\n`)}function J(e,t,n=5){let r=t.filter(e=>e.action.trim().length>0).slice(0,n),i=Xa(e,r);i&&(process.stdout.write(`
39
- `),ta(i),t.length>r.length&&process.stdout.write(`Showing ${r.length} of ${t.length}. Use --json for full data.\n`))}function Xa(e,t){if(t.length)return{title:e,status:`info`,summary:`${t.length} ${t.length===1?`action`:`actions`} shown.`,sections:[{title:`Actions`,diagnostics:t.map(e=>({status:`info`,title:e.label,explanation:e.context??`Review the evidence before making changes.`,fix:e.action}))}]}}function Za(e){process.stdout.write(`\nNext: ${e}\n`)}function Y(e,t){let n=t.filter(e=>e.trim().length>0);if(n.length){process.stdout.write(`\n${e}\n`);for(let e of n)process.stdout.write(`- ${e}\n`)}}const Qa=b({meta:{name:`content`,description:`Optimize page content from real GSC queries`},subCommands:{optimize:b({meta:{name:`optimize`,description:`Build a content optimization report for one URL`},args:{site:{type:`string`},url:{type:`string`,required:!0},days:{type:`string`,description:`GSC lookback days. Defaults to 28.`},limit:{type:`string`,description:`Maximum GSC queries to inspect. Defaults to 25.`},"min-impressions":{type:`string`,description:`Minimum query impressions. Defaults to 10.`},"include-brand":{type:`boolean`,default:!1,description:`Include branded queries.`},"verify-content":ui(`Fetch the page for content checks. Defaults to true.`,`Skip fetching the page for content checks.`),js:{type:`boolean`,default:!1,description:`Force JavaScript rendering for page extraction.`},refresh:{type:`boolean`,default:!1,description:`Bypass local GSC and HTTP cache.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=A(e),n=j(w(e.site)),r=await Vt({site:n,url:w(e.url)??``,days:D(e.days),limit:D(e.limit),minImpressions:D(e[`min-impressions`]),includeBrand:T(e[`include-brand`]),verifyContent:T(e[`verify-content`])!==!1,js:T(e.js)?!0:`auto`,refresh:T(e.refresh)});t?I(r):(K({title:`Content optimization`,target:r.url,status:`info`,summary:r.summary.verdict,metrics:[{label:`Score`,value:`${r.summary.score}/100`},{label:`Primary intent`,value:r.summary.primaryIntent},{label:`Primary query`,value:r.summary.primaryQuery??`None`},{label:`Queries`,value:U(r.summary.queries)},{label:`Opportunities`,value:U(r.summary.opportunities)},{label:`Estimated lift`,value:U(r.summary.estimatedClickLift)}]}),J(`Top actions`,r.topActions.map(e=>({label:e.title,action:e.action,context:e.plainEnglish}))),r.brief.sections.length&&(process.stdout.write(`
38
+ Your actual sensitive data is the local refresh token, not the desktop app client secret.`,`Advanced`);let t=B(await qr({message:`Google Desktop OAuth client ID`,validate:e=>e?void 0:`Client ID is required`})),n=B(await Gr({message:`Google Desktop OAuth client secret`,validate:e=>e?void 0:`Client secret is required`}));nt({clientId:t,clientSecret:n}),C(`Saved local OAuth config to ${ln().oauthClientFile}`)}})}}),Ga=[`gsc`,`google-analytics`,`dataforseo`,`ahrefs`,`clicky`,`http`];function Ka(e){if(e!==void 0){if(typeof e==`string`&&Ga.includes(e))return e;throw new g(`INVALID_INPUT`,`--provider must be one of: ${Ga.join(`, `)}.`)}}const qa=b({meta:{name:`cache`,description:`Cache helpers`},subCommands:{stats:b({meta:{name:`stats`,description:`Show local cache size and row counts`},run:async()=>{let e=Te();R([[`DB`,e.dbPath],[`Size`,ca(e.sizeBytes)],[`Cached data`,`${ca(e.logicalSizeBytes)} of ${ca(e.maxSizeBytes)} automatic limit`],[`Sites`,String(e.counts.sites??0)],[`Search Console`,String(e.counts.gsc_cache??0)],[`URL Inspection`,String(e.counts.url_inspection_cache??0)],[`Google Analytics`,String(e.counts.google_analytics_cache??0)],[`DataForSEO`,String(e.counts.provider_cache??0)],[`Ahrefs`,String(e.counts.ahrefs_cache??0)],[`Clicky`,String(e.counts.clicky_cache??0)],[`HTTP`,String(e.counts.http_cache??0)]])}}),clear:b({meta:{name:`clear`,description:`Clear cached API and HTTP data`},args:{provider:{type:`string`,description:`Optional cache provider: gsc, google-analytics, dataforseo, ahrefs, clicky, or http`}},run:async({args:e})=>{let t=Zt(Ka(e.provider));process.stdout.write(`Removed ${t} cached rows.\n`)}})}});function U(e){return Math.round(e).toLocaleString(`en-GB`)}function Ja(e){return e.toFixed(1)}function W(e){return`${(e*100).toFixed(1)}%`}function G(e,t=72){return Ci(e,t)}function Ya(e){return e.verification?.requested?`${e.verification.verified} checked, ${e.verification.failed} failed`:`off`}function Xa(e){return{title:e.title,target:e.target,status:e.status,summary:e.summary,metrics:e.metrics,sections:(e.diagnostics??[]).map(e=>({title:e.title,diagnostics:e.items})),notes:e.notes}}function K(e){na(Xa(e))}function q(e,t){z(e,t.slice(0,25)),t.length>25&&process.stdout.write(`Showing 25 of ${t.length}. Use --json for full data.\n`)}function J(e,t,n=5){let r=t.filter(e=>e.action.trim().length>0).slice(0,n),i=Za(e,r);i&&(process.stdout.write(`
39
+ `),na(i),t.length>r.length&&process.stdout.write(`Showing ${r.length} of ${t.length}. Use --json for full data.\n`))}function Za(e,t){if(t.length)return{title:e,status:`info`,summary:`${t.length} ${t.length===1?`action`:`actions`} shown.`,sections:[{title:`Actions`,diagnostics:t.map(e=>({status:`info`,title:e.label,explanation:e.context??`Review the evidence before making changes.`,fix:e.action}))}]}}function Qa(e){process.stdout.write(`\nNext: ${e}\n`)}function Y(e,t){let n=t.filter(e=>e.trim().length>0);if(n.length){process.stdout.write(`\n${e}\n`);for(let e of n)process.stdout.write(`- ${e}\n`)}}const $a=b({meta:{name:`content`,description:`Optimize page content from real GSC queries`},subCommands:{optimize:b({meta:{name:`optimize`,description:`Build a content optimization report for one URL`},args:{site:{type:`string`},url:{type:`string`,required:!0},days:{type:`string`,description:`GSC lookback days. Defaults to 28.`},limit:{type:`string`,description:`Maximum GSC queries to inspect. Defaults to 25.`},"min-impressions":{type:`string`,description:`Minimum query impressions. Defaults to 10.`},"include-brand":{type:`boolean`,default:!1,description:`Include branded queries.`},"verify-content":ui(`Fetch the page for content checks. Defaults to true.`,`Skip fetching the page for content checks.`),js:{type:`boolean`,default:!1,description:`Force JavaScript rendering for page extraction.`},refresh:{type:`boolean`,default:!1,description:`Bypass local GSC and HTTP cache.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=j(e),n=M(T(e.site)),r=await mt({site:n,url:T(e.url)??``,days:O(e.days),limit:O(e.limit),minImpressions:O(e[`min-impressions`]),includeBrand:E(e[`include-brand`]),verifyContent:E(e[`verify-content`])!==!1,js:E(e.js)?!0:`auto`,refresh:E(e.refresh)});t?L(r):(K({title:`Content optimization`,target:r.url,status:`info`,summary:r.summary.verdict,metrics:[{label:`Score`,value:`${r.summary.score}/100`},{label:`Primary intent`,value:r.summary.primaryIntent},{label:`Primary query`,value:r.summary.primaryQuery??`None`},{label:`Queries`,value:U(r.summary.queries)},{label:`Opportunities`,value:U(r.summary.opportunities)},{label:`Estimated lift`,value:U(r.summary.estimatedClickLift)}]}),J(`Top actions`,r.topActions.map(e=>({label:e.title,action:e.action,context:e.plainEnglish}))),r.brief.sections.length&&(process.stdout.write(`
40
40
  Suggested sections
41
41
  `),q([`Heading`,`Queries`],r.brief.sections.map(e=>[G(e.heading,48),G(e.queries.join(`, `),80)]))),r.intentMix.length&&(process.stdout.write(`
42
42
  Intent mix
43
- `),q([`Intent`,`Queries`,`Impressions`,`Clicks`],r.intentMix.map(e=>[e.intent,U(e.queries),U(e.impressions),U(e.clicks)]))),Y(`Caveats`,r.caveats))}})}});var $a=class{stream;buffer=``;constructor(e){this.stream=e}async write(e){this.buffer+=e,this.buffer.length>=65536&&await this.flush()}async flush(){if(!this.buffer)return;let e=this.buffer;this.buffer=``,this.stream.write(e)||await new Promise((e,t)=>{let n=()=>{i(),e()},r=e=>{i(),t(e)},i=()=>{this.stream.off(`drain`,n),this.stream.off(`error`,r)};this.stream.once(`drain`,n),this.stream.once(`error`,r)})}};function eo(e){return` `.repeat(e)}function to(e){return e&&typeof e==`object`&&`toJSON`in e&&typeof e.toJSON==`function`?e.toJSON():e}async function no(e,t,n,r,i=!1){let a=to(t);if(a===null)return await e.write(`null`),!0;if(typeof a==`string`||typeof a==`boolean`)return await e.write(JSON.stringify(a)),!0;if(typeof a==`number`)return await e.write(Number.isFinite(a)?String(a):`null`),!0;if(typeof a==`bigint`&&JSON.stringify(a),a===void 0||typeof a==`function`||typeof a==`symbol`)return i&&await e.write(`null`),i;if(!a||typeof a!=`object`)return!1;if(r.has(a))throw TypeError(`Converting circular structure to JSON`);r.add(a);try{if(Array.isArray(a)){if(a.length===0)return await e.write(`[]`),!0;await e.write(`[
43
+ `),q([`Intent`,`Queries`,`Impressions`,`Clicks`],r.intentMix.map(e=>[e.intent,U(e.queries),U(e.impressions),U(e.clicks)]))),Y(`Caveats`,r.caveats))}})}});var eo=class{stream;buffer=``;constructor(e){this.stream=e}async write(e){this.buffer+=e,this.buffer.length>=65536&&await this.flush()}async flush(){if(!this.buffer)return;let e=this.buffer;this.buffer=``,this.stream.write(e)||await new Promise((e,t)=>{let n=()=>{i(),e()},r=e=>{i(),t(e)},i=()=>{this.stream.off(`drain`,n),this.stream.off(`error`,r)};this.stream.once(`drain`,n),this.stream.once(`error`,r)})}};function to(e){return` `.repeat(e)}function no(e){return e&&typeof e==`object`&&`toJSON`in e&&typeof e.toJSON==`function`?e.toJSON():e}async function ro(e,t,n,r,i=!1){let a=no(t);if(a===null)return await e.write(`null`),!0;if(typeof a==`string`||typeof a==`boolean`)return await e.write(JSON.stringify(a)),!0;if(typeof a==`number`)return await e.write(Number.isFinite(a)?String(a):`null`),!0;if(typeof a==`bigint`&&JSON.stringify(a),a===void 0||typeof a==`function`||typeof a==`symbol`)return i&&await e.write(`null`),i;if(!a||typeof a!=`object`)return!1;if(r.has(a))throw TypeError(`Converting circular structure to JSON`);r.add(a);try{if(Array.isArray(a)){if(a.length===0)return await e.write(`[]`),!0;await e.write(`[
44
44
  `);for(let t=0;t<a.length;t+=1)t>0&&await e.write(`,
45
- `),await e.write(eo(n+1)),await no(e,a[t],n+1,r,!0);return await e.write(`\n${eo(n)}]`),!0}let t=Object.entries(a).filter(([,e])=>{let t=to(e);return![`undefined`,`function`,`symbol`].includes(typeof t)});if(t.length===0)return await e.write(`{}`),!0;await e.write(`{
45
+ `),await e.write(to(n+1)),await ro(e,a[t],n+1,r,!0);return await e.write(`\n${to(n)}]`),!0}let t=Object.entries(a).filter(([,e])=>{let t=no(e);return![`undefined`,`function`,`symbol`].includes(typeof t)});if(t.length===0)return await e.write(`{}`),!0;await e.write(`{
46
46
  `);for(let i=0;i<t.length;i+=1){let[a,o]=t[i];i>0&&await e.write(`,
47
- `),await e.write(`${eo(n+1)}${JSON.stringify(a)}: `),await no(e,o,n+1,r)}return await e.write(`\n${eo(n)}}`),!0}finally{r.delete(a)}}async function ro(e,t){let n=new $a(e);await no(n,t,0,/* @__PURE__ */ new Set)||await n.write(`undefined`),await n.write(`
48
- `),await n.flush()}async function io(e,t){if(!e){await ro(process.stdout,t);return}await Dr(cr(e),{recursive:!0});let n=gr(e);try{await ro(n,t),n.end(),await Zr(n)}catch(e){throw n.destroy(),e}process.stdout.write(`Wrote ${e}\n`)}function ao(e){return e.length?e.join(`, `):`none`}function oo(e,t){let n=e.robotsTxt,r=(n?.botAccess??[]).filter(e=>e.purpose===t),i=t===`search`?`AI search bots`:`AI training bots`;if(!n||r.some(e=>e.allowed===null))return{status:`unknown`,title:`${i}: unknown`,explanation:n?.servesHtml?`robots.txt returned a web page, such as a bot challenge, not a robots.txt file.`:`robots.txt could not be read (${n?.availability??`not checked`}).`};let a=r.filter(e=>e.allowed===!1);return{status:a.length?t===`search`?`warning`:`info`:`pass`,title:a.length?`${i}: ${a.length} of ${r.length} blocked`:`${i}: all ${r.length} allowed`,explanation:a.length?`Blocked: ${ao(a.map(e=>e.userAgent))}.`:`Checked: ${ao(r.map(e=>e.userAgent))}.`}}function so(e){let t=e.markdownNegotiation,n=e.markdownCopies,r=t?.markdown?`Accept: text/markdown returns Markdown${t.varyAccept?` with Vary: Accept`:` without Vary: Accept`}.`:t?.availability===`available`?`Accept: text/markdown returns ${t.contentType||`no content type`}.`:`Accept: text/markdown could not be checked (${t?.availability??`not checked`}).`,i=n?` ${n.publicPagesWithLink} of ${n.publicPages} public pages link to a Markdown copy${n.privatePagesWithLink?`, and ${n.privatePagesWithLink} signed-in pages do`:``}. ${n.checked} copies checked, ${n.broken.length} broken.${n.largest?` Largest checked copy: about ${n.largest.estimatedTokens} tokens.`:``}`:``;return{status:t?.markdown||n?.publicPagesWithLink?n?.broken.length||n?.privatePagesWithLink?`warning`:`pass`:t?.availability===`available`?`info`:`unknown`,title:`Markdown copies`,explanation:`${r}${i}`}}function co(e){let t=e.llmsTxt;if(!t||t.availability===void 0)return{status:`unknown`,title:`llms.txt`,explanation:`Not checked.`};if(t.availability!==`available`)return{status:t.availability===`absent`?`info`:`unknown`,title:`llms.txt`,explanation:t.availability===`absent`?`Not found (HTTP ${t.status??`none`}).`:`Could not be read (${t.availability}).`};let n=t.links;return{status:n?.broken.length?`warning`:`pass`,title:`llms.txt`,explanation:n?`${n.found} same-site links; ${n.checked} checked, ${n.broken.length} broken.`:`Present.`}}function lo(e){if(!e)return[];let t=(e.agentResources??[]).filter(e=>e.exists).map(e=>new URL(e.url).pathname),n=[...e.robotsTxt?.contentSignals??[],...e.startPageHeaders?.contentSignal?[e.startPageHeaders.contentSignal]:[]];return[oo(e,`search`),oo(e,`training`),so(e),co(e),{status:`info`,title:`Agent files`,explanation:`Found: ${ao(t)}.`},{status:`info`,title:`Content-Signal and Link header`,explanation:`Content-Signal: ${ao([...new Set(n)])}. Link header relations on the start page: ${ao(e.startPageHeaders?.linkRelations??[])}.`}]}function uo(e){return e===`high`?`fail`:`warning`}function fo(e,t=xn(e)){let n=Lt(e),r=e.status===`failed`||e.summary.highIssues>0?`fail`:e.status===`partial`?`unknown`:e.summary.mediumIssues>0||e.summary.lowIssues>0?`warning`:`pass`,i=e.access.samples.slice(0,5).map(e=>({status:`fail`,title:e.evidence.guidance.summary,explanation:`${e.evidence.provider} returned ${e.evidence.kind} evidence for the crawler request.`,evidence:[e.url],fix:e.evidence.guidance.recommendedAction})),a=t.slice(0,10).map(e=>({status:uo(e.severity),title:`${e.title} on ${e.count} ${e.count===1?`URL`:`URLs`}`,explanation:We(e.ruleId)?.whyItMatters??e.whyThisRanks,evidence:e.sampleUrls.slice(0,5),fix:e.howToFix})),o=n.slice(0,10).map(e=>({status:`warning`,title:`${e.title} on ${e.count} ${e.count===1?`URL`:`URLs`}`,explanation:We(e.ruleId)?.whyItMatters??e.whyThisRanks,evidence:e.sampleUrls.slice(0,5),fix:e.howToFix}));ta({title:e.config.strategy===`health`?`Sitemap health pass`:`Site crawl`,target:e.config.url,status:r,summary:vt(e)??(e.status===`partial`?`Partial evidence from ${e.summary.totalPages} retained pages. Review the caveats before treating missing findings as an all-clear.`:`${e.summary.totalPages} pages retained with ${e.issues.length} findings.`),metrics:[{label:`Pages`,value:e.summary.totalPages},{label:`High`,value:e.summary.highIssues,status:`fail`},{label:`Medium`,value:e.summary.mediumIssues,status:`warning`},{label:`Low`,value:e.summary.lowIssues,status:`info`},{label:`Failed fetches`,value:e.summary.failedUrls}],sections:[{title:`Access diagnostics`,diagnostics:i},{title:`Prioritised fixes`,diagnostics:a},{title:`Review before changing anything`,diagnostics:o},{title:`Agent access`,diagnostics:lo(e.ai)},...!i.length&&!a.length&&!o.length?[{title:`Checks`,diagnostics:[{status:`pass`,title:`No prioritised findings were observed`,explanation:`The evaluated crawl evidence did not produce a prioritised fix or review item.`}]}]:[]],notes:[`Strategy: ${e.config.strategy}. Data: ${e.status}. Report: ${e.id}.`,...e.warnings,...e.caveats]})}const po={"google-analytics-property":{type:`string`,description:`Google Analytics property ID for landing-page sessions.`},"clicky-site-id":{type:`string`,description:`Clicky site ID for landing-page visits.`}};function mo(e){let t=w(e[`google-analytics-property`]),n=w(e[`clicky-site-id`]);if(t&&n)throw new g(`INVALID_INPUT`,`Pass either --google-analytics-property or --clicky-site-id, not both.`);if(t)return{provider:`google`,propertyId:t};if(n)return{provider:`clicky`,siteId:pn(n)}}const ho=[`health`,`sitemap-url`,`site`,`severity`,`fail-on`,`csv`,`max-pages`,`max-depth`,`include`,`exclude`],go=[`GPTBot`,`OAI-SearchBot`,`ClaudeBot`,`PerplexityBot`,`Google-Extended`,`CCBot`];async function _o(e){process.stdout.write(e)||await new Promise(e=>process.stdout.once(`drain`,e))}async function vo(e,t){let n=w(e.format),r=A(e);if(n&&n!==`jsonl`&&n!==`pretty`)throw new g(`INVALID_INPUT`,`Sites mode writes one JSON row per line. Use --format jsonl (or --json), or --format pretty for counts only.`);if(r&&n===`pretty`)throw new g(`INVALID_INPUT`,`Use either --json or --format, not both.`);for(let t of ho)if(e[t]!==void 0&&e[t]!==!1)throw new g(`INVALID_INPUT`,`--${t} does not apply to sites mode.`);let i=r||n===`jsonl`,a=w(e.output),o=w(e.crawlUrl)??w(e.url),s=o?[o,...t]:t;if(!s.length)throw new g(`INVALID_INPUT`,`No sites given. Pass --urls example.com,example.org or --urls-file with one site per line.`);a&&await Dr(cr(a),{recursive:!0});let c=a?await Or(a,`w`):void 0,l=async e=>{c?await c.write(e):i&&await _o(e)},u=!i&&process.stderr.isTTY,d=0,f;try{f=await En({sites:s,concurrency:D(e.concurrency),respectRobots:!E(e,`robots`),onRow:async e=>{await l(`${JSON.stringify(e)}\n`),d+=1,u&&process.stderr.write(`\rChecked ${d} sites`)}}),u&&d&&process.stderr.write(`
49
- `),await l(`${JSON.stringify(f)}\n`)}finally{await c?.close()}i||bo(f,a),a&&process.stdout.write(`Wrote ${a}\n`)}function yo(e,t,n=[]){return[e,[`${t.available} found`,...n,`${t.absent} missing`,`${t[`access-blocked`]} access blocked`,`${t[`rate-limited`]} rate limited`,`${t.unreachable} unreachable (${t.failed} with no response)`,...t.unreadable?[`${t.unreadable} answered with a web page, so the rules are unknown`]:[]].join(`, `)]}function bo(e,t){let n=[e.invalid?`${e.invalid} not valid sites`:``,e.duplicatesRemoved?`${e.duplicatesRemoved} duplicates removed`:``].filter(Boolean),r=e.homepage;L([[`Sites`,`${e.checked} checked of ${e.sites}${n.length?` (${n.join(`, `)})`:``}. Limit ${be.toLocaleString(`en-US`)} per run.`],yo(`robots.txt`,e.robotsTxt),[`Home page`,[`${r.available} answered 2xx`,`${r.absent} answered 4xx`,`${r[`access-blocked`]} access blocked`,`${r[`rate-limited`]} rate limited`,`${r.unreachable} unreachable (${r.failed} with no response)`,`${r.skipped} not requested because robots.txt disallows it or could not be read`].join(`, `)],yo(`llms.txt`,e.llmsTxt,[`${e.llmsTxt.startsWithH1} start with an H1`]),...e.botAccess.filter(e=>go.includes(e.userAgent)).map(e=>[e.userAgent,`${e.blocked} block it in robots.txt, ${e.allowed} allow it, ${e.unknown} unknown (robots.txt not read)`]),[`Run`,`${e.status}, ${Math.round(e.durationMs/1e3)}s, ${e.concurrency} sites at a time, ${e.timeoutMs/1e3}s per request`]]),t||process.stdout.write(`
47
+ `),await e.write(`${to(n+1)}${JSON.stringify(a)}: `),await ro(e,o,n+1,r)}return await e.write(`\n${to(n)}}`),!0}finally{r.delete(a)}}async function io(e,t){let n=new eo(e);await ro(n,t,0,/* @__PURE__ */ new Set)||await n.write(`undefined`),await n.write(`
48
+ `),await n.flush()}async function ao(e,t){if(!e){await io(process.stdout,t);return}await Or(lr(e),{recursive:!0});let n=_r(e);try{await io(n,t),n.end(),await Zr(n)}catch(e){throw n.destroy(),e}process.stdout.write(`Wrote ${e}\n`)}function oo(e){return e.length?e.join(`, `):`none`}function so(e,t){let n=e.robotsTxt,r=(n?.botAccess??[]).filter(e=>e.purpose===t),i=t===`search`?`AI search bots`:`AI training bots`;if(!n||r.some(e=>e.allowed===null))return{status:`unknown`,title:`${i}: unknown`,explanation:n?.servesHtml?`robots.txt returned a web page, such as a bot challenge, not a robots.txt file.`:`robots.txt could not be read (${n?.availability??`not checked`}).`};let a=r.filter(e=>e.allowed===!1);return{status:a.length?t===`search`?`warning`:`info`:`pass`,title:a.length?`${i}: ${a.length} of ${r.length} blocked`:`${i}: all ${r.length} allowed`,explanation:a.length?`Blocked: ${oo(a.map(e=>e.userAgent))}.`:`Checked: ${oo(r.map(e=>e.userAgent))}.`}}function co(e){let t=e.markdownNegotiation,n=e.markdownCopies,r=t?.markdown?`Accept: text/markdown returns Markdown${t.varyAccept?` with Vary: Accept`:` without Vary: Accept`}.`:t?.availability===`available`?`Accept: text/markdown returns ${t.contentType||`no content type`}.`:`Accept: text/markdown could not be checked (${t?.availability??`not checked`}).`,i=n?` ${n.publicPagesWithLink} of ${n.publicPages} public pages link to a Markdown copy${n.privatePagesWithLink?`, and ${n.privatePagesWithLink} signed-in pages do`:``}. ${n.checked} copies checked, ${n.broken.length} broken.${n.largest?` Largest checked copy: about ${n.largest.estimatedTokens} tokens.`:``}`:``;return{status:t?.markdown||n?.publicPagesWithLink?n?.broken.length||n?.privatePagesWithLink?`warning`:`pass`:t?.availability===`available`?`info`:`unknown`,title:`Markdown copies`,explanation:`${r}${i}`}}function lo(e){let t=e.llmsTxt;if(!t||t.availability===void 0)return{status:`unknown`,title:`llms.txt`,explanation:`Not checked.`};if(t.availability!==`available`)return{status:t.availability===`absent`?`info`:`unknown`,title:`llms.txt`,explanation:t.availability===`absent`?`Not found (HTTP ${t.status??`none`}).`:`Could not be read (${t.availability}).`};let n=t.links;return{status:n?.broken.length?`warning`:`pass`,title:`llms.txt`,explanation:n?`${n.found} same-site links; ${n.checked} checked, ${n.broken.length} broken.`:`Present.`}}function uo(e){if(!e)return[];let t=(e.agentResources??[]).filter(e=>e.exists).map(e=>new URL(e.url).pathname),n=[...e.robotsTxt?.contentSignals??[],...e.startPageHeaders?.contentSignal?[e.startPageHeaders.contentSignal]:[]];return[so(e,`search`),so(e,`training`),co(e),lo(e),{status:`info`,title:`Agent files`,explanation:`Found: ${oo(t)}.`},{status:`info`,title:`Content-Signal and Link header`,explanation:`Content-Signal: ${oo([...new Set(n)])}. Link header relations on the start page: ${oo(e.startPageHeaders?.linkRelations??[])}.`}]}function fo(e){return e===`high`?`fail`:`warning`}function po(e,t=cn(e)){let n=yn(e),r=e.status===`failed`||e.summary.highIssues>0?`fail`:e.status===`partial`?`unknown`:e.summary.mediumIssues>0||e.summary.lowIssues>0?`warning`:`pass`,i=e.access.samples.slice(0,5).map(e=>({status:`fail`,title:e.evidence.guidance.summary,explanation:`${e.evidence.provider} returned ${e.evidence.kind} evidence for the crawler request.`,evidence:[e.url],fix:e.evidence.guidance.recommendedAction})),a=t.slice(0,10).map(e=>({status:fo(e.severity),title:`${e.title} on ${e.count} ${e.count===1?`URL`:`URLs`}`,explanation:d(e.ruleId)?.whyItMatters??e.whyThisRanks,evidence:e.sampleUrls.slice(0,5),fix:e.howToFix})),o=n.slice(0,10).map(e=>({status:`warning`,title:`${e.title} on ${e.count} ${e.count===1?`URL`:`URLs`}`,explanation:d(e.ruleId)?.whyItMatters??e.whyThisRanks,evidence:e.sampleUrls.slice(0,5),fix:e.howToFix}));na({title:e.config.strategy===`health`?`Sitemap health pass`:`Site crawl`,target:e.config.url,status:r,summary:hn(e)??(e.status===`partial`?`Partial evidence from ${e.summary.totalPages} retained pages. Review the caveats before treating missing findings as an all-clear.`:`${e.summary.totalPages} pages retained with ${e.issues.length} findings.`),metrics:[{label:`Pages`,value:e.summary.totalPages},{label:`High`,value:e.summary.highIssues,status:`fail`},{label:`Medium`,value:e.summary.mediumIssues,status:`warning`},{label:`Low`,value:e.summary.lowIssues,status:`info`},{label:`Failed fetches`,value:e.summary.failedUrls}],sections:[{title:`Access diagnostics`,diagnostics:i},{title:`Prioritised fixes`,diagnostics:a},{title:`Review before changing anything`,diagnostics:o},{title:`Agent access`,diagnostics:uo(e.ai)},...!i.length&&!a.length&&!o.length?[{title:`Checks`,diagnostics:[{status:`pass`,title:`No prioritised findings were observed`,explanation:`The evaluated crawl evidence did not produce a prioritised fix or review item.`}]}]:[]],notes:[`Strategy: ${e.config.strategy}. Data: ${e.status}. Report: ${e.id}.`,...e.warnings,...e.caveats]})}const mo={"google-analytics-property":{type:`string`,description:`Google Analytics property ID for landing-page sessions.`},"clicky-site-id":{type:`string`,description:`Clicky site ID for landing-page visits.`}};function ho(e){let t=T(e[`google-analytics-property`]),n=T(e[`clicky-site-id`]);if(t&&n)throw new g(`INVALID_INPUT`,`Pass either --google-analytics-property or --clicky-site-id, not both.`);if(t)return{provider:`google`,propertyId:t};if(n)return{provider:`clicky`,siteId:Tn(n)}}const go=[`health`,`sitemap-url`,`site`,`severity`,`fail-on`,`csv`,`max-pages`,`max-depth`,`include`,`exclude`],_o=[`GPTBot`,`OAI-SearchBot`,`ClaudeBot`,`PerplexityBot`,`Google-Extended`,`CCBot`];async function vo(e){process.stdout.write(e)||await new Promise(e=>process.stdout.once(`drain`,e))}async function yo(e,t){let n=T(e.format),r=j(e);if(n&&n!==`jsonl`&&n!==`pretty`)throw new g(`INVALID_INPUT`,`Sites mode writes one JSON row per line. Use --format jsonl (or --json), or --format pretty for counts only.`);if(r&&n===`pretty`)throw new g(`INVALID_INPUT`,`Use either --json or --format, not both.`);for(let t of go)if(e[t]!==void 0&&e[t]!==!1)throw new g(`INVALID_INPUT`,`--${t} does not apply to sites mode.`);let i=r||n===`jsonl`,a=T(e.output),o=T(e.crawlUrl)??T(e.url),s=o?[o,...t]:t;if(!s.length)throw new g(`INVALID_INPUT`,`No sites given. Pass --urls example.com,example.org or --urls-file with one site per line.`);a&&await Or(lr(a),{recursive:!0});let c=a?await kr(a,`w`):void 0,l=async e=>{c?await c.write(e):i&&await vo(e)},u=!i&&process.stderr.isTTY,d=0,f;try{f=await _e({sites:s,concurrency:O(e.concurrency),respectRobots:!D(e,`robots`),onRow:async e=>{await l(`${JSON.stringify(e)}\n`),d+=1,u&&process.stderr.write(`\rChecked ${d} sites`)}}),u&&d&&process.stderr.write(`
49
+ `),await l(`${JSON.stringify(f)}\n`)}finally{await c?.close()}i||xo(f,a),a&&process.stdout.write(`Wrote ${a}\n`)}function bo(e,t,n=[]){return[e,[`${t.available} found`,...n,`${t.absent} missing`,`${t[`access-blocked`]} access blocked`,`${t[`rate-limited`]} rate limited`,`${t.unreachable} unreachable (${t.failed} with no response)`,...t.unreadable?[`${t.unreadable} answered with a web page, so the rules are unknown`]:[]].join(`, `)]}function xo(e,t){let n=[e.invalid?`${e.invalid} not valid sites`:``,e.duplicatesRemoved?`${e.duplicatesRemoved} duplicates removed`:``].filter(Boolean),r=e.homepage;R([[`Sites`,`${e.checked} checked of ${e.sites}${n.length?` (${n.join(`, `)})`:``}. Limit ${wn.toLocaleString(`en-US`)} per run.`],bo(`robots.txt`,e.robotsTxt),[`Home page`,[`${r.available} answered 2xx`,`${r.absent} answered 4xx`,`${r[`access-blocked`]} access blocked`,`${r[`rate-limited`]} rate limited`,`${r.unreachable} unreachable (${r.failed} with no response)`,`${r.skipped} not requested because robots.txt disallows it or could not be read`].join(`, `)],bo(`llms.txt`,e.llmsTxt,[`${e.llmsTxt.startsWithH1} start with an H1`]),...e.botAccess.filter(e=>_o.includes(e.userAgent)).map(e=>[e.userAgent,`${e.blocked} block it in robots.txt, ${e.allowed} allow it, ${e.unknown} unknown (robots.txt not read)`]),[`Run`,`${e.status}, ${Math.round(e.durationMs/1e3)}s, ${e.concurrency} sites at a time, ${e.timeoutMs/1e3}s per request`]]),t||process.stdout.write(`
50
50
  Add --json for one row per site, or --output <file> to save the rows.
51
- `)}function xo(e){let t=e.trim();if(/^https?:\/\//i.test(t))return t;if(t.startsWith(`sc-domain:`))return`https://${t.slice(10)}/`;if(/^[a-z0-9-]+(\.[a-z0-9-]+)+(:\d+)?(\/\S*)?$/i.test(t))return`https://${t.includes(`/`)?t:`${t}/`}`}function So(e){return e?e.backpressure?.status===`stopped`?`stopped: ${e.backpressure.reason??`origin cooldown`}`:e.backpressure?.status===`slowed`?`slowed ${e.backpressure.delayMs}ms`:e.rendering?.status===`unavailable`?`raw HTML; rendering unavailable`:e.rendering?.status===`failed`?`raw HTML; rendering failed`:e.blocked?`blocked`:e.rendered?`rendered`:e.cache===`hit`?`cached`:e.fetched?`fetched`:e.source:`-`}function Co(e){return e?e===`serp-framing`?`wording gap`:e===`content-gap`?`content gap`:e===`technical-check`?`technical check`:`covered`:`-`}const wo={high:3,medium:2,low:1},To=b({meta:{name:`crawl`,description:`Run a fast sitemap health pass or a full technical crawl`},args:{crawlUrl:{type:`positional`,required:!1,description:`Site start URL. Optional with --sitemap-url.`},url:{type:`string`,description:`Start URL to crawl. Defaults to the --site home page.`},site:{type:`string`,description:`GSC property URL for joining page metrics.`},...po,mode:{type:`string`,description:`Crawl mode: site, page, list, sitemap, or sites. Start with sitemap --health for large sites. sites checks robots.txt, the home page and llms.txt for up to 1,000 sites from --urls or --urls-file.`},health:{type:`boolean`,default:!1,description:`Run the uncached sitemap status and redirect pass before a full audit. No page-body analysis.`},"sitemap-url":{type:`string`,description:`Explicit sitemap URL. Selects sitemap mode when --mode is omitted.`},urls:{type:`string`,description:`Comma-separated URLs for list mode, or sites for sites mode.`},"urls-file":{type:`string`,description:`File with one URL per line for list mode, or one site per line for sites mode.`},"max-pages":{type:`string`,description:`Maximum pages to crawl, up to 10,000. Defaults to 500.`},"max-depth":{type:`string`,description:`Maximum click depth, up to 64. Defaults to 16.`},concurrency:{type:`string`,description:`Parallel page fetches, up to 16. Defaults to 8, or progressively up to 4 with --health. In sites mode, sites checked at once.`},"fetch-interval-cap":{type:`string`,description:`Maximum page fetches per interval per host.`},"fetch-interval-ms":{type:`string`,description:`Fetch rate interval in milliseconds.`},refresh:{type:`boolean`,default:!1,description:`Bypass local HTTP cache and fetch fresh pages.`},include:{type:`string`,description:`Comma-separated URL patterns to include.`},exclude:{type:`string`,description:`Comma-separated URL patterns to exclude.`},"no-sitemap":{type:`boolean`,default:!1,description:`Do not seed URLs from sitemap.xml.`},"no-robots":{type:`boolean`,default:!1,description:`Do not skip URLs disallowed by robots.txt.`},"no-external":{type:`boolean`,default:!1,description:`Do not check external links. Reserved for full link checks.`},js:{type:`boolean`,default:!1,description:`Legacy alias for --rendering on.`},rendering:{type:`string`,description:`JavaScript rendering mode: auto, on, or off. Defaults to auto.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON. Alias for --format json, or --format jsonl in sites mode.`},format:{type:`string`,description:`Output format: pretty, json, csv, html, markdown, or junit. JUnit requires --health. Sites mode takes jsonl or pretty.`},output:{type:`string`,description:`Write output to this path instead of stdout.`},csv:{type:`string`,description:`CSV table to render: issues or pages. Defaults to issues.`},severity:{type:`string`,description:`Only show top fixes at this severity.`},"fail-on":{type:`string`,description:`Exit non-zero when issues exist at this severity or above.`}},run:async({args:e})=>{let t=Eo(e.mode);if(t===`sites`){await vo(e,await jo(e));return}let n=A(e),r=ko(e.format,n),i=w(e.output),a=Ao(e.csv),o=Do(e.severity),s=Do(e[`fail-on`]),c=await jo(e),l=Oo(e),u=w(e[`sitemap-url`]),d=T(e.health);if(r===`junit`&&!d)throw new g(`INVALID_INPUT`,`--format junit requires --health.`);if((d||u)&&t&&t!==`sitemap`)throw new g(`INVALID_INPUT`,`--health and --sitemap-url require sitemap mode.`);if(d&&E(e,`sitemap`))throw new g(`INVALID_INPUT`,`--health cannot be combined with --no-sitemap.`);if((d||u)&&c.length)throw new g(`INVALID_INPUT`,`--health and --sitemap-url cannot be combined with URL list input.`);let f=w(e.site),ee=(l?xo(l)??l:void 0)??(f?xo(f):void 0)??(u?new URL(`/`,u).toString():void 0)??c[0];if(!ee)throw new g(`INVALID_INPUT`,"No crawl URL given. Pass a URL, --url or --site. Run `seo sites` to list your Search Console properties.");let p=await _e({url:ee,site:f,mode:t??(d||u?`sitemap`:c.length?`list`:void 0),strategy:d?`health`:`full`,sitemapUrl:u,urls:c,maxPages:D(e[`max-pages`]),maxDepth:D(e[`max-depth`]),concurrency:D(e.concurrency),refresh:T(e.refresh),fetchRate:hi({...e,"fetch-concurrency":e.concurrency}),include:k(e.include),exclude:k(e.exclude),googleAnalyticsPropertyId:w(e[`google-analytics-property`]),analyticsConnection:mo(e),useSitemap:!E(e,`sitemap`),respectRobots:!E(e,`robots`),checkExternal:!E(e,`external`),js:di(e)}),m=xn(p,{severity:o}),h=s?p.issues.some(e=>wo[e.severity]>=wo[s]):!1,te=p.status===`failed`,ne={...p,topFixes:m,reviewObservations:Lt(p,{severity:o}),...s?{failOn:s,failedThreshold:h}:{}};r===`json`?(await io(i,ne),(te||h)&&(process.exitCode=1)):r===`csv`?(await No(i,a===`pages`?Rn(p):_n(p)),(te||h)&&(process.exitCode=1)):r===`html`?(await No(i,An(p,m)),(te||h)&&(process.exitCode=1)):r===`junit`?(await No(i,Et(p)),(te||h)&&(process.exitCode=1)):r===`markdown`?(await No(i,on(p,m)),(te||h)&&(process.exitCode=1)):i?(await No(i,At(p,m)),(te||h)&&(process.exitCode=1)):(fo(p,m),s&&(process.stdout.write(`
51
+ `)}function So(e){let t=e.trim();if(/^https?:\/\//i.test(t))return t;if(t.startsWith(`sc-domain:`))return`https://${t.slice(10)}/`;if(/^[a-z0-9-]+(\.[a-z0-9-]+)+(:\d+)?(\/\S*)?$/i.test(t))return`https://${t.includes(`/`)?t:`${t}/`}`}function Co(e){return e?e.backpressure?.status===`stopped`?`stopped: ${e.backpressure.reason??`origin cooldown`}`:e.backpressure?.status===`slowed`?`slowed ${e.backpressure.delayMs}ms`:e.rendering?.status===`unavailable`?`raw HTML; rendering unavailable`:e.rendering?.status===`failed`?`raw HTML; rendering failed`:e.blocked?`blocked`:e.rendered?`rendered`:e.cache===`hit`?`cached`:e.fetched?`fetched`:e.source:`-`}function wo(e){return e?e===`serp-framing`?`wording gap`:e===`content-gap`?`content gap`:e===`technical-check`?`technical check`:`covered`:`-`}const To={high:3,medium:2,low:1},Eo=b({meta:{name:`crawl`,description:`Run a fast sitemap health pass or a full technical crawl`},args:{crawlUrl:{type:`positional`,required:!1,description:`Site start URL. Optional with --sitemap-url.`},url:{type:`string`,description:`Start URL to crawl. Defaults to the --site home page.`},site:{type:`string`,description:`GSC property URL for joining page metrics.`},...mo,mode:{type:`string`,description:`Crawl mode: site, page, list, sitemap, or sites. Start with sitemap --health for large sites. sites checks robots.txt, the home page and llms.txt for up to 1,000 sites from --urls or --urls-file.`},health:{type:`boolean`,default:!1,description:`Run the uncached sitemap status and redirect pass before a full audit. No page-body analysis.`},"sitemap-url":{type:`string`,description:`Explicit sitemap URL. Selects sitemap mode when --mode is omitted.`},urls:{type:`string`,description:`Comma-separated URLs for list mode, or sites for sites mode.`},"urls-file":{type:`string`,description:`File with one URL per line for list mode, or one site per line for sites mode.`},"max-pages":{type:`string`,description:`Maximum pages to crawl, up to 10,000. Defaults to 500.`},"max-depth":{type:`string`,description:`Maximum click depth, up to 64. Defaults to 16.`},concurrency:{type:`string`,description:`Parallel page fetches, up to 16. Defaults to 8, or progressively up to 4 with --health. In sites mode, sites checked at once.`},"fetch-interval-cap":{type:`string`,description:`Maximum page fetches per interval per host.`},"fetch-interval-ms":{type:`string`,description:`Fetch rate interval in milliseconds.`},refresh:{type:`boolean`,default:!1,description:`Bypass local HTTP cache and fetch fresh pages.`},include:{type:`string`,description:`Comma-separated URL patterns to include.`},exclude:{type:`string`,description:`Comma-separated URL patterns to exclude.`},"no-sitemap":{type:`boolean`,default:!1,description:`Do not seed URLs from sitemap.xml.`},"no-robots":{type:`boolean`,default:!1,description:`Do not skip URLs disallowed by robots.txt.`},"no-external":{type:`boolean`,default:!1,description:`Do not check external links. Reserved for full link checks.`},js:{type:`boolean`,default:!1,description:`Legacy alias for --rendering on.`},rendering:{type:`string`,description:`JavaScript rendering mode: auto, on, or off. Defaults to auto.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON. Alias for --format json, or --format jsonl in sites mode.`},format:{type:`string`,description:`Output format: pretty, json, csv, html, markdown, or junit. JUnit requires --health. Sites mode takes jsonl or pretty.`},output:{type:`string`,description:`Write output to this path instead of stdout.`},csv:{type:`string`,description:`CSV table to render: issues or pages. Defaults to issues.`},severity:{type:`string`,description:`Only show top fixes at this severity.`},"fail-on":{type:`string`,description:`Exit non-zero when issues exist at this severity or above.`}},run:async({args:e})=>{let t=Do(e.mode);if(t===`sites`){await yo(e,await Mo(e));return}let n=j(e),r=Ao(e.format,n),i=T(e.output),a=jo(e.csv),o=Oo(e.severity),s=Oo(e[`fail-on`]),c=await Mo(e),l=ko(e),u=T(e[`sitemap-url`]),d=E(e.health);if(r===`junit`&&!d)throw new g(`INVALID_INPUT`,`--format junit requires --health.`);if((d||u)&&t&&t!==`sitemap`)throw new g(`INVALID_INPUT`,`--health and --sitemap-url require sitemap mode.`);if(d&&D(e,`sitemap`))throw new g(`INVALID_INPUT`,`--health cannot be combined with --no-sitemap.`);if((d||u)&&c.length)throw new g(`INVALID_INPUT`,`--health and --sitemap-url cannot be combined with URL list input.`);let f=T(e.site),ee=(l?So(l)??l:void 0)??(f?So(f):void 0)??(u?new URL(`/`,u).toString():void 0)??c[0];if(!ee)throw new g(`INVALID_INPUT`,"No crawl URL given. Pass a URL, --url or --site. Run `seo sites` to list your Search Console properties.");let p=await lt({url:ee,site:f,mode:t??(d||u?`sitemap`:c.length?`list`:void 0),strategy:d?`health`:`full`,sitemapUrl:u,urls:c,maxPages:O(e[`max-pages`]),maxDepth:O(e[`max-depth`]),concurrency:O(e.concurrency),refresh:E(e.refresh),fetchRate:hi({...e,"fetch-concurrency":e.concurrency}),include:A(e.include),exclude:A(e.exclude),googleAnalyticsPropertyId:T(e[`google-analytics-property`]),analyticsConnection:ho(e),useSitemap:!D(e,`sitemap`),respectRobots:!D(e,`robots`),checkExternal:!D(e,`external`),js:di(e)}),te=cn(p,{severity:o}),m=s?p.issues.some(e=>To[e.severity]>=To[s]):!1,h=p.status===`failed`,ne={...p,topFixes:te,reviewObservations:yn(p,{severity:o}),...s?{failOn:s,failedThreshold:m}:{}};r===`json`?(await ao(i,ne),(h||m)&&(process.exitCode=1)):r===`csv`?(await Po(i,a===`pages`?Ot(p):On(p)),(h||m)&&(process.exitCode=1)):r===`html`?(await Po(i,wt(p,te)),(h||m)&&(process.exitCode=1)):r===`junit`?(await Po(i,rn(p)),(h||m)&&(process.exitCode=1)):r===`markdown`?(await Po(i,In(p,te)),(h||m)&&(process.exitCode=1)):i?(await Po(i,Ft(p,te)),(h||m)&&(process.exitCode=1)):(po(p,te),s&&(process.stdout.write(`
52
52
  Run metadata
53
- `),L([[`Fail threshold`,s]])),(te||h)&&(process.exitCode=1))}});function Eo(e){let t=w(e);if(t){if([`site`,`page`,`list`,`sitemap`,`sites`].includes(t))return t;throw new g(`INVALID_INPUT`,`Mode must be one of: site, page, list, sitemap, sites.`)}}function Do(e){let t=w(e);if(t){if([`low`,`medium`,`high`].includes(t))return t;throw new g(`INVALID_INPUT`,`Severity must be one of: low, medium, high.`)}}function Oo(e){let t=w(e.crawlUrl),n=w(e.url);if(t&&n&&t!==n)throw new g(`INVALID_INPUT`,`Use either a URL argument or --url, not both.`);return t??n}function ko(e,t){let n=w(e);if(t&&n&&n!==`json`)throw new g(`INVALID_INPUT`,`Use either --json or --format, not both.`);if(!n)return t?`json`:`pretty`;if([`pretty`,`json`,`csv`,`html`,`markdown`,`junit`].includes(n))return n;throw new g(`INVALID_INPUT`,`Format must be one of: pretty, json, csv, html, markdown, junit.`)}function Ao(e){let t=w(e);if(!t)return`issues`;if([`issues`,`pages`].includes(t))return t;throw new g(`INVALID_INPUT`,`CSV table must be one of: issues, pages.`)}async function jo(e){let t=k(e.urls)??[],n=w(e[`urls-file`]);return n&&t.push(...Mo(await y(n,`utf8`))),[...new Set(t)]}function Mo(e){return e.split(/[\n,]/).map(e=>e.trim()).filter(e=>e&&!e.startsWith(`#`))}async function No(e,t){e?(await Dr(cr(e),{recursive:!0}),await Mr(e,t),process.stdout.write(`Wrote ${e}\n`)):process.stdout.write(t)}const Po=b({meta:{name:`gsc-query`,description:`Run a raw Search Console Search Analytics query`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},account:{type:`string`,description:`Saved Google account email.`},"start-date":{type:`string`,description:`Start date YYYY-MM-DD.`},"end-date":{type:`string`,description:`End date YYYY-MM-DD. Without dates: the last 28 days of final data.`},dimensions:{type:`string`,description:`Comma-separated GSC dimensions, for example query,page.`},type:{type:`string`,description:`Search type. Defaults to web.`},limit:{type:`string`,description:`Maximum rows to request and print.`},body:{type:`string`,description:`Raw Search Analytics JSON body.`},"body-file":{type:`string`,description:`Path to a Search Analytics JSON body file.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`},refresh:{type:`boolean`,default:!1,description:`Bypass local cache and fetch fresh GSC data.`}},run:async({args:e})=>{let t=await gi(e.body,e[`body-file`])??{startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),dimensions:k(e.dimensions)??[`query`,`page`],type:w(e.type)??`web`,rowLimit:D(e.limit),dataState:`final`};try{Object.assign(t,dn({startDate:w(t.startDate),endDate:w(t.endDate)},28))}catch(e){throw h(e)}let n=A(e),r=j(w(e.site)??w(t.siteUrl));delete t.siteUrl;let i=await St(r,t,{refresh:T(e.refresh),accountEmail:w(e.account)}),a=D(e.limit),o=a?i.rows.slice(0,a):i.rows;n?I({site:r,request:t,...i,rows:o,rowsReturned:o.length}):(K({title:`Search Console query`,target:r,status:i.rowsFetched>o.length?`unknown`:`info`,summary:`${o.length} rows returned from ${i.rowsFetched} fetched rows.`,metrics:[{label:`Rows returned`,value:o.length},{label:`Rows fetched`,value:i.rowsFetched},{label:`API calls`,value:i.calls}]}),R([`Keys`,`Clicks`,`Impr`,`CTR`,`Pos`],o.slice(0,25).map(e=>[e.keys.join(` | `),Math.round(e.clicks),Math.round(e.impressions),e.ctr.toFixed(3),e.position.toFixed(1)])))}}),Fo=b({meta:{name:`url-inspect`,description:`Inspect a URL with the Search Console URL Inspection API`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},account:{type:`string`,description:`Saved Google account email.`},url:{type:`string`,description:`URL to inspect.`},language:{type:`string`,description:`Optional inspection language code.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=j(w(e.site)),r=w(e.url);if(!r)throw Error(`Pass --url.`);let i=await Yn({siteUrl:n,inspectionUrl:r,accountEmail:w(e.account),languageCode:w(e.language)});if(t){I(i);return}let a=i.inspectionResult?.indexStatusResult;K({title:`URL Inspection`,target:r,status:a===void 0?`unknown`:a.verdict===`PASS`?`pass`:`warning`,summary:a===void 0?`Google did not return index status evidence for this URL.`:`${a.verdict??`Unknown`}: ${a.coverageState??`Coverage state unavailable`}.`,metrics:[{label:`Property`,value:n},{label:`Robots`,value:a?.robotsTxtState??`Unknown`},{label:`Last crawl`,value:a?.lastCrawlTime??`Unknown`},{label:`Google canonical`,value:a?.googleCanonical??`Unknown`}]})}}),Io={days:{cli:`days`,arg:{type:`string`,description:`GSC lookback window. Defaults to 28.`}},recentDays:{cli:`recent`,arg:{type:`string`,description:`Recent anomaly window in days. Defaults to 14.`}},range:{cli:`days`,arg:{type:`string`,description:`GSC lookback window. Defaults to 28.`}},limit:{cli:`limit`,arg:{type:`string`,description:`Maximum rows to return.`}},checkLimit:{cli:`check-limit`,arg:{type:`string`,description:`Maximum candidate pages to fetch and check.`}},minImpressions:{cli:`min-impressions`,arg:{type:`string`,description:`Minimum query impressions.`}},includeBrand:{cli:`include-brand`,arg:{type:`boolean`,default:!1,description:`Include branded queries in opportunity reports.`}},verifyContent:{cli:`verify-content`,arg:{type:`boolean`,default:!1,description:`Verify top opportunities against page title, meta, and content.`}},verifyLimit:{cli:`verify-limit`,arg:{type:`string`,description:`Maximum opportunity URLs to verify.`}},js:{cli:`js`,arg:{type:`boolean`,default:!1,description:`Force JavaScript rendering for verified pages.`}},fetchConcurrency:{cli:`fetch-concurrency`,arg:{type:`string`,description:`Maximum concurrent page fetches per host, up to 16. Defaults to 4.`}},fetchIntervalCap:{cli:`fetch-interval-cap`,arg:{type:`string`,description:`Maximum page fetches per interval per host. Defaults to 4.`}},fetchIntervalMs:{cli:`fetch-interval-ms`,arg:{type:`string`,description:`Fetch rate interval in milliseconds. Defaults to 1000.`}},refresh:{cli:`refresh`,arg:{type:`boolean`,default:!1,description:`Bypass local cache and fetch fresh data.`}}};function X(e,t={}){return Object.fromEntries(e.map(e=>{let n=Io[e];return[n.cli,{...n.arg,...t[e]}]}))}function Lo(e){return e.toLocaleString(`en-GB`,{maximumFractionDigits:e>=100?0:1})}function Ro(e){if(e===null)return`n/a`;let t=Math.abs(e)>=10?e.toFixed(0):e.toFixed(1);return`${e>0?`+`:``}${t}%`}const zo=b({meta:{name:`traffic-anomaly`,description:`Detect unusual recent GSC traffic movement`},args:{site:{type:`string`},...X([`days`,`recentDays`,`refresh`],{days:{description:`Baseline window length in days. Defaults to 90.`},recentDays:{description:`Recent anomaly window in days. Defaults to 7.`},refresh:{description:`Bypass local cache and fetch fresh GSC data.`}}),json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=await me({site:j(w(e.site)),days:D(e.days),recentDays:D(e.recent),refresh:T(e.refresh)});t?I(n):(K({title:`Traffic anomaly report`,target:n.site,status:n.coverage?.status===`complete`?n.anomalies.some(e=>e.significant)?`warning`:`pass`:`unknown`,summary:n.anomalies.some(e=>e.significant)?`At least one recent metric moved beyond the report threshold.`:`No significant recent movement was found in the retained evidence.`,metrics:[{label:`Metrics checked`,value:n.anomalies.length},{label:`Significant`,value:n.anomalies.filter(e=>e.significant).length},{label:`Observed days`,value:n.coverage?.observedDays??`Unavailable`},{label:`Expected days`,value:n.coverage?.expectedDays??`Unavailable`}]}),q([`Metric`,`Direction`,`Baseline/day`,`Recent/day`,`Change`,`z`,`Significant`],n.anomalies.map(e=>[e.metric,e.direction,Lo(e.baselineMean),Lo(e.comparisonMean),Ro(e.percentChange),e.zScore??`n/a`,e.significant?`yes`:`no`])),n.coverage&&Y(`Evidence coverage`,[`${n.coverage.observedDays} of ${n.coverage.expectedDays} requested calendar days returned date aggregates.`,...n.coverage.caveats]))}}),Bo=b({meta:{name:`update-correlate`,description:`Compare traffic movement with official Google update windows`},args:{site:{type:`string`},...X([`days`,`recentDays`,`refresh`],{days:{description:`Baseline window length in days. Defaults to 90.`},recentDays:{description:`Recent anomaly window in days. Defaults to 7.`},refresh:{description:`Bypass local cache and fetch fresh GSC data.`}}),"padding-days":{type:`string`,description:`Days around update windows to count as overlap.`},"known-change":{type:`string`,description:`Manual site-side change to treat as a confounder, for example pruning pages or blocking traffic.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=j(w(e.site)),r=await Ce({site:n,days:D(e.days),recentDays:D(e.recent),paddingDays:D(e[`padding-days`]),knownConfounders:w(e[`known-change`])?[w(e[`known-change`])??``]:void 0,refresh:T(e.refresh)});t?I(r):(K({title:`Google update correlation`,target:r.site,status:`unknown`,summary:r.summary,metrics:[{label:`Classification`,value:r.classification.replaceAll(`-`,` `)},{label:`Attribution`,value:r.attribution.replaceAll(`-`,` `)},{label:`Confidence`,value:r.confidence},{label:`Updates matched`,value:r.overlappingUpdates.length},{label:`Known confounders`,value:r.confounders.length},{label:`Source`,value:r.source.name}]}),Y(`Evidence`,r.evidence),Y(`Report caveats`,r.caveats),Y(`Recommended next checks`,r.actions),q([`Metric`,`Direction`,`Baseline/day`,`Recent/day`,`Change`,`z`],r.anomalies.map(e=>[e.metric,e.direction,Lo(e.baselineMean),Lo(e.comparisonMean),Ro(e.percentChange),e.zScore??`n/a`])),r.overlappingUpdates.length&&(process.stdout.write(`
53
+ `),R([[`Fail threshold`,s]])),(h||m)&&(process.exitCode=1))}});function Do(e){let t=T(e);if(t){if([`site`,`page`,`list`,`sitemap`,`sites`].includes(t))return t;throw new g(`INVALID_INPUT`,`Mode must be one of: site, page, list, sitemap, sites.`)}}function Oo(e){let t=T(e);if(t){if([`low`,`medium`,`high`].includes(t))return t;throw new g(`INVALID_INPUT`,`Severity must be one of: low, medium, high.`)}}function ko(e){let t=T(e.crawlUrl),n=T(e.url);if(t&&n&&t!==n)throw new g(`INVALID_INPUT`,`Use either a URL argument or --url, not both.`);return t??n}function Ao(e,t){let n=T(e);if(t&&n&&n!==`json`)throw new g(`INVALID_INPUT`,`Use either --json or --format, not both.`);if(!n)return t?`json`:`pretty`;if([`pretty`,`json`,`csv`,`html`,`markdown`,`junit`].includes(n))return n;throw new g(`INVALID_INPUT`,`Format must be one of: pretty, json, csv, html, markdown, junit.`)}function jo(e){let t=T(e);if(!t)return`issues`;if([`issues`,`pages`].includes(t))return t;throw new g(`INVALID_INPUT`,`CSV table must be one of: issues, pages.`)}async function Mo(e){let t=A(e.urls)??[],n=T(e[`urls-file`]);return n&&t.push(...No(await y(n,`utf8`))),[...new Set(t)]}function No(e){return e.split(/[\n,]/).map(e=>e.trim()).filter(e=>e&&!e.startsWith(`#`))}async function Po(e,t){e?(await Or(lr(e),{recursive:!0}),await Nr(e,t),process.stdout.write(`Wrote ${e}\n`)):process.stdout.write(t)}const Fo=b({meta:{name:`gsc-query`,description:`Run a raw Search Console Search Analytics query`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},account:{type:`string`,description:`Saved Google account email.`},"start-date":{type:`string`,description:`Start date YYYY-MM-DD.`},"end-date":{type:`string`,description:`End date YYYY-MM-DD. Without dates: the last 28 days of final data.`},dimensions:{type:`string`,description:`Comma-separated GSC dimensions, for example query,page.`},type:{type:`string`,description:`Search type. Defaults to web.`},limit:{type:`string`,description:`Maximum rows to request and print.`},body:{type:`string`,description:`Raw Search Analytics JSON body.`},"body-file":{type:`string`,description:`Path to a Search Analytics JSON body file.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`},refresh:{type:`boolean`,default:!1,description:`Bypass local cache and fetch fresh GSC data.`}},run:async({args:e})=>{let t=await gi(e.body,e[`body-file`])??{startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),dimensions:A(e.dimensions)??[`query`,`page`],type:T(e.type)??`web`,rowLimit:O(e.limit),dataState:`final`};try{Object.assign(t,Wt({startDate:T(t.startDate),endDate:T(t.endDate)},28))}catch(e){throw m(e)}let n=j(e),r=M(T(e.site)??T(t.siteUrl));delete t.siteUrl;let i=await Jn(r,t,{refresh:E(e.refresh),accountEmail:T(e.account)}),a=O(e.limit),o=a?i.rows.slice(0,a):i.rows;n?L({site:r,request:t,...i,rows:o,rowsReturned:o.length}):(K({title:`Search Console query`,target:r,status:i.rowsFetched>o.length?`unknown`:`info`,summary:`${o.length} rows returned from ${i.rowsFetched} fetched rows.`,metrics:[{label:`Rows returned`,value:o.length},{label:`Rows fetched`,value:i.rowsFetched},{label:`API calls`,value:i.calls}]}),z([`Keys`,`Clicks`,`Impr`,`CTR`,`Pos`],o.slice(0,25).map(e=>[e.keys.join(` | `),Math.round(e.clicks),Math.round(e.impressions),e.ctr.toFixed(3),e.position.toFixed(1)])))}}),Io=b({meta:{name:`url-inspect`,description:`Inspect a URL with the Search Console URL Inspection API`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},account:{type:`string`,description:`Saved Google account email.`},url:{type:`string`,description:`URL to inspect.`},language:{type:`string`,description:`Optional inspection language code.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=M(T(e.site)),r=T(e.url);if(!r)throw Error(`Pass --url.`);let i=await Ue({siteUrl:n,inspectionUrl:r,accountEmail:T(e.account),languageCode:T(e.language)});if(t){L(i);return}let a=i.inspectionResult?.indexStatusResult;K({title:`URL Inspection`,target:r,status:a===void 0?`unknown`:a.verdict===`PASS`?`pass`:`warning`,summary:a===void 0?`Google did not return index status evidence for this URL.`:`${a.verdict??`Unknown`}: ${a.coverageState??`Coverage state unavailable`}.`,metrics:[{label:`Property`,value:n},{label:`Robots`,value:a?.robotsTxtState??`Unknown`},{label:`Last crawl`,value:a?.lastCrawlTime??`Unknown`},{label:`Google canonical`,value:a?.googleCanonical??`Unknown`}]})}}),Lo={days:{cli:`days`,arg:{type:`string`,description:`GSC lookback window. Defaults to 28.`}},recentDays:{cli:`recent`,arg:{type:`string`,description:`Recent anomaly window in days. Defaults to 14.`}},range:{cli:`days`,arg:{type:`string`,description:`GSC lookback window. Defaults to 28.`}},limit:{cli:`limit`,arg:{type:`string`,description:`Maximum rows to return.`}},checkLimit:{cli:`check-limit`,arg:{type:`string`,description:`Maximum candidate pages to fetch and check.`}},minImpressions:{cli:`min-impressions`,arg:{type:`string`,description:`Minimum query impressions.`}},includeBrand:{cli:`include-brand`,arg:{type:`boolean`,default:!1,description:`Include branded queries in opportunity reports.`}},verifyContent:{cli:`verify-content`,arg:{type:`boolean`,default:!1,description:`Verify top opportunities against page title, meta, and content.`}},verifyLimit:{cli:`verify-limit`,arg:{type:`string`,description:`Maximum opportunity URLs to verify.`}},js:{cli:`js`,arg:{type:`boolean`,default:!1,description:`Force JavaScript rendering for verified pages.`}},fetchConcurrency:{cli:`fetch-concurrency`,arg:{type:`string`,description:`Maximum concurrent page fetches per host, up to 16. Defaults to 4.`}},fetchIntervalCap:{cli:`fetch-interval-cap`,arg:{type:`string`,description:`Maximum page fetches per interval per host. Defaults to 4.`}},fetchIntervalMs:{cli:`fetch-interval-ms`,arg:{type:`string`,description:`Fetch rate interval in milliseconds. Defaults to 1000.`}},refresh:{cli:`refresh`,arg:{type:`boolean`,default:!1,description:`Bypass local cache and fetch fresh data.`}}};function X(e,t={}){return Object.fromEntries(e.map(e=>{let n=Lo[e];return[n.cli,{...n.arg,...t[e]}]}))}function Ro(e){return e.toLocaleString(`en-GB`,{maximumFractionDigits:e>=100?0:1})}function zo(e){if(e===null)return`n/a`;let t=Math.abs(e)>=10?e.toFixed(0):e.toFixed(1);return`${e>0?`+`:``}${t}%`}const Bo=b({meta:{name:`traffic-anomaly`,description:`Detect unusual recent GSC traffic movement`},args:{site:{type:`string`},...X([`days`,`recentDays`,`refresh`],{days:{description:`Baseline window length in days. Defaults to 90.`},recentDays:{description:`Recent anomaly window in days. Defaults to 7.`},refresh:{description:`Bypass local cache and fetch fresh GSC data.`}}),json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=await Ce({site:M(T(e.site)),days:O(e.days),recentDays:O(e.recent),refresh:E(e.refresh)});t?L(n):(K({title:`Traffic anomaly report`,target:n.site,status:n.coverage?.status===`complete`?n.anomalies.some(e=>e.significant)?`warning`:`pass`:`unknown`,summary:n.anomalies.some(e=>e.significant)?`At least one recent metric moved beyond the report threshold.`:`No significant recent movement was found in the retained evidence.`,metrics:[{label:`Metrics checked`,value:n.anomalies.length},{label:`Significant`,value:n.anomalies.filter(e=>e.significant).length},{label:`Observed days`,value:n.coverage?.observedDays??`Unavailable`},{label:`Expected days`,value:n.coverage?.expectedDays??`Unavailable`}]}),q([`Metric`,`Direction`,`Baseline/day`,`Recent/day`,`Change`,`z`,`Significant`],n.anomalies.map(e=>[e.metric,e.direction,Ro(e.baselineMean),Ro(e.comparisonMean),zo(e.percentChange),e.zScore??`n/a`,e.significant?`yes`:`no`])),n.coverage&&Y(`Evidence coverage`,[`${n.coverage.observedDays} of ${n.coverage.expectedDays} requested calendar days returned date aggregates.`,...n.coverage.caveats]))}}),Vo=b({meta:{name:`update-correlate`,description:`Compare traffic movement with official Google update windows`},args:{site:{type:`string`},...X([`days`,`recentDays`,`refresh`],{days:{description:`Baseline window length in days. Defaults to 90.`},recentDays:{description:`Recent anomaly window in days. Defaults to 7.`},refresh:{description:`Bypass local cache and fetch fresh GSC data.`}}),"padding-days":{type:`string`,description:`Days around update windows to count as overlap.`},"known-change":{type:`string`,description:`Manual site-side change to treat as a confounder, for example pruning pages or blocking traffic.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=M(T(e.site)),r=await Ve({site:n,days:O(e.days),recentDays:O(e.recent),paddingDays:O(e[`padding-days`]),knownConfounders:T(e[`known-change`])?[T(e[`known-change`])??``]:void 0,refresh:E(e.refresh)});t?L(r):(K({title:`Google update correlation`,target:r.site,status:`unknown`,summary:r.summary,metrics:[{label:`Classification`,value:r.classification.replaceAll(`-`,` `)},{label:`Attribution`,value:r.attribution.replaceAll(`-`,` `)},{label:`Confidence`,value:r.confidence},{label:`Updates matched`,value:r.overlappingUpdates.length},{label:`Known confounders`,value:r.confounders.length},{label:`Source`,value:r.source.name}]}),Y(`Evidence`,r.evidence),Y(`Report caveats`,r.caveats),Y(`Recommended next checks`,r.actions),q([`Metric`,`Direction`,`Baseline/day`,`Recent/day`,`Change`,`z`],r.anomalies.map(e=>[e.metric,e.direction,Ro(e.baselineMean),Ro(e.comparisonMean),zo(e.percentChange),e.zScore??`n/a`])),r.overlappingUpdates.length&&(process.stdout.write(`
54
54
  Updates
55
- `),R([`Start`,`End`,`Type`,`Name`],r.overlappingUpdates.map(e=>[e.start.slice(0,10),e.end?.slice(0,10)??`open`,e.type,e.name]))),Za(`seo segment-impact --site ${JSON.stringify(n)} --dimension page`))}}),Vo={site:{type:`string`,description:`Search Console property, for example sc-domain:example.com.`}};function Ho(e){return{type:`string`,description:`Output folder. Defaults to ./seo-export/<site>-${e}-YYYY-MM-DD.`}}function Uo(e){return e.toLowerCase().replace(/^sc-domain:/,``).replace(/^https?:\/\//,``).replace(/[^a-z0-9._-]+/g,`-`).replace(/^-+|-+$/g,``)}function Wo(e){let t=(/* @__PURE__ */ new Date()).toISOString().slice(0,10),n=Uo(e.site);return dr(process.cwd(),`seo-export`,`${n}-${e.report}-${t}`)}async function Go(e,t){await Dr(e,{recursive:!0});let n=[];for(let r of t){let t=dr(e,r.filename);await Mr(t,Gt(r.rows,r.headers),`utf8`),n.push(t)}return n}function Ko(e,t){process.stdout.write(`Wrote ${rt(t.length,`CSV file`)} to ${e}\n`);for(let e of t)process.stdout.write(`- ${e}\n`)}const qo=b({meta:{name:`refresh-priorities`,description:`Export the ranked SEO priority queue, score breakdowns, grouped findings, and diagnosis detail CSVs.`},args:{...Vo,out:Ho(`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.`}}),...po,"verify-content":ui(`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=j(w(e.site)),n=await m({site:t,days:D(e.days),recentDays:D(e.recent),limit:D(e.limit),includeBrand:T(e[`include-brand`]),googleAnalyticsPropertyId:w(e[`google-analytics-property`]),analyticsConnection:mo(e),verifyContent:T(e[`verify-content`]),verifyLimit:D(e[`verify-limit`]),refresh:T(e.refresh)}),r=w(e.out)??Wo({report:`refresh-priorities`,site:t});Ko(r,await Go(r,un(n)))}}),Jo=b({meta:{name:`update-postmortem`,description:`Export update postmortem attribution, findings, template movement, and segment movers.`},args:{...Vo,out:Ho(`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.`}},run:async({args:e})=>{let t=j(w(e.site)),n=await ne({site:t,days:D(e.days),recentDays:D(e.recent),limit:D(e.limit),includeBrand:T(e[`include-brand`]),knownConfounders:w(e[`known-change`])?[w(e[`known-change`])??``]:void 0,refresh:T(e.refresh)}),r=w(e.out)??Wo({report:`update-postmortem`,site:t});Ko(r,await Go(r,ai(n)))}}),Yo=b({meta:{name:`export`,description:`Export full report data to CSV files`},subCommands:{"update-postmortem":Jo,"refresh-priorities":qo}});function Xo(e,t,n={}){if(process.platform!==`win32`)return Ir(e,t,n);let r=e=>/^[\w./:=@\\-]+$/.test(e)?e:`"${e.replaceAll(`"`,`""`)}"`;return Ir([e,...t].map(r).join(` `),{...n,shell:!0})}function Zo(e){let t=e.platform===`win32`?pr:lr;return e.platform===`win32`?e.env.APPDATA??t.resolve(e.home,`AppData/Roaming`):e.platform===`darwin`?t.resolve(e.home,`Library/Application Support`):e.env.XDG_CONFIG_HOME??t.resolve(e.home,`.config`)}function Qo(e={}){let t=e.home??ar(),n=e.platform??process.platform,r=Zo({home:t,platform:n,env:e.env??process.env}),i=n===`win32`?pr:lr,a=[];if(n===`darwin`||n===`win32`){let e=i.resolve(r,`Claude`);a.push({kind:`json`,client:`claude-desktop`,label:`Claude Desktop`,path:i.resolve(e,`claude_desktop_config.json`),commandNames:[],detectionPaths:n===`darwin`?[e,`/Applications/Claude.app`]:[e]})}return a.push({kind:`json`,client:`claude-code`,label:`Claude Code`,path:i.resolve(t,`.claude.json`),includeType:!0,commandNames:[`claude`],detectionPaths:[i.resolve(t,`.claude`)]},{kind:`native`,client:`codex`,label:`Codex`,path:i.resolve(t,`.codex/config.toml`),executable:`codex`,commandNames:[`codex`],detectionPaths:[i.resolve(t,`.codex`)]},{kind:`json`,client:`cursor`,label:`Cursor`,path:i.resolve(t,`.cursor/mcp.json`),commandNames:[`cursor`,`cursor-agent`],detectionPaths:[i.resolve(t,`.cursor`),...n===`darwin`?[`/Applications/Cursor.app`]:[]]}),a}function $o(e,t={}){let n=t.platform??process.platform,r=t.exists??v,i=t.nodePath??process.execPath,a=n===`win32`?[`${e}.cmd`,`${e}.exe`,e]:[e],o=(t.path??process.env.PATH??``).split(n===`win32`?`;`:`:`).filter(Boolean);for(let e of o)for(let t of a){let n=_(e,t);if(r(n))return n}for(let e of a){let t=_(cr(i),e);if(r(t))return t}return e}function es(e){return $o(e)!==e}function ts(e={}){let t=e.targets??Qo(),n=e.hasCommand??es,r=e.hasPath??v;return t.filter(e=>e.kind===`native`?e.commandNames.some(n):r(e.path)||e.detectionPaths.some(r)||e.commandNames.some(n))}const ns={insertSpaces:!0,tabSize:2,eol:`
56
- `};function rs(){return(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g,`-`)}function is(e,t){_r(cr(e),{recursive:!0,mode:448});let n=`${e}.${process.pid}.${Date.now()}.tmp`,r=v(e)?Cr(e).mode&511:384;wr(n,t,{encoding:`utf8`,mode:r}),xr(n,e)}function as(e){let t=`${e}.bak-${rs()}`,n=t,r=2;for(;v(n);)n=`${t}-${r}`,r+=1;hr(e,n,mr.COPYFILE_EXCL)}function os(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ss(e){if(!v(e.path))return{source:`{}
57
- `,config:{}};let t=vr(e.path,`utf8`),n=[],r=ei(t,n,{allowTrailingComma:!0});if(n.length>0){let t=n[0],r=t?`${ti(t.error)} at offset ${t.offset}`:`unknown parse error`;throw Error(`Cannot update ${e.path}: invalid JSONC (${r}).`)}if(!os(r))throw Error(`Cannot update ${e.path}: config root must be an object.`);if(r.mcpServers!==void 0&&!os(r.mcpServers))throw Error(`Cannot update ${e.path}: mcpServers must be an object.`);return{source:t,config:r}}function cs(e){return e.includes(`mcp`)&&e.includes(`serve`)&&e.some(e=>e===`seo`||e.startsWith(`seo@`)||e===`@seo/cli`)}function ls(e,t){return typeof e.command!=`string`||t.length!==3||t[1]!==`mcp`||t[2]!==`serve`||!os(e.env)?!1:e.env.SEO_MCP_CONFIGURED_BY===`seo`}function us(e){if(!os(e)||!Array.isArray(e.args))return!1;let t=e.args.filter(e=>typeof e==`string`);return cs(t)||ls(e,t)}function ds(e){let t=Qr(e.source,$r(e.source,[`mcpServers`,e.name??`seo`],e.value,{formattingOptions:ns}));v(e.target.path)&&as(e.target.path),is(e.target.path,t)}function fs(e,t){let n=Xo(e,t,{encoding:`utf8`});return{status:n.status,stdout:String(n.stdout??``),stderr:String(n.stderr??``),error:n.error}}function ps(e){let t=e.replaceAll(`\\`,`/`).toLowerCase();return t.includes(`/_npx/`)||t.includes(`/.npm/_npx/`)||t.includes(`/.cache/pnpm/dlx/`)}function ms(e){if(!e||!v(e))return;let t=br(e);return ps(t)?void 0:t}function hs(e={}){let t=ms(e.cliPath??process.argv[1]);if(t)return{command:e.nodePath??process.execPath,args:[t,`mcp`,`serve`]};let n=`seo@${e.version??sn}`;return(e.platform??process.platform)===`win32`?{command:`cmd`,args:[`/c`,`npx`,`-y`,n,`mcp`,`serve`]}:{command:`npx`,args:[`-y`,n,`mcp`,`serve`]}}function gs(e=!1,t=hs()){let n=Sn(),r={command:t.command,args:t.args,env:{SEO_CONFIG_DIR:n.configDir,SEO_MCP_CONFIGURED_BY:`seo`}};return e&&(r.type=`stdio`),r}function _s(e){return e.includes(`SEO_MCP_CONFIGURED_BY`)||/args:\s+-y\s+(?:seo(?:@\S+)?|@seo\/cli)\s+mcp\s+serve\b/.test(e)}function vs(e,t,n){let r=$o(e.executable),i=n.runCommand(r,[`mcp`,`get`,`seo`]);if(i.error?.code===`ENOENT`)throw Error("Cannot install SEO MCP for Codex: the `codex` command is not available on PATH. Install Codex CLI or choose another MCP client.");let a=i.status===0;if(a&&!_s(i.stdout))throw Error(`Cannot ${t} SEO MCP: ${e.path} already has an unmanaged mcp_servers.seo entry.`);if(t===`install`&&a&&!n.reinstall||t===`uninstall`&&!a)return{client:e.client,path:e.path,changed:!1};if(t===`install`&&a){let e=n.runCommand(r,[`mcp`,`remove`,`seo`]);if(e.error||e.status!==0){let t=e.error?.message??e.stderr.trim()??`unknown error`;throw Error(`Cannot reinstall SEO MCP for Codex: ${t}`)}}let o=Sn(),s=t===`install`?[`mcp`,`add`,`seo`,`--env`,`SEO_CONFIG_DIR=${o.configDir}`,`--env`,`SEO_MCP_CONFIGURED_BY=seo`,`--`,n.serverCommand.command,...n.serverCommand.args]:[`mcp`,`remove`,`seo`],c=n.runCommand(r,s);if(c.error||c.status!==0){let e=c.error?.message??c.stderr.trim()??`unknown error`;throw Error(`Cannot ${t} SEO MCP for Codex: ${e}`)}return{client:e.client,path:e.path,changed:!0}}function ys(e,t={}){let n={runCommand:t.runCommand??fs,reinstall:t.reinstall??!1,serverCommand:t.serverCommand??hs()};if(e.kind===`native`)return vs(e,`install`,n);let{source:r,config:i}=ss(e),a=i.mcpServers?.seo,o=gs(e.includeType,n.serverCommand);if(Nr(a,o)&&!n.reinstall)return{client:e.client,path:e.path,changed:!1};if(a!==void 0&&!us(a))throw Error(`Cannot install SEO MCP: ${e.path} already has an unmanaged mcpServers.seo entry.`);return ds({target:e,source:r,value:o}),{client:e.client,path:e.path,changed:!0}}function bs(e,t={}){if(e.kind===`native`)return vs(e,`uninstall`,{runCommand:t.runCommand??fs,reinstall:!1,serverCommand:hs()});if(!v(e.path))return{client:e.client,path:e.path,changed:!1};let{source:n,config:r}=ss(e),i=r.mcpServers;if(i?.seo===void 0)return{client:e.client,path:e.path,changed:!1};if(!us(i.seo))throw Error(`Cannot uninstall SEO MCP: ${e.path} has an unmanaged mcpServers.seo entry.`);return ds({target:e,source:n,value:void 0}),{client:e.client,path:e.path,changed:!0}}const xs=`https://seoskill.dev/mcp`,Ss=`seoskill`;function Cs(e){return e.client!==`claude-desktop`}function ws(e){return e.includeType?{type:`http`,url:xs}:{url:xs}}function Ts(e){return os(e)&&e.url===`https://seoskill.dev/mcp`}function Es(e,t={}){if(!Cs(e))throw Error(`${e.label} takes the hosted server from its Connectors settings, not its config file.`);if(e.kind===`native`){let n=t.runCommand??fs,r=$o(e.executable),i=n(r,[`mcp`,`get`,Ss]);if(i.error?.code===`ENOENT`)throw Error("Cannot add the hosted MCP server for Codex: the `codex` command is not available on PATH.");if(i.status===0){if(i.stdout.includes(`https://seoskill.dev/mcp`))return{client:e.client,path:e.path,changed:!1};throw Error(`Cannot add the hosted MCP server: ${e.path} already has a different mcp_servers.${Ss} entry.`)}let a=n(r,[`mcp`,`add`,Ss,`--url`,xs]);if(a.error||a.status!==0){let e=a.error?.message??a.stderr.trim()??`unknown error`;throw Error(`Cannot add the hosted MCP server for Codex: ${e}`)}return{client:e.client,path:e.path,changed:!0}}let{source:n,config:r}=ss(e),i=r.mcpServers?.[Ss];if(Ts(i))return{client:e.client,path:e.path,changed:!1};if(i!==void 0)throw Error(`Cannot add the hosted MCP server: ${e.path} already has a different mcpServers.${Ss} entry.`);return ds({target:e,source:n,value:ws(e),name:Ss}),{client:e.client,path:e.path,changed:!0}}const Ds=e=>process.stdout.write(`${e}\n`);async function Os(){let{google:e,bing:t,keys:n}=await se();return{status:`linked`,google:e.map(e=>e.email),bing:t.length,keys:Object.keys(n).filter(e=>e!==`x-dataforseo-login`&&!e.startsWith(`x-clicky-`)),clicky:Object.keys(n).filter(e=>e.startsWith(Ot)).length}}function ks(e){return[`Linked to your seoskill.dev account.`,`Google: ${e.google.join(`, `)||`none`}.`,`Bing: ${e.bing?`${e.bing} linked`:`not connected (add it at ${nt}/account)`}.`,`Keys: ${e.keys.length?e.keys.map(e=>e.replace(/^x-|-(key|password)$/g,``)).join(`, `):`none`}.`,`Clicky: ${e.clicky?`${e.clicky} site${e.clicky===1?``:`s`}`:`none`}.`,`Next: seo sites, then seo report --site <site>.`].join(`
58
- `)}const As=b({meta:{name:`login`,description:`Link this machine to your seoskill.dev account: its Google accounts, Bing and keys`},args:{json:{type:`boolean`,default:!1,description:`Print JSON.`}},run:async({args:e})=>{let t=A(e),n=async()=>{let e=await Os();t?I(e):Ds(ks(e))};if(process.env.SEO_TOKEN?.trim()){await n();return}if(dt()){let e=await ge();if(e===`linked`)return n();if(e===`pending`&&!F({json:t})){let e=dt();t?I({status:`pending`,...Ms(e)}):Ds(`Waiting for approval. Open ${e?.url} and click "Link this machine", then run \`seo login\` again.`);return}}else if(ye()){await n();return}let r=dt()??await Le();if(!F({json:t}))t?I({status:`pending`,...Ms(r)}):Ds([`Open this link in the person's browser and ask them to approve it:`,r.url,`Code: ${r.code}. After they approve, run \`seo login\` again.`].join(`
59
- `));else{if(await js())return n();Ds("The login expired. Run `seo login` again."),process.exitCode=1}}});async function js(){let e=dt()??await Le();for(Ds(`Opening ${e.url}\nApprove code ${e.code} there. Waiting…`),await at(e.url);dt();){await new Promise(e=>setTimeout(e,3e3));let e=await ge();if(e===`linked`)return!0;if(e===`expired`)break}return!1}const Ms=e=>({url:e?.url,code:e?.code}),Ns=b({meta:{name:`logout`,description:`Unlink this machine from your seoskill.dev account`},args:{json:{type:`boolean`,default:!1,description:`Print JSON.`}},run:async({args:e})=>{let t=await Tn();A(e)?I({unlinked:t}):Ds(t?`This machine is no longer linked to your seoskill.dev account.`:`This machine was not linked.`)}});function Ps(e={}){let t=Xo(`npx`,[`-y`,`skills`,`add`,`iannuttall/seo`],{stdio:e.quiet?`ignore`:`inherit`});return t.error?{status:`failed`,error:t.error.message}:t.status===0?{status:`installed`}:{status:`failed`,error:`npx skills add exited with status ${t.status??`unknown`}.`}}async function Fs(e){let t=async()=>{let{google:e}=await se().catch(()=>({google:[]}));return{status:`linked`,accountEmail:e.map(e=>e.email).join(`, `)||void 0}};if(process.env.SEO_TOKEN?.trim()||ye())return t();if(!e.interactive)return null;let n=B(await Kr({message:`How do you want to sign in?`,options:[{value:`account`,label:`Sign in or create a free seoskill.dev account`,hint:`Recommended. Your Google accounts, Bing and keys in one place, for the CLI and the hosted MCP`},{value:`local`,label:`Sign in with Google on this machine only`},{value:`skip`,label:`Skip for now`}]}));return n===`skip`?{status:`skipped`}:n===`local`?null:await js()?t():(x("The link expired. Run `seo login` to try again.",`Not signed in`),{status:`skipped`})}function Is(e){let t={value:`skip`,label:`Skip for now`};return e.sharedConfigured||e.byoConfigured?[{value:`login`,label:`Connect Google`,hint:`Opens your browser for read-only Search Console and Google Analytics access`},t]:[{value:`setup`,label:`Set up Google login for local development`,hint:`This source checkout does not include the public app credentials`},t]}async function Ls(){let e=await Ra();return x(`Connected as ${e.account_email}. seo has read-only access and cannot change your site.`,`Google connected`),{status:`connected`,accountEmail:e.account_email}}async function Rs(e){let t=await Pe();if(t.activeMode===`service-account`)return{status:`service-account`,accountEmail:t.identity};if(!e.interactive){if(t.tokens)return{status:`already-connected`,accountEmail:t.tokens.account_email};throw new g(`AUTH_REQUIRED`,ct())}let n=tr();if(n.length>0){let e=B(await Kr({message:`Google account to use`,options:[...n.map(e=>({value:{type:`account`,accountEmail:e.accountEmail},label:e.accountEmail,hint:e.active?`Current default`:void 0})),{value:{type:`connect`},label:`Connect another Google account`}]}));if(e.type===`account`)return $e(e.accountEmail),{status:`already-connected`,accountEmail:e.accountEmail};if(t.sharedConfigured||t.byoConfigured)return Ls()}let r=B(await Kr({message:`Connect Google now?`,options:Is({sharedConfigured:t.sharedConfigured,byoConfigured:t.byoConfigured})}));if(r===`skip`)return{status:`skipped`};if(r===`setup`){let e=B(await qr({message:`Google Desktop OAuth client ID`,validate:e=>e?void 0:`Client ID is required`})),t=B(await Gr({message:`Google Desktop OAuth client secret`,validate:e=>e?void 0:`Client secret is required`}));Xe({clientId:e,clientSecret:t})}return Ls()}async function zs(){return B(await Br({message:`Install the SEO skill so your agents know how to use seo?`,initialValue:!0}))?Ps():{status:`declined`}}async function Bs(){let e=ts();if(e.length===0)return[];let t=B(await Kr({message:`Add an MCP server to your agents?`,options:[{value:`hosted`,label:`Free hosted server (recommended)`,hint:`${xs}, your agent asks you to sign in with Google`},{value:`local`,label:`Local server`,hint:`Runs on this machine with the sign-in from this setup`},{value:`skip`,label:`Skip for now`}]}));if(t===`skip`)return[];let n=B(await Wr({message:`Which agents?`,options:e.map(e=>({value:e.client,label:e.label,hint:e.path})),initialValues:e.map(e=>e.client)})),r=e.filter(e=>n.includes(e.client)).map(e=>{let n=t===`hosted`&&Cs(e)?`hosted`:`local`;try{return{...n===`hosted`?Es(e):ys(e),server:n}}catch(t){return{client:e.client,path:e.path,server:n,changed:!1,error:t instanceof Error?t.message:String(t)}}});return t===`hosted`&&r.some(e=>e.client===`claude-desktop`)&&x(`Claude Desktop got the local server. To use the hosted one there, open Settings, then Connectors, and add ${xs} as a custom connector.`,`Claude Desktop`),r}const Vs=[`seo sites`,`seo report --site <site>`],Hs=[`Sign in or create your free seoskill.dev account, or sign in with Google on this machine only (read-only Search Console and Google Analytics).`,`Add the free hosted MCP server (${xs}) or a local one to your agents.`,`Install the SEO skill for your agents.`];function Us(e){return e.status===`skipped`?`skipped`:e.status===`linked`?`seoskill.dev account${e.accountEmail?` (${e.accountEmail})`:``}`:e.status===`service-account`?`service account${e.accountEmail?` ${e.accountEmail}`:``}`:e.accountEmail??`connected`}function Ws(e){return e.status===`installed`?`installed`:e.status===`failed`?`install failed`:`not installed`}function Gs(e){return e.length===0?`not added`:e.map(e=>e.error?`${e.client} skipped`:`${e.client} (${e.server})`).join(`, `)}function Ks(){process.stdout.write(`
60
- `),z({title:`Help other people find SEO Skill`,body:`If SEO Skill saved you time, a GitHub star helps other people find it.`,command:`https://github.com/iannuttall/seo`})}async function qs(e){let t=A(e);if(t||Vr(process.argv[2]===`setup`?`seo setup`:`seo start`),e[`dry-run`]){t?I({dryRun:!0,steps:Hs,next:Vs}):(x(Hs.map((e,t)=>`${t+1}. ${e}`).join(`
61
- `),`Setup will`),x(Vs.join(`
62
- `),`Then run`),S(`Dry run complete. Nothing was changed.`));return}Rt();let n=F({json:t}),r=await Fs({interactive:n})??await Rs({interactive:n}),i=n?await Bs():[],a=n?await zs():{status:`skipped`};if(t){I({auth:r,mcp:i,skill:a,next:Vs});return}L([[`Sign-in`,Us(r)],[`MCP server`,Gs(i)],[`SEO skill`,Ws(a)]]);let o=i.filter(e=>e.error);o.length&&x(o.map(e=>`${e.client}: ${e.error}`).join(`
63
- `),`MCP setup skipped`),a.error&&x(a.error,`SEO skill install failed`),x(`seo sites list your Search Console properties
64
- seo report --site <site> run the main report for one of them`,`Try next`),S(`Setup complete.`),Ks()}const Js=b({meta:{name:`setup`,description:`Sign in with Google, then add the MCP server and skill`},args:{"dry-run":{type:`boolean`,default:!1,description:`Show what setup does without changing anything.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON. Never prompts.`}},run:async({args:e})=>qs(e)}),Ys=b({...Js,meta:{name:`start`,description:`Start here: sign in with Google, then add the MCP server and skill to your agents`}}),Xs=b({...Ys,meta:{name:`init`,description:"Legacy alias for `seo start`."}}),Zs=b({meta:{name:`logs`,description:`Manage local log retention`},subCommands:{prune:b({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=ci();A(e)?I(t):e.quiet||process.stdout.write(`Rotated ${t.rotated} and removed ${t.removed} log files. ${sa(t.sizeBytes)} retained.\n`)}})}}),Qs=[{client:`claude-desktop`,flag:`claude-desktop`},{client:`claude-code`,flag:`claude-code`},{client:`codex`,flag:`codex`},{client:`cursor`,flag:`cursor`}];function $s(e,t){return T(e.all)?t:Qs.filter(({flag:t})=>T(e[t])).map(({client:e})=>e)}const ec=b({meta:{name:`mcp`,description:`MCP server helpers`},subCommands:{serve:b({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-B9mN7y6S.js`);await t({test:T(e.test)})}}),install:b({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(T(e.uninstall)&&T(e.reinstall))throw new g(`INVALID_INPUT`,`Use either --uninstall or --reinstall, not both.`);let t=Qo(),n=ts({targets:t}),r=A(e),i=$s(e,n.map(e=>e.client));if(i.length===0){if(!F({json:r}))throw new g(`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 Wr({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 g(`INVALID_INPUT`,`MCP setup is not supported for ${a.join(`, `)} on ${process.platform}.`);let o=t.filter(e=>i.includes(e.client)).map(t=>T(e.uninstall)?bs(t):ys(t,{reinstall:T(e.reinstall)}));if(r)I({operation:T(e.uninstall)?`uninstall`:`install`,results:o});else for(let e of o)process.stdout.write(`${e.changed?`updated`:`skipped`} ${e.client} · ${e.path}\n`)}})}}),tc=b({meta:{name:`index-coverage`,description:`Find crawlable pages that deserve a closer Google index check`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},url:{type:`string`,description:`Start URL to crawl. Defaults to the home page.`},sitemaps:{type:`string`,description:`Comma-separated XML sitemap URLs. Defaults to declarations captured by the crawl.`},days:{type:`string`,description:`Finalized Search Console date range. Defaults to 90 days.`},"row-limit":{type:`string`,description:`Maximum Search Console page rows. Defaults to 100000.`},"max-sitemap-urls":{type:`string`,description:`Maximum sitemap inventory URLs. Defaults to 100000.`},limit:{type:`string`,description:`Maximum URLs returned in each evidence group. Defaults to 100.`},refresh:{type:`boolean`,default:!1,description:`Bypass the local Search Console cache.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=j(w(e.site)),r=mi(e.sitemaps),i=await Fe({site:n,url:w(e.url),sitemaps:r.length?r:void 0,days:D(e.days),rowLimit:D(e[`row-limit`]),maxSitemapUrls:D(e[`max-sitemap-urls`]),itemsPerSection:D(e.limit),refresh:T(e.refresh)});t?I(i):(K({title:`Index coverage`,target:i.site,status:i.sources.crawl.completeness===`complete`&&i.sources.searchConsole.completeness===`complete`?i.summary.crawlableCandidatesWithoutRetainedSearchVisibility>0?`warning`:`pass`:`unknown`,summary:`${i.summary.crawlableCandidatesWithoutRetainedSearchVisibility} crawlable pages need URL Inspection review.`,metrics:[{label:`Crawl report`,value:i.input.crawlReport.id},{label:`Crawl evidence`,value:i.sources.crawl.completeness},{label:`Sitemap evidence`,value:i.sources.sitemap.completeness},{label:`Search evidence`,value:i.sources.searchConsole.completeness},{label:`Search window`,value:`${i.sources.searchConsole.startDate} to ${i.sources.searchConsole.endDate}`},{label:`Search-visible pages`,value:i.summary.retainedSearchVisibleUrls},{label:`Blocked/non-indexable`,value:i.summary.blockedOrNonIndexableCrawlUrls},{label:`Sitemap only`,value:i.summary.sitemapOnlyUrls},{label:`Search Console only`,value:i.summary.searchConsoleOnlyUrls}]}),i.crawlableWithoutRetainedSearchVisibility.items.length&&(process.stdout.write(`
55
+ `),z([`Start`,`End`,`Type`,`Name`],r.overlappingUpdates.map(e=>[e.start.slice(0,10),e.end?.slice(0,10)??`open`,e.type,e.name]))),Qa(`seo segment-impact --site ${JSON.stringify(n)} --dimension page`))}}),Ho={site:{type:`string`,description:`Search Console property, for example sc-domain:example.com.`}};function Uo(e){return{type:`string`,description:`Output folder. Defaults to ./seo-export/<site>-${e}-YYYY-MM-DD.`}}function Wo(e){return e.toLowerCase().replace(/^sc-domain:/,``).replace(/^https?:\/\//,``).replace(/[^a-z0-9._-]+/g,`-`).replace(/^-+|-+$/g,``)}function Go(e){let t=(/* @__PURE__ */ new Date()).toISOString().slice(0,10),n=Wo(e.site);return fr(process.cwd(),`seo-export`,`${n}-${e.report}-${t}`)}async function Ko(e,t){await Or(e,{recursive:!0});let n=[];for(let r of t){let t=fr(e,r.filename);await Nr(t,zt(r.rows,r.headers),`utf8`),n.push(t)}return n}function qo(e,t){process.stdout.write(`Wrote ${ce(t.length,`CSV file`)} to ${e}\n`);for(let e of t)process.stdout.write(`- ${e}\n`)}const Jo=b({meta:{name:`refresh-priorities`,description:`Export the ranked SEO priority queue, score breakdowns, grouped findings, and diagnosis detail CSVs.`},args:{...Ho,out:Uo(`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.`}}),...mo,"verify-content":ui(`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=M(T(e.site)),n=await Me({site:t,days:O(e.days),recentDays:O(e.recent),limit:O(e.limit),includeBrand:E(e[`include-brand`]),googleAnalyticsPropertyId:T(e[`google-analytics-property`]),analyticsConnection:ho(e),verifyContent:E(e[`verify-content`]),verifyLimit:O(e[`verify-limit`]),refresh:E(e.refresh)}),r=T(e.out)??Go({report:`refresh-priorities`,site:t});qo(r,await Ko(r,Ut(n)))}}),Yo=b({meta:{name:`update-postmortem`,description:`Export update postmortem attribution, findings, template movement, and segment movers.`},args:{...Ho,out:Uo(`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.`}},run:async({args:e})=>{let t=M(T(e.site)),n=await te({site:t,days:O(e.days),recentDays:O(e.recent),limit:O(e.limit),includeBrand:E(e[`include-brand`]),knownConfounders:T(e[`known-change`])?[T(e[`known-change`])??``]:void 0,refresh:E(e.refresh)}),r=T(e.out)??Go({report:`update-postmortem`,site:t});qo(r,await Ko(r,ai(n)))}}),Xo=b({meta:{name:`export`,description:`Export full report data to CSV files`},subCommands:{"update-postmortem":Yo,"refresh-priorities":Jo}});function Zo(e,t,n={}){if(process.platform!==`win32`)return Lr(e,t,n);let r=e=>/^[\w./:=@\\-]+$/.test(e)?e:`"${e.replaceAll(`"`,`""`)}"`;return Lr([e,...t].map(r).join(` `),{...n,shell:!0})}function Qo(e){let t=e.platform===`win32`?mr:ur;return e.platform===`win32`?e.env.APPDATA??t.resolve(e.home,`AppData/Roaming`):e.platform===`darwin`?t.resolve(e.home,`Library/Application Support`):e.env.XDG_CONFIG_HOME??t.resolve(e.home,`.config`)}function $o(e={}){let t=e.home??or(),n=e.platform??process.platform,r=Qo({home:t,platform:n,env:e.env??process.env}),i=n===`win32`?mr:ur,a=[];if(n===`darwin`||n===`win32`){let e=i.resolve(r,`Claude`);a.push({kind:`json`,client:`claude-desktop`,label:`Claude Desktop`,path:i.resolve(e,`claude_desktop_config.json`),commandNames:[],detectionPaths:n===`darwin`?[e,`/Applications/Claude.app`]:[e]})}return a.push({kind:`json`,client:`claude-code`,label:`Claude Code`,path:i.resolve(t,`.claude.json`),includeType:!0,commandNames:[`claude`],detectionPaths:[i.resolve(t,`.claude`)]},{kind:`native`,client:`codex`,label:`Codex`,path:i.resolve(t,`.codex/config.toml`),executable:`codex`,commandNames:[`codex`],detectionPaths:[i.resolve(t,`.codex`)]},{kind:`json`,client:`cursor`,label:`Cursor`,path:i.resolve(t,`.cursor/mcp.json`),commandNames:[`cursor`,`cursor-agent`],detectionPaths:[i.resolve(t,`.cursor`),...n===`darwin`?[`/Applications/Cursor.app`]:[]]}),a}function es(e,t={}){let n=t.platform??process.platform,r=t.exists??v,i=t.nodePath??process.execPath,a=n===`win32`?[`${e}.cmd`,`${e}.exe`,e]:[e],o=(t.path??process.env.PATH??``).split(n===`win32`?`;`:`:`).filter(Boolean);for(let e of o)for(let t of a){let n=_(e,t);if(r(n))return n}for(let e of a){let t=_(lr(i),e);if(r(t))return t}return e}function ts(e){return es(e)!==e}function ns(e={}){let t=e.targets??$o(),n=e.hasCommand??ts,r=e.hasPath??v;return t.filter(e=>e.kind===`native`?e.commandNames.some(n):r(e.path)||e.detectionPaths.some(r)||e.commandNames.some(n))}const rs={insertSpaces:!0,tabSize:2,eol:`
56
+ `};function is(){return(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g,`-`)}function as(e,t){vr(lr(e),{recursive:!0,mode:448});let n=`${e}.${process.pid}.${Date.now()}.tmp`,r=v(e)?wr(e).mode&511:384;Tr(n,t,{encoding:`utf8`,mode:r}),Sr(n,e)}function os(e){let t=`${e}.bak-${is()}`,n=t,r=2;for(;v(n);)n=`${t}-${r}`,r+=1;gr(e,n,hr.COPYFILE_EXCL)}function ss(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function cs(e){if(!v(e.path))return{source:`{}
57
+ `,config:{}};let t=yr(e.path,`utf8`),n=[],r=ei(t,n,{allowTrailingComma:!0});if(n.length>0){let t=n[0],r=t?`${ti(t.error)} at offset ${t.offset}`:`unknown parse error`;throw Error(`Cannot update ${e.path}: invalid JSONC (${r}).`)}if(!ss(r))throw Error(`Cannot update ${e.path}: config root must be an object.`);if(r.mcpServers!==void 0&&!ss(r.mcpServers))throw Error(`Cannot update ${e.path}: mcpServers must be an object.`);return{source:t,config:r}}function ls(e){return e.includes(`mcp`)&&e.includes(`serve`)&&e.some(e=>e===`seo`||e.startsWith(`seo@`)||e===`@seo/cli`)}function us(e,t){return typeof e.command!=`string`||t.length!==3||t[1]!==`mcp`||t[2]!==`serve`||!ss(e.env)?!1:e.env.SEO_MCP_CONFIGURED_BY===`seo`}function ds(e){if(!ss(e)||!Array.isArray(e.args))return!1;let t=e.args.filter(e=>typeof e==`string`);return ls(t)||us(e,t)}function fs(e){let t=Qr(e.source,$r(e.source,[`mcpServers`,e.name??`seo`],e.value,{formattingOptions:rs}));v(e.target.path)&&os(e.target.path),as(e.target.path,t)}function ps(e,t){let n=Zo(e,t,{encoding:`utf8`});return{status:n.status,stdout:String(n.stdout??``),stderr:String(n.stderr??``),error:n.error}}function ms(e){let t=e.replaceAll(`\\`,`/`).toLowerCase();return t.includes(`/_npx/`)||t.includes(`/.npm/_npx/`)||t.includes(`/.cache/pnpm/dlx/`)}function hs(e){if(!e||!v(e))return;let t=xr(e);return ms(t)?void 0:t}function gs(e={}){let t=hs(e.cliPath??process.argv[1]);if(t)return{command:e.nodePath??process.execPath,args:[t,`mcp`,`serve`]};let n=`seo@${e.version??Ln}`;return(e.platform??process.platform)===`win32`?{command:`cmd`,args:[`/c`,`npx`,`-y`,n,`mcp`,`serve`]}:{command:`npx`,args:[`-y`,n,`mcp`,`serve`]}}function _s(e=!1,t=gs()){let n=ln(),r={command:t.command,args:t.args,env:{SEO_CONFIG_DIR:n.configDir,SEO_MCP_CONFIGURED_BY:`seo`}};return e&&(r.type=`stdio`),r}function vs(e){return e.includes(`SEO_MCP_CONFIGURED_BY`)||/args:\s+-y\s+(?:seo(?:@\S+)?|@seo\/cli)\s+mcp\s+serve\b/.test(e)}function ys(e,t,n){let r=es(e.executable),i=n.runCommand(r,[`mcp`,`get`,`seo`]);if(i.error?.code===`ENOENT`)throw Error("Cannot install SEO MCP for Codex: the `codex` command is not available on PATH. Install Codex CLI or choose another MCP client.");let a=i.status===0;if(a&&!vs(i.stdout))throw Error(`Cannot ${t} SEO MCP: ${e.path} already has an unmanaged mcp_servers.seo entry.`);if(t===`install`&&a&&!n.reinstall||t===`uninstall`&&!a)return{client:e.client,path:e.path,changed:!1};if(t===`install`&&a){let e=n.runCommand(r,[`mcp`,`remove`,`seo`]);if(e.error||e.status!==0){let t=e.error?.message??e.stderr.trim()??`unknown error`;throw Error(`Cannot reinstall SEO MCP for Codex: ${t}`)}}let o=ln(),s=t===`install`?[`mcp`,`add`,`seo`,`--env`,`SEO_CONFIG_DIR=${o.configDir}`,`--env`,`SEO_MCP_CONFIGURED_BY=seo`,`--`,n.serverCommand.command,...n.serverCommand.args]:[`mcp`,`remove`,`seo`],c=n.runCommand(r,s);if(c.error||c.status!==0){let e=c.error?.message??c.stderr.trim()??`unknown error`;throw Error(`Cannot ${t} SEO MCP for Codex: ${e}`)}return{client:e.client,path:e.path,changed:!0}}function bs(e,t={}){let n={runCommand:t.runCommand??ps,reinstall:t.reinstall??!1,serverCommand:t.serverCommand??gs()};if(e.kind===`native`)return ys(e,`install`,n);let{source:r,config:i}=cs(e),a=i.mcpServers?.seo,o=_s(e.includeType,n.serverCommand);if(Pr(a,o)&&!n.reinstall)return{client:e.client,path:e.path,changed:!1};if(a!==void 0&&!ds(a))throw Error(`Cannot install SEO MCP: ${e.path} already has an unmanaged mcpServers.seo entry.`);return fs({target:e,source:r,value:o}),{client:e.client,path:e.path,changed:!0}}function xs(e,t={}){if(e.kind===`native`)return ys(e,`uninstall`,{runCommand:t.runCommand??ps,reinstall:!1,serverCommand:gs()});if(!v(e.path))return{client:e.client,path:e.path,changed:!1};let{source:n,config:r}=cs(e),i=r.mcpServers;if(i?.seo===void 0)return{client:e.client,path:e.path,changed:!1};if(!ds(i.seo))throw Error(`Cannot uninstall SEO MCP: ${e.path} has an unmanaged mcpServers.seo entry.`);return fs({target:e,source:n,value:void 0}),{client:e.client,path:e.path,changed:!0}}const Ss=`https://seoskill.dev/mcp`,Cs=`seoskill`;function ws(e){return e.client!==`claude-desktop`}function Ts(e){return e.includeType?{type:`http`,url:Ss}:{url:Ss}}function Es(e){return ss(e)&&e.url===`https://seoskill.dev/mcp`}function Ds(e,t={}){if(!ws(e))throw Error(`${e.label} takes the hosted server from its Connectors settings, not its config file.`);if(e.kind===`native`){let n=t.runCommand??ps,r=es(e.executable),i=n(r,[`mcp`,`get`,Cs]);if(i.error?.code===`ENOENT`)throw Error("Cannot add the hosted MCP server for Codex: the `codex` command is not available on PATH.");if(i.status===0){if(i.stdout.includes(`https://seoskill.dev/mcp`))return{client:e.client,path:e.path,changed:!1};throw Error(`Cannot add the hosted MCP server: ${e.path} already has a different mcp_servers.${Cs} entry.`)}let a=n(r,[`mcp`,`add`,Cs,`--url`,Ss]);if(a.error||a.status!==0){let e=a.error?.message??a.stderr.trim()??`unknown error`;throw Error(`Cannot add the hosted MCP server for Codex: ${e}`)}return{client:e.client,path:e.path,changed:!0}}let{source:n,config:r}=cs(e),i=r.mcpServers?.[Cs];if(Es(i))return{client:e.client,path:e.path,changed:!1};if(i!==void 0)throw Error(`Cannot add the hosted MCP server: ${e.path} already has a different mcpServers.${Cs} entry.`);return fs({target:e,source:n,value:Ts(e),name:Cs}),{client:e.client,path:e.path,changed:!0}}const Os=e=>process.stdout.write(`${e}\n`);async function ks(){let{google:e,bing:t,keys:n}=await ye();return{status:`linked`,google:e.map(e=>e.email),bing:t.length,keys:Object.keys(n).filter(e=>e!==`x-dataforseo-login`&&!e.startsWith(`x-clicky-`)),clicky:Object.keys(n).filter(e=>e.startsWith(Et)).length}}function As(e){return[`Linked to your seoskill.dev account.`,`Google: ${e.google.join(`, `)||`none`}.`,`Bing: ${e.bing?`${e.bing} linked`:`not connected (add it at ${se}/account)`}.`,`Keys: ${e.keys.length?e.keys.map(e=>e.replace(/^x-|-(key|password)$/g,``)).join(`, `):`none`}.`,`Clicky: ${e.clicky?`${e.clicky} site${e.clicky===1?``:`s`}`:`none`}.`,`Next: seo sites, then seo report --site <site>.`].join(`
58
+ `)}const js=b({meta:{name:`login`,description:`Link this machine to your seoskill.dev account: its Google accounts, Bing and keys`},args:{json:{type:`boolean`,default:!1,description:`Print JSON.`}},run:async({args:e})=>{let n=j(e),r=async()=>{let e=await ks();n?L(e):Os(As(e))};if(process.env.SEO_TOKEN?.trim()){await r();return}if(Le()){let e=await ct();if(e===`linked`)return r();if(e===`pending`&&!I({json:n})){let e=Le();n?L({status:`pending`,...Ns(e)}):Os(`Waiting for approval. Open ${e?.url} and click "Link this machine", then run \`seo login\` again.`);return}}else if(Cn()){await r();return}let i=Le()??await t();if(!I({json:n}))n?L({status:`pending`,...Ns(i)}):Os([`Open this link in the person's browser and ask them to approve it:`,i.url,`Code: ${i.code}. After they approve, run \`seo login\` again.`].join(`
59
+ `));else{if(await Ms())return r();Os("The login expired. Run `seo login` again."),process.exitCode=1}}});async function Ms(){let e=Le()??await t();for(Os(`Opening ${e.url}\nApprove code ${e.code} there. Waiting…`),await dt(e.url);Le();){await new Promise(e=>setTimeout(e,3e3));let e=await ct();if(e===`linked`)return!0;if(e===`expired`)break}return!1}const Ns=e=>({url:e?.url,code:e?.code}),Ps=b({meta:{name:`logout`,description:`Unlink this machine from your seoskill.dev account`},args:{json:{type:`boolean`,default:!1,description:`Print JSON.`}},run:async({args:e})=>{let t=await ge();j(e)?L({unlinked:t}):Os(t?`This machine is no longer linked to your seoskill.dev account.`:`This machine was not linked.`)}});function Fs(e={}){let t=Zo(`npx`,[`-y`,`skills`,`add`,`iannuttall/seo`],{stdio:e.quiet?`ignore`:`inherit`});return t.error?{status:`failed`,error:t.error.message}:t.status===0?{status:`installed`}:{status:`failed`,error:`npx skills add exited with status ${t.status??`unknown`}.`}}async function Is(e){let t=async()=>{let{google:e}=await ye().catch(()=>({google:[]}));return{status:`linked`,accountEmail:e.map(e=>e.email).join(`, `)||void 0}};if(process.env.SEO_TOKEN?.trim()||Cn())return t();if(!e.interactive)return null;let n=B(await Kr({message:`How do you want to sign in?`,options:[{value:`account`,label:`Sign in or create a free seoskill.dev account`,hint:`Recommended. Your Google accounts, Bing and keys in one place, for the CLI and the hosted MCP`},{value:`local`,label:`Sign in with Google on this machine only`},{value:`skip`,label:`Skip for now`}]}));return n===`skip`?{status:`skipped`}:n===`local`?null:await Ms()?t():(S("The link expired. Run `seo login` to try again.",`Not signed in`),{status:`skipped`})}function Ls(e){let t={value:`skip`,label:`Skip for now`};return e.sharedConfigured||e.byoConfigured?[{value:`login`,label:`Connect Google`,hint:`Opens your browser for read-only Search Console and Google Analytics access`},t]:[{value:`setup`,label:`Set up Google login for local development`,hint:`This source checkout does not include the public app credentials`},t]}async function Rs(){let e=await za();return S(`Connected as ${e.account_email}. seo has read-only access and cannot change your site.`,`Google connected`),{status:`connected`,accountEmail:e.account_email}}async function zs(e){let t=await ie();if(t.activeMode===`service-account`)return{status:`service-account`,accountEmail:t.identity};if(!e.interactive){if(t.tokens)return{status:`already-connected`,accountEmail:t.tokens.account_email};throw new g(`AUTH_REQUIRED`,at())}let n=s();if(n.length>0){let e=B(await Kr({message:`Google account to use`,options:[...n.map(e=>({value:{type:`account`,accountEmail:e.accountEmail},label:e.accountEmail,hint:e.active?`Current default`:void 0})),{value:{type:`connect`},label:`Connect another Google account`}]}));if(e.type===`account`)return ue(e.accountEmail),{status:`already-connected`,accountEmail:e.accountEmail};if(t.sharedConfigured||t.byoConfigured)return Rs()}let r=B(await Kr({message:`Connect Google now?`,options:Ls({sharedConfigured:t.sharedConfigured,byoConfigured:t.byoConfigured})}));if(r===`skip`)return{status:`skipped`};if(r===`setup`){let e=B(await qr({message:`Google Desktop OAuth client ID`,validate:e=>e?void 0:`Client ID is required`})),t=B(await Gr({message:`Google Desktop OAuth client secret`,validate:e=>e?void 0:`Client secret is required`}));nt({clientId:e,clientSecret:t})}return Rs()}async function Bs(){return B(await Vr({message:`Install the SEO skill so your agents know how to use seo?`,initialValue:!0}))?Fs():{status:`declined`}}async function Vs(){let e=ns();if(e.length===0)return[];let t=B(await Kr({message:`Add an MCP server to your agents?`,options:[{value:`hosted`,label:`Free hosted server (recommended)`,hint:`${Ss}, your agent asks you to sign in with Google`},{value:`local`,label:`Local server`,hint:`Runs on this machine with the sign-in from this setup`},{value:`skip`,label:`Skip for now`}]}));if(t===`skip`)return[];let n=B(await Wr({message:`Which agents?`,options:e.map(e=>({value:e.client,label:e.label,hint:e.path})),initialValues:e.map(e=>e.client)})),r=e.filter(e=>n.includes(e.client)).map(e=>{let n=t===`hosted`&&ws(e)?`hosted`:`local`;try{return{...n===`hosted`?Ds(e):bs(e),server:n}}catch(t){return{client:e.client,path:e.path,server:n,changed:!1,error:t instanceof Error?t.message:String(t)}}});return t===`hosted`&&r.some(e=>e.client===`claude-desktop`)&&S(`Claude Desktop got the local server. To use the hosted one there, open Settings, then Connectors, and add ${Ss} as a custom connector.`,`Claude Desktop`),r}const Hs=[`seo sites`,`seo report --site <site>`],Us=[`Sign in or create your free seoskill.dev account, or sign in with Google on this machine only (read-only Search Console and Google Analytics).`,`Add the free hosted MCP server (${Ss}) or a local one to your agents.`,`Install the SEO skill for your agents.`];function Ws(e){return e.status===`skipped`?`skipped`:e.status===`linked`?`seoskill.dev account${e.accountEmail?` (${e.accountEmail})`:``}`:e.status===`service-account`?`service account${e.accountEmail?` ${e.accountEmail}`:``}`:e.accountEmail??`connected`}function Gs(e){return e.status===`installed`?`installed`:e.status===`failed`?`install failed`:`not installed`}function Ks(e){return e.length===0?`not added`:e.map(e=>e.error?`${e.client} skipped`:`${e.client} (${e.server})`).join(`, `)}function qs(){process.stdout.write(`
60
+ `),$i({title:`Help other people find SEO Skill`,body:`If SEO Skill saved you time, a GitHub star helps other people find it.`,command:`https://github.com/iannuttall/seo`})}async function Js(e){let t=j(e);if(t||x(process.argv[2]===`setup`?`seo setup`:`seo start`),e[`dry-run`]){t?L({dryRun:!0,steps:Us,next:Hs}):(S(Us.map((e,t)=>`${t+1}. ${e}`).join(`
61
+ `),`Setup will`),S(Hs.join(`
62
+ `),`Then run`),C(`Dry run complete. Nothing was changed.`));return}bn();let n=I({json:t}),r=await Is({interactive:n})??await zs({interactive:n}),i=n?await Vs():[],a=n?await Bs():{status:`skipped`};if(t){L({auth:r,mcp:i,skill:a,next:Hs});return}R([[`Sign-in`,Ws(r)],[`MCP server`,Ks(i)],[`SEO skill`,Gs(a)]]);let o=i.filter(e=>e.error);o.length&&S(o.map(e=>`${e.client}: ${e.error}`).join(`
63
+ `),`MCP setup skipped`),a.error&&S(a.error,`SEO skill install failed`),S(`seo sites list your Search Console properties
64
+ seo report --site <site> run the main report for one of them`,`Try next`),C(`Setup complete.`),qs()}const Ys=b({meta:{name:`setup`,description:`Sign in with Google, then add the MCP server and skill`},args:{"dry-run":{type:`boolean`,default:!1,description:`Show what setup does without changing anything.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON. Never prompts.`}},run:async({args:e})=>Js(e)}),Xs=b({...Ys,meta:{name:`start`,description:`Start here: sign in with Google, then add the MCP server and skill to your agents`}}),Zs=b({...Xs,meta:{name:`init`,description:"Legacy alias for `seo start`."}}),Qs=b({meta:{name:`logs`,description:`Manage local log retention`},subCommands:{prune:b({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=ci();j(e)?L(t):e.quiet||process.stdout.write(`Rotated ${t.rotated} and removed ${t.removed} log files. ${ca(t.sizeBytes)} retained.\n`)}})}}),$s=[{client:`claude-desktop`,flag:`claude-desktop`},{client:`claude-code`,flag:`claude-code`},{client:`codex`,flag:`codex`},{client:`cursor`,flag:`cursor`}];function ec(e,t){return E(e.all)?t:$s.filter(({flag:t})=>E(e[t])).map(({client:e})=>e)}const tc=b({meta:{name:`mcp`,description:`MCP server helpers`},subCommands:{serve:b({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-Ck71nZiP.js`);await t({test:E(e.test)})}}),install:b({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(E(e.uninstall)&&E(e.reinstall))throw new g(`INVALID_INPUT`,`Use either --uninstall or --reinstall, not both.`);let t=$o(),n=ns({targets:t}),r=j(e),i=ec(e,n.map(e=>e.client));if(i.length===0){if(!I({json:r}))throw new g(`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 Wr({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 g(`INVALID_INPUT`,`MCP setup is not supported for ${a.join(`, `)} on ${process.platform}.`);let o=t.filter(e=>i.includes(e.client)).map(t=>E(e.uninstall)?xs(t):bs(t,{reinstall:E(e.reinstall)}));if(r)L({operation:E(e.uninstall)?`uninstall`:`install`,results:o});else for(let e of o)process.stdout.write(`${e.changed?`updated`:`skipped`} ${e.client} · ${e.path}\n`)}})}}),nc=b({meta:{name:`index-coverage`,description:`Find crawlable pages that deserve a closer Google index check`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},url:{type:`string`,description:`Start URL to crawl. Defaults to the home page.`},sitemaps:{type:`string`,description:`Comma-separated XML sitemap URLs. Defaults to declarations captured by the crawl.`},days:{type:`string`,description:`Finalized Search Console date range. Defaults to 90 days.`},"row-limit":{type:`string`,description:`Maximum Search Console page rows. Defaults to 100000.`},"max-sitemap-urls":{type:`string`,description:`Maximum sitemap inventory URLs. Defaults to 100000.`},limit:{type:`string`,description:`Maximum URLs returned in each evidence group. Defaults to 100.`},refresh:{type:`boolean`,default:!1,description:`Bypass the local Search Console cache.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=M(T(e.site)),r=mi(e.sitemaps),i=await ae({site:n,url:T(e.url),sitemaps:r.length?r:void 0,days:O(e.days),rowLimit:O(e[`row-limit`]),maxSitemapUrls:O(e[`max-sitemap-urls`]),itemsPerSection:O(e.limit),refresh:E(e.refresh)});t?L(i):(K({title:`Index coverage`,target:i.site,status:i.sources.crawl.completeness===`complete`&&i.sources.searchConsole.completeness===`complete`?i.summary.crawlableCandidatesWithoutRetainedSearchVisibility>0?`warning`:`pass`:`unknown`,summary:`${i.summary.crawlableCandidatesWithoutRetainedSearchVisibility} crawlable pages need URL Inspection review.`,metrics:[{label:`Crawl report`,value:i.input.crawlReport.id},{label:`Crawl evidence`,value:i.sources.crawl.completeness},{label:`Sitemap evidence`,value:i.sources.sitemap.completeness},{label:`Search evidence`,value:i.sources.searchConsole.completeness},{label:`Search window`,value:`${i.sources.searchConsole.startDate} to ${i.sources.searchConsole.endDate}`},{label:`Search-visible pages`,value:i.summary.retainedSearchVisibleUrls},{label:`Blocked/non-indexable`,value:i.summary.blockedOrNonIndexableCrawlUrls},{label:`Sitemap only`,value:i.summary.sitemapOnlyUrls},{label:`Search Console only`,value:i.summary.searchConsoleOnlyUrls}]}),i.crawlableWithoutRetainedSearchVisibility.items.length&&(process.stdout.write(`
65
65
  Representative URL Inspection candidates
66
- `),R([`URL`,`Status`,`In sitemap`],i.crawlableWithoutRetainedSearchVisibility.items.map(e=>[G(e.url,80),e.status,e.inSitemap?`yes`:`no`]))),Y(`How to read this report`,i.caveats))}}),nc=b({meta:{name:`link-recover`,description:`Find search-value URLs that are now broken, blocked, or poorly redirected`},args:{site:{type:`string`},...X([`days`,`limit`,`minImpressions`,`refresh`,`js`],{days:{description:`GSC lookback window. Defaults to 90.`},limit:{description:`Maximum GSC pages to check. Defaults to 50.`},minImpressions:{description:`Minimum impressions to check a page. Defaults to 100.`},refresh:{description:`Bypass local GSC and HTTP caches.`},js:{description:`Force JavaScript rendering for final page extraction.`}}),"min-clicks":{type:`string`,description:`Minimum clicks to check a page. Defaults to 1.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=await Ee({site:j(w(e.site)),days:D(e.days),limit:D(e.limit),minClicks:D(e[`min-clicks`]),minImpressions:D(e[`min-impressions`]),refresh:T(e.refresh),js:T(e.js)?!0:`auto`});t?I(n):(K({title:`Link recovery`,target:n.site,status:n.warnings.length>0?`unknown`:n.summary.recoverable>0?`warning`:`pass`,summary:n.summary.recoverable>0?`${U(n.summary.recoverable)} previously visible URLs need recovery review.`:`No recoverable URLs were found in the retained evidence.`,metrics:[{label:`Range`,value:`${n.range.startDate} to ${n.range.endDate}`},{label:`Checked`,value:U(n.summary.checked)},{label:`Recoverable`,value:U(n.summary.recoverable)},{label:`High severity`,value:U(n.summary.high)},{label:`Clicks at risk`,value:U(n.summary.clicksAtRisk)},{label:`Impressions at risk`,value:U(n.summary.impressionsAtRisk)}]}),n.items.length&&(q([`Severity`,`Issue`,`Clicks`,`Impr`,`URL`,`Action`],n.items.map(e=>[e.severity,e.issue,U(e.clicks),U(e.impressions),G(e.url,56),G(e.recommendation.action,72)])),J(`Top recovery actions`,n.items.map(e=>({label:G(e.url,96),context:`${e.severity}, ${U(e.clicks)} clicks at risk`,action:e.recommendation.action})))),Y(`Warnings`,n.warnings.slice(0,10)))}}),rc=b({meta:{name:`redirect-trace`,description:`Trace redirects and report final indexability/canonical issues, or check a redirect map`},args:{url:{type:`string`,description:`URL to trace.`},map:{type:`string`,description:`CSV of old_url,expected_new_url lines (header optional), or one URL per line. Traces up to ${Kn.toLocaleString(`en-US`)} rows, 5 at a time.`},"max-hops":{type:`string`,description:`Maximum redirect hops to follow, up to 10. Defaults to 10.`},refresh:{type:`boolean`,default:!1,description:`Bypass local HTTP cache for final page extraction.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering for final page extraction.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.map);if(t){await sc(e,t);return}let n=w(e.url);if(!n)throw new g(`INVALID_INPUT`,`Pass --url to trace one URL, or --map with a CSV of old and expected URLs.`);let r=await ke({url:n,maxHops:D(e[`max-hops`]),refresh:T(e.refresh),js:T(e.js)?!0:`auto`});A(e)?I(r):(K({title:`Redirect trace`,target:r.url,status:r.summary.issues.length>0||r.summary.error?`warning`:r.summary.finalIndexable===void 0?`unknown`:`pass`,summary:[r.summary.error?`${r.summary.outcome===`timeout`?`Timed out`:`Failed`} at ${r.finalUrl}: ${r.summary.error}`:r.summary.issues.length>0?``:`${r.summary.hops} redirect ${r.summary.hops===1?`hop`:`hops`} ended at HTTP ${r.summary.finalStatus}.`,r.summary.issues.join(`, `)].filter(Boolean).join(` `),metrics:[{label:`Final URL`,value:r.finalUrl},{label:`Final status`,value:r.summary.finalStatus??`No response`},{label:`Indexable`,value:r.summary.finalIndexable===void 0?`Unknown`:r.summary.finalIndexable?`Yes`:`No`},{label:`Hops`,value:r.summary.hops}]}),R([`#`,`Status`,`URL`,`Next`],r.chain.map((e,t)=>[t+1,e.status,G(e.url,72),G(e.nextUrl??`-`,72)])),r.finalPage&&(process.stdout.write(`
66
+ `),z([`URL`,`Status`,`In sitemap`],i.crawlableWithoutRetainedSearchVisibility.items.map(e=>[G(e.url,80),e.status,e.inSitemap?`yes`:`no`]))),Y(`How to read this report`,i.caveats))}}),rc=b({meta:{name:`link-recover`,description:`Find search-value URLs that are now broken, blocked, or poorly redirected`},args:{site:{type:`string`},...X([`days`,`limit`,`minImpressions`,`refresh`,`js`],{days:{description:`GSC lookback window. Defaults to 90.`},limit:{description:`Maximum GSC pages to check. Defaults to 50.`},minImpressions:{description:`Minimum impressions to check a page. Defaults to 100.`},refresh:{description:`Bypass local GSC and HTTP caches.`},js:{description:`Force JavaScript rendering for final page extraction.`}}),"min-clicks":{type:`string`,description:`Minimum clicks to check a page. Defaults to 1.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=await ke({site:M(T(e.site)),days:O(e.days),limit:O(e.limit),minClicks:O(e[`min-clicks`]),minImpressions:O(e[`min-impressions`]),refresh:E(e.refresh),js:E(e.js)?!0:`auto`});t?L(n):(K({title:`Link recovery`,target:n.site,status:n.warnings.length>0?`unknown`:n.summary.recoverable>0?`warning`:`pass`,summary:n.summary.recoverable>0?`${U(n.summary.recoverable)} previously visible URLs need recovery review.`:`No recoverable URLs were found in the retained evidence.`,metrics:[{label:`Range`,value:`${n.range.startDate} to ${n.range.endDate}`},{label:`Checked`,value:U(n.summary.checked)},{label:`Recoverable`,value:U(n.summary.recoverable)},{label:`High severity`,value:U(n.summary.high)},{label:`Clicks at risk`,value:U(n.summary.clicksAtRisk)},{label:`Impressions at risk`,value:U(n.summary.impressionsAtRisk)}]}),n.items.length&&(q([`Severity`,`Issue`,`Clicks`,`Impr`,`URL`,`Action`],n.items.map(e=>[e.severity,e.issue,U(e.clicks),U(e.impressions),G(e.url,56),G(e.recommendation.action,72)])),J(`Top recovery actions`,n.items.map(e=>({label:G(e.url,96),context:`${e.severity}, ${U(e.clicks)} clicks at risk`,action:e.recommendation.action})))),Y(`Warnings`,n.warnings.slice(0,10)))}}),ic=b({meta:{name:`redirect-trace`,description:`Trace redirects and report final indexability/canonical issues, or check a redirect map`},args:{url:{type:`string`,description:`URL to trace.`},map:{type:`string`,description:`CSV of old_url,expected_new_url lines (header optional), or one URL per line. Traces up to ${qe.toLocaleString(`en-US`)} rows, 5 at a time.`},"max-hops":{type:`string`,description:`Maximum redirect hops to follow, up to 10. Defaults to 10.`},refresh:{type:`boolean`,default:!1,description:`Bypass local HTTP cache for final page extraction.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering for final page extraction.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.map);if(t){await cc(e,t);return}let n=T(e.url);if(!n)throw new g(`INVALID_INPUT`,`Pass --url to trace one URL, or --map with a CSV of old and expected URLs.`);let r=await Fe({url:n,maxHops:O(e[`max-hops`]),refresh:E(e.refresh),js:E(e.js)?!0:`auto`});j(e)?L(r):(K({title:`Redirect trace`,target:r.url,status:r.summary.issues.length>0||r.summary.error?`warning`:r.summary.finalIndexable===void 0?`unknown`:`pass`,summary:[r.summary.error?`${r.summary.outcome===`timeout`?`Timed out`:`Failed`} at ${r.finalUrl}: ${r.summary.error}`:r.summary.issues.length>0?``:`${r.summary.hops} redirect ${r.summary.hops===1?`hop`:`hops`} ended at HTTP ${r.summary.finalStatus}.`,r.summary.issues.join(`, `)].filter(Boolean).join(` `),metrics:[{label:`Final URL`,value:r.finalUrl},{label:`Final status`,value:r.summary.finalStatus??`No response`},{label:`Indexable`,value:r.summary.finalIndexable===void 0?`Unknown`:r.summary.finalIndexable?`Yes`:`No`},{label:`Hops`,value:r.summary.hops}]}),z([`#`,`Status`,`URL`,`Next`],r.chain.map((e,t)=>[t+1,e.status,G(e.url,72),G(e.nextUrl??`-`,72)])),r.finalPage&&(process.stdout.write(`
67
67
  Final page
68
- `),L([[`Title`,r.finalPage.title??`-`],[`Canonical`,r.finalPage.canonical??`-`],[`Meta robots`,r.finalPage.metaRobots??`-`]])),Y(`Warnings`,r.warnings.slice(0,10)))}}),ic={matches:`matches`,chain:`chain`,redirects:`redirects`,wrong_target:`wrong target`,not_redirected:`not redirected`,loop:`loop`,too_many_hops:`too many hops`,timeout:`timeout`,error:`error`},ac=[`matches`,`redirects`];async function oc(e){process.stdout.write(e)||await new Promise(e=>process.stdout.once(`drain`,e))}async function sc(e,t){for(let t of[`url`,`max-hops`])if(w(e[t])!==void 0)throw new g(`INVALID_INPUT`,`--${t} does not apply to --map.`);for(let t of[`refresh`,`js`])if(e[t]===!0)throw new g(`INVALID_INPUT`,`--${t} does not apply to --map.`);let n=(await jr(t)).size;if(n>2e6)throw new g(`INVALID_INPUT`,`${t} is ${n.toLocaleString(`en-US`)} bytes. Map mode reads up to ${u.toLocaleString(`en-US`)} bytes and ${Kn.toLocaleString(`en-US`)} rows per run, so split the file.`);let r=qe(await y(t,`utf8`)),i=A(e),a=!i&&process.stderr.isTTY,o=[],s=0,c=0,l=await te(r,async e=>{c+=1,i?await oc(`${JSON.stringify(e)}\n`):ac.includes(e.verdict)&&e.final_status===200||(s+=1,o.length<50&&o.push(e)),a&&process.stderr.write(`\rTraced ${c} of ${r.length}`)});if(a&&process.stderr.write(`
69
- `),i){await oc(`${JSON.stringify(l)}\n`);return}let d=Object.keys(ic).filter(e=>l.verdicts[e]).map(e=>`${l.verdicts[e]} ${ic[e]}`);K({title:`Redirect map`,target:t,status:ac.reduce((e,t)=>e+(l.verdicts[t]??0),0)===l.rows&&!l.final_not_200?`pass`:`warning`,summary:`${l.rows} rows traced: ${d.join(`, `)}.${l.final_not_200?` ${l.final_not_200} ended on a status other than 200.`:``}`,metrics:[{label:`Rows`,value:l.rows},...Object.keys(ic).filter(e=>l.verdicts[e]).map(e=>({label:ic[e],value:l.verdicts[e]??0}))]}),o.length&&(process.stdout.write(`
68
+ `),R([[`Title`,r.finalPage.title??`-`],[`Canonical`,r.finalPage.canonical??`-`],[`Meta robots`,r.finalPage.metaRobots??`-`]])),Y(`Warnings`,r.warnings.slice(0,10)))}}),ac={matches:`matches`,chain:`chain`,redirects:`redirects`,wrong_target:`wrong target`,not_redirected:`not redirected`,loop:`loop`,too_many_hops:`too many hops`,timeout:`timeout`,error:`error`},oc=[`matches`,`redirects`];async function sc(e){process.stdout.write(e)||await new Promise(e=>process.stdout.once(`drain`,e))}async function cc(e,t){for(let t of[`url`,`max-hops`])if(T(e[t])!==void 0)throw new g(`INVALID_INPUT`,`--${t} does not apply to --map.`);for(let t of[`refresh`,`js`])if(e[t]===!0)throw new g(`INVALID_INPUT`,`--${t} does not apply to --map.`);let n=(await Mr(t)).size;if(n>2e6)throw new g(`INVALID_INPUT`,`${t} is ${n.toLocaleString(`en-US`)} bytes. Map mode reads up to ${Wn.toLocaleString(`en-US`)} bytes and ${qe.toLocaleString(`en-US`)} rows per run, so split the file.`);let r=h(await y(t,`utf8`)),i=j(e),a=!i&&process.stderr.isTTY,o=[],s=0,c=0,l=await p(r,async e=>{c+=1,i?await sc(`${JSON.stringify(e)}\n`):oc.includes(e.verdict)&&e.final_status===200||(s+=1,o.length<50&&o.push(e)),a&&process.stderr.write(`\rTraced ${c} of ${r.length}`)});if(a&&process.stderr.write(`
69
+ `),i){await sc(`${JSON.stringify(l)}\n`);return}let d=Object.keys(ac).filter(e=>l.verdicts[e]).map(e=>`${l.verdicts[e]} ${ac[e]}`);K({title:`Redirect map`,target:t,status:oc.reduce((e,t)=>e+(l.verdicts[t]??0),0)===l.rows&&!l.final_not_200?`pass`:`warning`,summary:`${l.rows} rows traced: ${d.join(`, `)}.${l.final_not_200?` ${l.final_not_200} ended on a status other than 200.`:``}`,metrics:[{label:`Rows`,value:l.rows},...Object.keys(ac).filter(e=>l.verdicts[e]).map(e=>({label:ac[e],value:l.verdicts[e]??0}))]}),o.length&&(process.stdout.write(`
70
70
  Rows to look at
71
- `),R([`Line`,`Verdict`,`Hops`,`Status`,`Old URL`,`Ends at`],o.map(e=>[e.line,ic[e.verdict],e.hops,e.final_status??`-`,G(e.url,50),G(e.error?`${e.error} at ${e.final_url}`:e.final_url,72)])),s>o.length&&process.stdout.write(`Showing the first ${o.length}. Use --json for every row.\n`)),process.stdout.write(`\nEach hop waits up to ${Ue/1e3}s, up to 10 hops per URL, with the SEO Skill crawler user agent.\n`)}const cc=b({meta:{name:`ai-referrals`,description:`Find AI referral traffic detected in Google Analytics`},args:{property:{type:`string`,description:"Numeric Google Analytics property ID. Run `seo analytics google properties` to list them."},"start-date":{type:`string`,default:`28daysAgo`},"end-date":{type:`string`,default:`yesterday`},"max-rows":{type:`string`,description:`Maximum Google Analytics rows per query. Defaults to 100000.`},"result-limit":{type:`string`,description:`Maximum ranked landing pages returned. Defaults to 25.`},limit:{type:`string`,description:`Legacy alias for --max-rows.`},refresh:{type:`boolean`,default:!1,description:`Bypass cached Google Analytics responses.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=A(e),n=vi(w(e.property)),r=await p({property:n,startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),maxRows:O(e[`max-rows`],`--max-rows`),resultLimit:O(e[`result-limit`],`--result-limit`),limit:O(e.limit,`--limit`),refresh:e.refresh===!0});if(t)I(r);else{if(K({title:`AI referral report`,target:r.property,status:r.dataStatus===`complete`?`info`:`unknown`,summary:r.summary.verdict,metrics:[{label:`Evidence`,value:r.dataStatus===`complete`?`Complete`:`Partial`,status:r.dataStatus===`complete`?`pass`:`unknown`},{label:`AI sessions`,value:U(r.summary.sessions)},{label:`AI users`,value:r.summary.totalUsers===null?`Unavailable`:U(r.summary.totalUsers)},{label:`Sources`,value:U(r.summary.sources)},{label:`Landing pages`,value:U(r.summary.landingPages)},{label:`Landing-page output`,value:`${U(r.selection.landingPages.returnedRows)} returned / ${U(r.selection.landingPages.retainedRows)} retained`},{label:`Source rows`,value:U(r.dataSource.sourceDiscovery.returnedRows)}]}),r.sources.length&&(process.stdout.write(`
71
+ `),z([`Line`,`Verdict`,`Hops`,`Status`,`Old URL`,`Ends at`],o.map(e=>[e.line,ac[e.verdict],e.hops,e.final_status??`-`,G(e.url,50),G(e.error?`${e.error} at ${e.final_url}`:e.final_url,72)])),s>o.length&&process.stdout.write(`Showing the first ${o.length}. Use --json for every row.\n`)),process.stdout.write(`\nEach hop waits up to ${u/1e3}s, up to 10 hops per URL, with the SEO Skill crawler user agent.\n`)}const lc=b({meta:{name:`ai-referrals`,description:`Find AI referral traffic detected in Google Analytics`},args:{property:{type:`string`,description:"Numeric Google Analytics property ID. Run `seo analytics google properties` to list them."},"start-date":{type:`string`,default:`28daysAgo`},"end-date":{type:`string`,default:`yesterday`},"max-rows":{type:`string`,description:`Maximum Google Analytics rows per query. Defaults to 100000.`},"result-limit":{type:`string`,description:`Maximum ranked landing pages returned. Defaults to 25.`},limit:{type:`string`,description:`Legacy alias for --max-rows.`},refresh:{type:`boolean`,default:!1,description:`Bypass cached Google Analytics responses.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=j(e),n=vi(T(e.property)),r=await je({property:n,startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),maxRows:k(e[`max-rows`],`--max-rows`),resultLimit:k(e[`result-limit`],`--result-limit`),limit:k(e.limit,`--limit`),refresh:e.refresh===!0});if(t)L(r);else{if(K({title:`AI referral report`,target:r.property,status:r.dataStatus===`complete`?`info`:`unknown`,summary:r.summary.verdict,metrics:[{label:`Evidence`,value:r.dataStatus===`complete`?`Complete`:`Partial`,status:r.dataStatus===`complete`?`pass`:`unknown`},{label:`AI sessions`,value:U(r.summary.sessions)},{label:`AI users`,value:r.summary.totalUsers===null?`Unavailable`:U(r.summary.totalUsers)},{label:`Sources`,value:U(r.summary.sources)},{label:`Landing pages`,value:U(r.summary.landingPages)},{label:`Landing-page output`,value:`${U(r.selection.landingPages.returnedRows)} returned / ${U(r.selection.landingPages.retainedRows)} retained`},{label:`Source rows`,value:U(r.dataSource.sourceDiscovery.returnedRows)}]}),r.sources.length&&(process.stdout.write(`
72
72
  AI sources
73
73
  `),q([`Source`,`Sessions`,`Events`,`Share of AI sessions`],r.sources.map(e=>[e.label,U(e.sessions),U(e.eventCount),W(e.shareOfAiSessions)]))),r.landingPages.length&&(process.stdout.write(`
74
74
  Landing pages
75
75
  `),q([`Landing page`,`Sessions`,`Events`,`Top source`],r.landingPages.map(e=>[G(e.landingPage,64),U(e.sessions),U(e.eventCount),e.topSourceDetails.label]))),r.dataSource.partialReasons.length){process.stdout.write(`
76
76
  Evidence warnings
77
- `);for(let e of r.dataSource.partialReasons)process.stdout.write(`- ${e}\n`)}process.stdout.write(`\nNote: ${r.summary.caveat}\n`)}}}),lc=b({meta:{name:`cannibal`,description:`Find queries with material exposure across multiple URLs`},args:{site:{type:`string`},...X([`days`,`limit`,`includeBrand`,`minImpressions`,`refresh`],{minImpressions:{description:`Minimum query impressions. Defaults to 50.`}}),"brand-terms":{type:`string`,description:`Comma-separated brand terms to exclude.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=A(e),n=T(e.refresh),r=j(w(e.site)),i=await de({site:r,days:O(e.days,`--days`),limit:O(e.limit,`--limit`),minImpressions:O(e[`min-impressions`],`--min-impressions`),brandTerms:[...k(e[`brand-terms`])??[]],includeBrand:T(e[`include-brand`]),refresh:n});t?I(i):(K({title:`Cannibalisation report`,target:i.site,status:i.dataStatus===`complete`?`info`:`unknown`,summary:i.summary.verdict,metrics:[{label:`Evidence`,value:i.dataStatus},{label:`Range`,value:`${i.range.startDate} to ${i.range.endDate}`},{label:`Eligible`,value:U(i.summary.eligibleClusters)},{label:`Returned`,value:U(i.summary.returnedClusters)},{label:`Suppressed brand queries`,value:U(i.summary.suppressedQueries)},{label:`GSC completeness`,value:i.source.completeness},{label:`Brand queries`,value:T(e[`include-brand`])?`included`:`excluded`}]}),q([`Query`,`URLs`,`Property impr`,`Exposure`,`HHI`,`Review first`],i.items.map(e=>[G(e.query,42),e.pages.length,e.propertyImpressions===void 0?`n/a`:U(e.propertyImpressions),U(e.pageExposureImpressions),e.hhi.toFixed(2),G(e.suggestedOwnerUrl,56)])),J(`Top cannibalisation actions`,i.items.map(e=>({label:e.query,context:`${e.pages.length} material URLs; first review ${G(e.suggestedOwnerUrl,64)}`,action:e.recommendation.action}))),Y(`Report caveats`,i.caveats),Y(`Provider usage`,[i.ledgerSummary]))}}),uc=b({meta:{name:`ctr-underperformers`,description:`Find high-impression queries with weak CTR for their position`},args:{site:{type:`string`},...X([`includeBrand`,`minImpressions`],{minImpressions:{description:`Minimum query impressions. Defaults to 1 per 5,000 retained impressions, from 30 to 200.`}}),json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=A(e),n=j(w(e.site)),r=await ce({site:n,minImpressions:D(e[`min-impressions`]),includeBrand:T(e[`include-brand`])});t?I(r):(K({title:`CTR underperformers`,target:r.site,status:r.dataStatus===`partial`?`unknown`:`info`,summary:r.summary.verdict,metrics:[{label:`Underperformers`,value:U(r.summary.underperformers)},{label:`Estimated click gap`,value:U(r.summary.estimatedClickShortfall)},{label:`Brand queries`,value:r.summary.brandFiltering},{label:`Evidence`,value:r.dataStatus},{label:`GSC completeness`,value:r.source.completeness}]}),Y(`Why this matters`,[`These queries already have visibility; the fastest win is usually better SERP framing, not new content.`,`Start with high-impression rows where actual CTR is far below the expected CTR for that ranking position.`]),Y(`Recommended actions`,r.recommendations),Y(`Report caveats`,r.caveats),q([`Query`,`URL`,`Pos`,`Impr`,`CTR`,`Expected`,`Gap`,`Action`],r.items.map(e=>[G(e.query,36),G(e.url,48),qa(e.position),U(e.impressions),W(e.actualCtr),W(e.expectedCtr),U(e.clickShortfall),G(e.recommendation.action,72)])),J(`Top CTR actions`,r.items.map(e=>({label:e.query,context:`${U(e.clickShortfall)} click gap, ${W(e.actualCtr)} CTR`,action:e.recommendation.action}))))}});function dc(e){return e>0?qa(e):`n/a`}const fc=b({meta:{name:`decaying`,description:`Find query/page rows losing clicks between two GSC windows`},args:{site:{type:`string`},...X([`days`,`limit`,`includeBrand`,`refresh`]),comparison:{type:`string`,description:`Comparison: previous-period or year-over-year.`},"brand-terms":{type:`string`,description:`Comma-separated brand terms to exclude.`},"min-drop-pct":{type:`string`,description:`Minimum click drop percentage. Defaults to 20.`},"min-previous-clicks":{type:`string`,description:`Minimum previous-window clicks for a query/page row. Defaults to 2.`},"min-click-loss":{type:`string`,description:`Minimum absolute click loss. Defaults to 1.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=A(e),n=T(e.refresh),r=w(e.comparison),i=O(e.days,`--days`),a=O(e.limit,`--limit`),o=O(e[`min-drop-pct`],`--min-drop-pct`),s=O(e[`min-previous-clicks`],`--min-previous-clicks`),c=O(e[`min-click-loss`],`--min-click-loss`);if(r!==void 0&&r!==`previous-period`&&r!==`year-over-year`)throw new g(`INVALID_INPUT`,`--comparison must be previous-period or year-over-year.`);let l=j(w(e.site)),u=await et({site:l,days:i,limit:a,comparison:r,brandTerms:[...k(e[`brand-terms`])??[]],includeBrand:T(e[`include-brand`]),minDropPct:o,minPreviousClicks:s,minClickLoss:c,refresh:n});t?I(u):(K({title:`Decaying pages`,target:u.site,status:u.dataStatus===`complete`?u.summary.returnedRows>0?`warning`:`pass`:`unknown`,summary:u.summary.verdict,metrics:[{label:`Evidence`,value:u.dataStatus},{label:`Comparison`,value:u.comparison},{label:`Observed declines`,value:U(u.summary.returnedRows)},{label:`Eligible declines`,value:U(u.summary.eligibleRows)},{label:`Decay clusters`,value:U(u.summary.groups)},{label:`Observed click decline`,value:U(u.summary.returnedObservedRetainedQueryClickLoss)},{label:`Eligible click decline`,value:U(u.summary.observedRetainedQueryClickLoss)},{label:`Brand queries`,value:u.summary.brandFiltering},{label:`Minimum drop`,value:`${u.filters.minDropPct}%`},{label:`Minimum previous clicks`,value:U(u.filters.minPreviousClicks)},{label:`GSC completeness`,value:u.source.completeness}]}),Y(`Why this matters`,[`This report compares retained query/page rows and only counts declines observed in both windows.`,`Position, CTR, and impression movements are signals for investigation, not proof of cause.`]),Y(`Recommended actions`,u.recommendations),Y(`Report caveats`,u.caveats),Y(`Warnings`,u.warnings),u.items.length&&(u.groups.length&&(q([`Cluster`,`Rows`,`Observed loss`,`Drop`,`Sample query`,`Action`],u.groups.map(e=>[G(e.label,42),U(e.count),U(e.totalClickLoss),`${e.averageDropPct.toFixed(1)}%`,G(e.sampleQueries[0]??`-`,40),G(e.recommendation,72)])),J(`Top decay cluster actions`,u.groups.map(e=>({label:e.label,context:`${U(e.count)} retained rows, ${U(e.totalClickLoss)} observed click decline`,action:e.recommendation})))),q([`Query`,`Template`,`URL`,`Signals`,`Decline`,`Clicks`,`Impr`,`CTR`,`Pos`,`Action`],u.items.map(e=>[G(e.query,32),G(e.template.label,26),G(e.url,44),e.signals.join(`, `),U(e.clickLoss),`${U(e.previous.clicks)} -> ${U(e.current.clicks)}`,`${U(e.previous.impressions)} -> ${U(e.current.impressions)}`,`${W(e.previous.ctr)} -> ${W(e.current.ctr)}`,`${dc(e.previous.position)} -> ${dc(e.current.position)}`,G(e.recommendation.action,64)])),J(`Top decay actions`,u.items.map(e=>({label:e.query,context:`${e.template.label}, ${U(e.clickLoss)} fewer observed clicks`,action:e.recommendation.action})))))}}),pc=b({meta:{name:`internal-links`,description:`Internal links to add across a site (--site) or to one page (--url), placed in real sentences`},args:{site:{type:`string`,description:`Site to audit: a domain, URL or Search Console property.`},url:{type:`string`,description:`One page to find internal links to.`},"max-pages":{type:`string`,description:`Site audits only: pages to crawl, 500 by default, up to 5,000.`},...pa,...ma,refresh:{type:`boolean`,default:!1,description:`Bypass the local cache and fetch fresh data.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=T(e.refresh),r=w(e.url),i=w(e.site);if(!!r==!!i)throw new g(`INVALID_INPUT`,`Send --site to audit a whole site, or --url to find links to one page.`);let a=w(e[`max-pages`]),o=a===void 0?void 0:D(a);if(a!==void 0&&o===void 0)throw new g(`INVALID_INPUT`,`--max-pages is a whole number.`);let s=r??i,c=await ae({site:s,targetUrl:r,maxPages:o,startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),refresh:n},{...await ee({site:s,refresh:n}),progress:ca(!t&&process.stderr.isTTY)});if(t){I(c);return}let l={...r?{url:r}:{site:i},...o===void 0?{}:{"max-pages":o}};process.stdout.write(`${yn(c,{detail:V(e),offset:D(e.offset)??0,more:(e,t)=>da(`internal-links`,{...l,detail:e,offset:t})})}\n`)}}),mc=b({meta:{name:`page-opportunities`,description:`Find first-party traffic growth opportunities for one URL`},args:{site:{type:`string`},url:{type:`string`,required:!0},...X([`days`,`limit`,`minImpressions`,`includeBrand`,`js`,`refresh`],{limit:{description:`Maximum page queries to inspect. Defaults to 25.`},minImpressions:{description:`Minimum query impressions. Defaults to 10.`},includeBrand:{description:`Include branded queries in page opportunity reports.`},js:{description:`Force JavaScript rendering for page extraction.`},refresh:{description:`Bypass local GSC and HTTP cache.`}}),"verify-content":ui(`Fetch the page for title/H1/body checks. Defaults to true.`,`Skip fetching the page for title/H1/body checks.`),json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=A(e),n=j(w(e.site)),r=await mt({site:n,url:w(e.url)??``,days:D(e.days),limit:D(e.limit),minImpressions:D(e[`min-impressions`]),includeBrand:T(e[`include-brand`]),verifyContent:T(e[`verify-content`])!==!1,js:T(e.js)?!0:`auto`,refresh:T(e.refresh)});t?I(r):(K({title:`Page opportunities`,target:r.url,status:r.verification.status===`failed`?`unknown`:`info`,summary:r.summary.verdict,metrics:[{label:`Title`,value:r.page?.title??`Not fetched`},{label:`Fetch`,value:So(r.page?.fetchDiagnostics)},{label:`Verification`,value:r.verification.status},{label:`Rows`,value:`${U(r.selection.returnedRows)} returned / ${U(r.selection.eligibleRows)} eligible / ${U(r.selection.sourceRows)} source`},{label:`Queries`,value:U(r.summary.queries)},{label:`Impressions`,value:U(r.summary.impressions)},{label:`Opportunities`,value:U(r.summary.opportunities)},{label:`CTR shortfall`,value:U(r.summary.estimatedCtrClickShortfall)},{label:`Focus`,value:r.summary.focus},{label:`Evidence`,value:r.dataStatus}]}),Y(`Why this matters`,[`This report starts from queries where this exact URL already has impressions, so recommendations stay tied to first-party demand.`,`Content verification separates body gaps from title/meta framing issues, which helps avoid adding copy when the page already covers the query.`]),Y(`Recommended actions`,r.recommendations),Y(`Report caveats`,r.caveats),r.items.length?(q([`Type`,`Query`,`Evidence`,`Pos`,`CTR`,`Expected`,`Impr`,`Shortfall`,`Action`],r.items.map(e=>[e.opportunityType,G(e.query,38),e.verification.signals.length?e.verification.signals.join(`, `):e.verification.status,e.position.toFixed(1),W(e.ctr),e.expectedCtr===void 0?`n/a`:W(e.expectedCtr),U(e.impressions),e.estimatedCtrClickShortfall===void 0?`n/a`:U(e.estimatedCtrClickShortfall),G(e.recommendation,72)])),J(`Top page actions`,r.items.map(e=>({label:e.query,context:`${e.opportunityType}, ${U(e.impressions)} impressions`,action:e.recommendation})))):process.stdout.write(r.dataStatus===`empty`?`No GSC query rows found for this exact URL.
77
+ `);for(let e of r.dataSource.partialReasons)process.stdout.write(`- ${e}\n`)}process.stdout.write(`\nNote: ${r.summary.caveat}\n`)}}}),uc=b({meta:{name:`cannibal`,description:`Find queries with material exposure across multiple URLs`},args:{site:{type:`string`},...X([`days`,`limit`,`includeBrand`,`minImpressions`,`refresh`],{minImpressions:{description:`Minimum query impressions. Defaults to 50.`}}),"brand-terms":{type:`string`,description:`Comma-separated brand terms to exclude.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=j(e),n=E(e.refresh),r=M(T(e.site)),i=await Ze({site:r,days:k(e.days,`--days`),limit:k(e.limit,`--limit`),minImpressions:k(e[`min-impressions`],`--min-impressions`),brandTerms:[...A(e[`brand-terms`])??[]],includeBrand:E(e[`include-brand`]),refresh:n});t?L(i):(K({title:`Cannibalisation report`,target:i.site,status:i.dataStatus===`complete`?`info`:`unknown`,summary:i.summary.verdict,metrics:[{label:`Evidence`,value:i.dataStatus},{label:`Range`,value:`${i.range.startDate} to ${i.range.endDate}`},{label:`Eligible`,value:U(i.summary.eligibleClusters)},{label:`Returned`,value:U(i.summary.returnedClusters)},{label:`Suppressed brand queries`,value:U(i.summary.suppressedQueries)},{label:`GSC completeness`,value:i.source.completeness},{label:`Brand queries`,value:E(e[`include-brand`])?`included`:`excluded`}]}),q([`Query`,`URLs`,`Property impr`,`Exposure`,`HHI`,`Review first`],i.items.map(e=>[G(e.query,42),e.pages.length,e.propertyImpressions===void 0?`n/a`:U(e.propertyImpressions),U(e.pageExposureImpressions),e.hhi.toFixed(2),G(e.suggestedOwnerUrl,56)])),J(`Top cannibalisation actions`,i.items.map(e=>({label:e.query,context:`${e.pages.length} material URLs; first review ${G(e.suggestedOwnerUrl,64)}`,action:e.recommendation.action}))),Y(`Report caveats`,i.caveats),Y(`Provider usage`,[i.ledgerSummary]))}}),dc=b({meta:{name:`ctr-underperformers`,description:`Find high-impression queries with weak CTR for their position`},args:{site:{type:`string`},...X([`includeBrand`,`minImpressions`],{minImpressions:{description:`Minimum query impressions. Defaults to 1 per 5,000 retained impressions, from 30 to 200.`}}),json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=j(e),n=M(T(e.site)),r=await be({site:n,minImpressions:O(e[`min-impressions`]),includeBrand:E(e[`include-brand`])});t?L(r):(K({title:`CTR underperformers`,target:r.site,status:r.dataStatus===`partial`?`unknown`:`info`,summary:r.summary.verdict,metrics:[{label:`Underperformers`,value:U(r.summary.underperformers)},{label:`Estimated click gap`,value:U(r.summary.estimatedClickShortfall)},{label:`Brand queries`,value:r.summary.brandFiltering},{label:`Evidence`,value:r.dataStatus},{label:`GSC completeness`,value:r.source.completeness}]}),Y(`Why this matters`,[`These queries already have visibility; the fastest win is usually better SERP framing, not new content.`,`Start with high-impression rows where actual CTR is far below the expected CTR for that ranking position.`]),Y(`Recommended actions`,r.recommendations),Y(`Report caveats`,r.caveats),q([`Query`,`URL`,`Pos`,`Impr`,`CTR`,`Expected`,`Gap`,`Action`],r.items.map(e=>[G(e.query,36),G(e.url,48),Ja(e.position),U(e.impressions),W(e.actualCtr),W(e.expectedCtr),U(e.clickShortfall),G(e.recommendation.action,72)])),J(`Top CTR actions`,r.items.map(e=>({label:e.query,context:`${U(e.clickShortfall)} click gap, ${W(e.actualCtr)} CTR`,action:e.recommendation.action}))))}});function fc(e){return e>0?Ja(e):`n/a`}const pc=b({meta:{name:`decaying`,description:`Find query/page rows losing clicks between two GSC windows`},args:{site:{type:`string`},...X([`days`,`limit`,`includeBrand`,`refresh`]),comparison:{type:`string`,description:`Comparison: previous-period or year-over-year.`},"brand-terms":{type:`string`,description:`Comma-separated brand terms to exclude.`},"min-drop-pct":{type:`string`,description:`Minimum click drop percentage. Defaults to 20.`},"min-previous-clicks":{type:`string`,description:`Minimum previous-window clicks for a query/page row. Defaults to 2.`},"min-click-loss":{type:`string`,description:`Minimum absolute click loss. Defaults to 1.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=j(e),n=E(e.refresh),r=T(e.comparison),i=k(e.days,`--days`),a=k(e.limit,`--limit`),o=k(e[`min-drop-pct`],`--min-drop-pct`),s=k(e[`min-previous-clicks`],`--min-previous-clicks`),c=k(e[`min-click-loss`],`--min-click-loss`);if(r!==void 0&&r!==`previous-period`&&r!==`year-over-year`)throw new g(`INVALID_INPUT`,`--comparison must be previous-period or year-over-year.`);let l=M(T(e.site)),u=await de({site:l,days:i,limit:a,comparison:r,brandTerms:[...A(e[`brand-terms`])??[]],includeBrand:E(e[`include-brand`]),minDropPct:o,minPreviousClicks:s,minClickLoss:c,refresh:n});t?L(u):(K({title:`Decaying pages`,target:u.site,status:u.dataStatus===`complete`?u.summary.returnedRows>0?`warning`:`pass`:`unknown`,summary:u.summary.verdict,metrics:[{label:`Evidence`,value:u.dataStatus},{label:`Comparison`,value:u.comparison},{label:`Observed declines`,value:U(u.summary.returnedRows)},{label:`Eligible declines`,value:U(u.summary.eligibleRows)},{label:`Decay clusters`,value:U(u.summary.groups)},{label:`Observed click decline`,value:U(u.summary.returnedObservedRetainedQueryClickLoss)},{label:`Eligible click decline`,value:U(u.summary.observedRetainedQueryClickLoss)},{label:`Brand queries`,value:u.summary.brandFiltering},{label:`Minimum drop`,value:`${u.filters.minDropPct}%`},{label:`Minimum previous clicks`,value:U(u.filters.minPreviousClicks)},{label:`GSC completeness`,value:u.source.completeness}]}),Y(`Why this matters`,[`This report compares retained query/page rows and only counts declines observed in both windows.`,`Position, CTR, and impression movements are signals for investigation, not proof of cause.`]),Y(`Recommended actions`,u.recommendations),Y(`Report caveats`,u.caveats),Y(`Warnings`,u.warnings),u.items.length&&(u.groups.length&&(q([`Cluster`,`Rows`,`Observed loss`,`Drop`,`Sample query`,`Action`],u.groups.map(e=>[G(e.label,42),U(e.count),U(e.totalClickLoss),`${e.averageDropPct.toFixed(1)}%`,G(e.sampleQueries[0]??`-`,40),G(e.recommendation,72)])),J(`Top decay cluster actions`,u.groups.map(e=>({label:e.label,context:`${U(e.count)} retained rows, ${U(e.totalClickLoss)} observed click decline`,action:e.recommendation})))),q([`Query`,`Template`,`URL`,`Signals`,`Decline`,`Clicks`,`Impr`,`CTR`,`Pos`,`Action`],u.items.map(e=>[G(e.query,32),G(e.template.label,26),G(e.url,44),e.signals.join(`, `),U(e.clickLoss),`${U(e.previous.clicks)} -> ${U(e.current.clicks)}`,`${U(e.previous.impressions)} -> ${U(e.current.impressions)}`,`${W(e.previous.ctr)} -> ${W(e.current.ctr)}`,`${fc(e.previous.position)} -> ${fc(e.current.position)}`,G(e.recommendation.action,64)])),J(`Top decay actions`,u.items.map(e=>({label:e.query,context:`${e.template.label}, ${U(e.clickLoss)} fewer observed clicks`,action:e.recommendation.action})))))}}),mc=b({meta:{name:`internal-links`,description:`Internal links to add across a site (--site) or to one page (--url), placed in real sentences`},args:{site:{type:`string`,description:`Site to audit: a domain, URL or Search Console property.`},url:{type:`string`,description:`One page to find internal links to.`},"max-pages":{type:`string`,description:`Site audits only: pages to crawl, 500 by default, up to 5,000.`},...ma,...ha,refresh:{type:`boolean`,default:!1,description:`Bypass the local cache and fetch fresh data.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=E(e.refresh),r=T(e.url),i=T(e.site);if(!!r==!!i)throw new g(`INVALID_INPUT`,`Send --site to audit a whole site, or --url to find links to one page.`);let a=T(e[`max-pages`]),o=a===void 0?void 0:O(a);if(a!==void 0&&o===void 0)throw new g(`INVALID_INPUT`,`--max-pages is a whole number.`);let s=r??i,c=await me({site:s,targetUrl:r,maxPages:o,startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),refresh:n},{...await ee({site:s,refresh:n}),progress:la(!t&&process.stderr.isTTY)});if(t){L(c);return}let l={...r?{url:r}:{site:i},...o===void 0?{}:{"max-pages":o}};process.stdout.write(`${_n(c,{detail:V(e),offset:O(e.offset)??0,more:(e,t)=>fa(`internal-links`,{...l,detail:e,offset:t})})}\n`)}}),hc=b({meta:{name:`page-opportunities`,description:`Find first-party traffic growth opportunities for one URL`},args:{site:{type:`string`},url:{type:`string`,required:!0},...X([`days`,`limit`,`minImpressions`,`includeBrand`,`js`,`refresh`],{limit:{description:`Maximum page queries to inspect. Defaults to 25.`},minImpressions:{description:`Minimum query impressions. Defaults to 10.`},includeBrand:{description:`Include branded queries in page opportunity reports.`},js:{description:`Force JavaScript rendering for page extraction.`},refresh:{description:`Bypass local GSC and HTTP cache.`}}),"verify-content":ui(`Fetch the page for title/H1/body checks. Defaults to true.`,`Skip fetching the page for title/H1/body checks.`),json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=j(e),n=M(T(e.site)),r=await Bn({site:n,url:T(e.url)??``,days:O(e.days),limit:O(e.limit),minImpressions:O(e[`min-impressions`]),includeBrand:E(e[`include-brand`]),verifyContent:E(e[`verify-content`])!==!1,js:E(e.js)?!0:`auto`,refresh:E(e.refresh)});t?L(r):(K({title:`Page opportunities`,target:r.url,status:r.verification.status===`failed`?`unknown`:`info`,summary:r.summary.verdict,metrics:[{label:`Title`,value:r.page?.title??`Not fetched`},{label:`Fetch`,value:Co(r.page?.fetchDiagnostics)},{label:`Verification`,value:r.verification.status},{label:`Rows`,value:`${U(r.selection.returnedRows)} returned / ${U(r.selection.eligibleRows)} eligible / ${U(r.selection.sourceRows)} source`},{label:`Queries`,value:U(r.summary.queries)},{label:`Impressions`,value:U(r.summary.impressions)},{label:`Opportunities`,value:U(r.summary.opportunities)},{label:`CTR shortfall`,value:U(r.summary.estimatedCtrClickShortfall)},{label:`Focus`,value:r.summary.focus},{label:`Evidence`,value:r.dataStatus}]}),Y(`Why this matters`,[`This report starts from queries where this exact URL already has impressions, so recommendations stay tied to first-party demand.`,`Content verification separates body gaps from title/meta framing issues, which helps avoid adding copy when the page already covers the query.`]),Y(`Recommended actions`,r.recommendations),Y(`Report caveats`,r.caveats),r.items.length?(q([`Type`,`Query`,`Evidence`,`Pos`,`CTR`,`Expected`,`Impr`,`Shortfall`,`Action`],r.items.map(e=>[e.opportunityType,G(e.query,38),e.verification.signals.length?e.verification.signals.join(`, `):e.verification.status,e.position.toFixed(1),W(e.ctr),e.expectedCtr===void 0?`n/a`:W(e.expectedCtr),U(e.impressions),e.estimatedCtrClickShortfall===void 0?`n/a`:U(e.estimatedCtrClickShortfall),G(e.recommendation,72)])),J(`Top page actions`,r.items.map(e=>({label:e.query,context:`${e.opportunityType}, ${U(e.impressions)} impressions`,action:e.recommendation})))):process.stdout.write(r.dataStatus===`empty`?`No GSC query rows found for this exact URL.
78
78
  `:`GSC rows were found, but none met the report criteria.
79
- `))}}),hc=b({meta:{name:`query-cluster`,description:`Cluster GSC queries into repeated demand themes`},args:{site:{type:`string`},scope:{type:`string`},...X([`includeBrand`,`minImpressions`,`limit`],{includeBrand:{description:`Include branded queries in query clustering.`},minImpressions:{description:`Minimum impressions per query. Defaults to 25.`},limit:{description:`Maximum clusters to return. Defaults to 25; max 100.`}}),json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=A(e),n=j(w(e.site)),r=await a({site:n,scope:w(e.scope),includeBrand:T(e[`include-brand`]),minImpressions:D(e[`min-impressions`]),limit:D(e.limit)});t?I(r):(K({title:`Query clusters`,target:r.site,status:`info`,summary:r.summary.verdict,metrics:[{label:`Scope`,value:r.scope??`All pages`},{label:`Clusters`,value:U(r.summary.clusters)},{label:`Queries`,value:U(r.summary.queries)},{label:`Impressions`,value:U(r.summary.impressions)},{label:`Clicks`,value:U(r.summary.clicks)},{label:`Minimum impressions`,value:U(r.summary.minImpressions)},{label:`Result limit`,value:U(r.summary.limit)},{label:`High opportunity`,value:U(r.summary.highOpportunityClusters)},{label:`Brand queries`,value:r.summary.brandFiltering}]}),Y(`Why this matters`,[`Clusters show repeated demand themes, so they are better inputs for page sections, hubs, and templates than one-off query exports.`,`Prioritise clusters with high impressions and low clicks; they usually reveal unclear intent coverage or weak SERP framing.`]),Y(`Recommended actions`,r.recommendations),Y(`Report caveats`,r.caveats),q([`Cluster`,`Intent`,`Queries`,`Impr`,`CTR`,`Expected`,`Lift`,`Action`],r.clusters.map(e=>[G(e.label,32),e.intent,e.queries.length,U(e.totals?.impressions??0),W(e.totals?.ctr??0),W(e.benchmark?.expectedCtr??0),e.estimatedClickLift===void 0?`-`:U(e.estimatedClickLift),G(e.recommendation??``,72)])),J(`Top cluster actions`,r.clusters.map(e=>({label:e.label,context:`${U(e.totals?.impressions??0)} impressions, ${e.estimatedClickLift===void 0?`ranking opportunity`:`${U(e.estimatedClickLift)} estimated click lift`}`,action:e.recommendation??``}))))}}),gc=b({meta:{name:`quick-wins`,description:`Find high-ranking low-CTR query/page opportunities`},args:{site:{type:`string`},...X([`days`,`limit`,`includeBrand`,`minImpressions`,`verifyContent`,`verifyLimit`,`js`,`fetchConcurrency`,`fetchIntervalCap`,`fetchIntervalMs`,`refresh`]),"brand-terms":{type:`string`,description:`Comma-separated brand terms to exclude.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=A(e),n=D(e[`verify-limit`]),r=T(e[`verify-content`])===!0||n!==void 0,i=j(w(e.site)),a=await c({site:i,days:D(e.days),limit:D(e.limit),minImpressions:D(e[`min-impressions`]),brandTerms:[...k(e[`brand-terms`])??[]],includeBrand:T(e[`include-brand`]),verifyContent:r,verifyLimit:n,js:T(e.js)?!0:void 0,rate:hi(e),refresh:T(e.refresh)});t?I(a):(K({title:`Quick wins`,target:a.site,status:a.source.possiblyTruncated?`unknown`:`info`,summary:a.summary.verdict,metrics:[{label:`Eligible rows`,value:U(a.summary.eligibleRows)},{label:`Returned rows`,value:U(a.summary.returnedRows)},{label:`Query clusters`,value:U(a.summary.repeatedQueryGroups)},{label:`Template patterns`,value:U(a.summary.templatePatterns)},{label:`CTR click shortfall`,value:U(a.summary.returnedEstimatedCtrClickShortfall)},{label:`Brand queries`,value:a.summary.brandFiltering},{label:`Evidence`,value:a.dataStatus},{label:`Verification`,value:Ja(a)}]}),Y(`Why this matters`,[`These rows have GSC average positions from 4 to 10 and observed CTR below a site-peer or versioned fallback target.`,`Page evidence is needed before treating a CTR shortfall as a title, content, or technical finding.`]),Y(`Recommended actions`,a.recommendations),Y(`Report caveats`,a.caveats),a.items.length&&(a.templateRecommendations.length&&(q([`Template`,`URLs`,`Rows`,`Shortfall`,`Impr`,`Action`],a.templateRecommendations.map(e=>[G(e.templateLabel,34),U(e.urlCount),U(e.rowCount),U(e.totalEstimatedCtrClickShortfall),U(e.totalImpressions),G(e.action,72)])),J(`Top template actions`,a.templateRecommendations.map(e=>({label:e.templateLabel,context:`${U(e.urlCount)} URLs, ${U(e.totalEstimatedCtrClickShortfall)} heuristic CTR click shortfall`,action:`${e.action} ${e.evidence}`})))),a.groups.length&&(q([`Cluster`,`URLs`,`Rows`,`Shortfall`,`Impr`,`Sample URL`,`Action`],a.groups.map(e=>[G(e.label,44),U(e.urlCount),U(e.rowCount),U(e.totalEstimatedCtrClickShortfall),U(e.totalImpressions),G(e.sampleUrls[0]??`-`,46),G(e.recommendation,72)])),J(`Top cluster actions`,a.groups.map(e=>({label:e.label,context:`${U(e.urlCount)} URLs, ${U(e.totalEstimatedCtrClickShortfall)} heuristic CTR click shortfall`,action:e.recommendation})))),q([`Query`,`Template`,`URL`,`Pos`,`Impr`,`CTR / target`,`Shortfall`,`Fetch`,`Check`,`Action`],a.items.map(e=>[G(e.query,36),G(e.template.label,24),G(e.url,48),qa(e.position),U(e.impressions),`${W(e.ctr)} / ${W(e.targetCtr)}`,U(e.estimatedCtrClickShortfall),So(e.contentVerification?.fetchDiagnostics),Co(e.contentVerification?.classification),G(e.recommendation.action,64)])),J(`Top opportunity actions`,a.items.map(e=>({label:e.query,context:`${e.template.label}, pos ${qa(e.position)}, ${U(e.impressions)} impressions`,action:e.recommendation.action})))))}}),_c=b({meta:{name:`second-page`,description:`Find URLs averaging positions above 10 through 20 in GSC`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},...X([`range`,`minImpressions`,`limit`,`includeBrand`,`verifyContent`,`verifyLimit`,`js`,`fetchConcurrency`,`fetchIntervalCap`,`fetchIntervalMs`,`refresh`]),json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=D(e[`verify-limit`]),r=T(e[`verify-content`])===!0||n!==void 0,i=j(w(e.site)),a=await rn({site:i,range:D(e.days),minImpressions:D(e[`min-impressions`]),limit:w(e.limit)?Number(w(e.limit)):10,includeBrand:T(e[`include-brand`]),verifyContent:r,verifyLimit:n,js:T(e.js)?!0:void 0,rate:hi(e),refresh:T(e.refresh)});t?I(a):(K({title:`Second-page opportunities`,target:a.site,status:a.source.possiblyTruncated||a.summary.fetchFailures>0?`unknown`:`info`,summary:a.summary.verdict,metrics:[{label:`Eligible pages`,value:U(a.summary.eligiblePages)},{label:`Returned pages`,value:U(a.summary.returnedPages)},{label:`Eligible queries`,value:U(a.summary.eligibleQueries)},{label:`Impressions`,value:U(a.summary.eligibleImpressions)},{label:`Content issues`,value:U(a.summary.contentIssues)},{label:`Technical issues`,value:U(a.summary.technicalIssues)},{label:`Fetch failures`,value:U(a.summary.fetchFailures)},{label:`Brand queries`,value:a.summary.brandFiltering},{label:`Evidence`,value:a.dataStatus},{label:`Verification`,value:Ja(a)}]}),Y(`Recommended actions`,a.recommendations),Y(`Report caveats`,a.caveats),a.items.length?(q([`Query`,`Template`,`Pos`,`Impr`,`CTR`,`Queries`,`Fetch`,`Check`,`Action`],a.items.map(e=>[e.primaryQuery,e.template.label,e.position.toFixed(1),Math.round(e.impressions),e.ctr.toFixed(3),U(e.queryCount),So(e.fetchDiagnostics),Co(e.contentVerification?.classification),e.recommendation.action])),J(`Top second-page actions`,a.items.map(e=>({label:e.primaryQuery,context:`${e.template.label}, pos ${qa(e.position)}, ${U(e.impressions)} impressions`,action:e.recommendation.action}))),process.stdout.write(`${a.ledgerSummary}\n`)):(process.stdout.write(`No average-position candidates matched this report.
80
- `),process.stdout.write(`${a.ledgerSummary}\n`)))}}),vc=b({meta:{name:`audit-page`,description:`Fetch one page and audit title, metadata, content, links, and schema`},args:{url:{type:`string`,required:!0,description:`URL to audit.`},site:{type:`string`,description:`Optional GSC property for page search metrics, for example sc-domain:example.com.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering for extraction.`},refresh:{type:`boolean`,default:!1,description:`Bypass local HTTP cache.`}},run:async({args:e})=>{let t=await Hn({url:w(e.url)??``,site:w(e.site),js:T(e.js)?!0:`auto`,refresh:T(e.refresh)});A(e)?I(t):K({title:`Page audit`,target:t.url,status:t.issues.some(e=>e.severity===`high`)?`fail`:t.issues.length>0?`warning`:`pass`,summary:t.issues.length>0?`${t.issues.length} ${t.issues.length===1?`issue needs`:`issues need`} review.`:`No page issues were found by the available checks.`,metrics:[{label:`Final URL`,value:t.page.finalUrl},{label:`Title`,value:t.page.title||(t.page.title===``?`Empty`:`Missing`)},{label:`Meta description`,value:t.page.metaDescription||(t.page.metaDescription===``?`Empty`:`Missing`)},{label:`Words`,value:t.page.wordCount},{label:`Fetch`,value:So(t.fetchDiagnostics)}],diagnostics:[{title:`Issues`,items:t.issues.map(e=>({status:e.severity===`high`?`fail`:`warning`,title:`${e.code}: ${e.principle}`,explanation:e.detail}))}]})}});function yc(e){let t=w(e)??`mobile`;if(t!==`mobile`&&t!==`desktop`)throw new g(`INVALID_INPUT`,`Invalid --strategy. Use mobile or desktop.`);return t}function bc(e){return e?e.displayValue??String(e.value??`not measured`):`not measured`}function xc(e){if(!e)return`not available`;let t=e.unit===`milliseconds`?`ms`:``;return`${e.p75}${t} (${e.rating})`}const Sc=b({meta:{name:`perf`,description:`Audit lab performance and field Core Web Vitals`},subCommands:{audit:b({meta:{name:`audit`,description:`Run a Lighthouse/Core Web Vitals performance audit`},args:{url:{type:`string`,description:`URL to audit.`},strategy:{type:`string`,default:`mobile`,description:`Audit strategy: mobile or desktop.`},"lighthouse-bin":{type:`string`,description:`Custom Lighthouse binary path. Defaults to the bundled version.`},"crux-key":{type:`string`,description:`CrUX API key. Prefer SEO_CRUX_API_KEY to avoid shell history.`},raw:{type:`boolean`,default:!1,description:`Include the full Lighthouse JSON in JSON output.`},refresh:{type:`boolean`,default:!1,description:`Bypass the local performance report cache.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=A(e),n=w(e.url);if(!n)throw new g(`INVALID_INPUT`,`No URL given. Pass --url.`);let r=await en({url:n,strategy:yc(e.strategy),lighthouseBin:w(e[`lighthouse-bin`]),cruxApiKey:w(e[`crux-key`]),refresh:T(e.refresh),includeRaw:t&&T(e.raw)});t?I(r):(K({title:`Performance report`,target:r.url,status:r.dataStatus===`partial`?`unknown`:r.grade===`good`?`pass`:`warning`,summary:r.headline,metrics:[{label:`Source`,value:r.source},{label:`Strategy`,value:r.strategy},{label:`Lab score`,value:r.score===void 0?`Unknown`:`${r.score}/100`},{label:`Lab grade`,value:r.grade},{label:`Evidence`,value:r.dataStatus},{label:`FCP`,value:bc(r.metrics.firstContentfulPaint)},{label:`LCP`,value:bc(r.metrics.largestContentfulPaint)},{label:`TBT`,value:bc(r.metrics.totalBlockingTime)},{label:`CLS`,value:bc(r.metrics.cumulativeLayoutShift)},{label:`INP`,value:bc(r.metrics.interactionToNextPaint)},{label:`Server response`,value:bc(r.metrics.serverResponseTime)},{label:`Fallback fetch`,value:bc(r.metrics.fallbackFetchDuration)},{label:`Field data`,value:r.fieldData?`${r.fieldData.scope} ${r.fieldData.formFactor.toLowerCase()} (${r.fieldData.assessment.status})`:r.fieldDataStatus.status},{label:`Field LCP p75`,value:xc(r.fieldData?.metrics.largestContentfulPaint)},{label:`Field INP p75`,value:xc(r.fieldData?.metrics.interactionToNextPaint)},{label:`Field CLS p75`,value:xc(r.fieldData?.metrics.cumulativeLayoutShift)},{label:`Field period`,value:r.fieldData?.collectionPeriod?.firstDate&&r.fieldData.collectionPeriod.lastDate?`${r.fieldData.collectionPeriod.firstDate} to ${r.fieldData.collectionPeriod.lastDate}`:`Unavailable`}]}),J(`Top actions`,r.topActions.map(e=>({label:e.title,action:e.action,context:e.plainEnglish}))),Y(`Caveats`,r.caveats))}})}}),Cc=b({meta:{name:`doctor`,description:`Check local auth, scopes, config, and defaults`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Gn();A(e)?(I(t),t.ok||(process.exitCode=1)):($i(`SEO doctor`,Gi(t.checks)),process.stdout.write(`
81
- `),ea(t.checks),process.stdout.write(`\nChecked ${t.generatedAt}\n`),t.ok||(process.exitCode=1))}}),wc=e=>{let t=w(e)??`page`;if(t!==`page`&&t!==`query`&&t!==`country`&&t!==`device`)throw Error(`Invalid --dimension. Use page, query, country, or device.`);return t},Tc=b({meta:{name:`segment-impact`,description:`Compare GSC movement by page, query, device, or country`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},dimension:{type:`string`,default:`page`,description:`Segment by page, query, country, or device.`},...X([`days`,`limit`,`refresh`],{days:{description:`Recent window length from 1 to 240 days. Defaults to 28.`},limit:{description:`Maximum segment rows to print. Defaults to 25.`},refresh:{description:`Bypass local cache and fetch fresh GSC data.`}}),compare:{type:`string`,description:`Legacy 1-240 day comparison length. Must equal the current window.`},"start-date":{type:`string`,description:`Exact current-window start date in YYYY-MM-DD format.`},"end-date":{type:`string`,description:`Exact current-window end date in YYYY-MM-DD format.`},"max-rows":{type:`string`,description:`Maximum retained GSC rows per window. Defaults to 100000.`},"unmatched-limit":{type:`string`,description:`Maximum one-window evidence rows to return. Defaults to 25.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=wc(e.dimension),r=O(e.days,`--days`),i=O(e.compare,`--compare`),a=O(e.limit,`--limit`),o=O(e[`unmatched-limit`],`--unmatched-limit`),s=O(e[`max-rows`],`--max-rows`),c=j(w(e.site)),l=await Ze({site:c,dimension:n,days:r,compareDays:i,startDate:w(e[`start-date`]),endDate:w(e[`end-date`]),limit:a,unmatchedLimit:o,maxRows:s,refresh:T(e.refresh)});t?I(l):(K({title:`Segment impact`,target:l.site,status:l.dataStatus===`complete`?`info`:`unknown`,summary:l.summary.verdict,metrics:[{label:`Dimension`,value:l.dimension},{label:`Evidence`,value:l.dataStatus},{label:`Before`,value:`${l.before.startDate} to ${l.before.endDate}`},{label:`After`,value:`${l.after.startDate} to ${l.after.endDate}`}]}),R([`Segment`,`Clicks before`,`Clicks after`,`Delta`,`Pos delta`],l.items.map(e=>[e.key,e.beforeClicks,e.afterClicks,e.clickDelta,e.positionDelta??`-`])),l.unmatchedSegments.length&&(process.stdout.write(`
79
+ `))}}),gc=b({meta:{name:`query-cluster`,description:`Cluster GSC queries into repeated demand themes`},args:{site:{type:`string`},scope:{type:`string`},...X([`includeBrand`,`minImpressions`,`limit`],{includeBrand:{description:`Include branded queries in query clustering.`},minImpressions:{description:`Minimum impressions per query. Defaults to 25.`},limit:{description:`Maximum clusters to return. Defaults to 25; max 100.`}}),json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=j(e),n=M(T(e.site)),r=await Qt({site:n,scope:T(e.scope),includeBrand:E(e[`include-brand`]),minImpressions:O(e[`min-impressions`]),limit:O(e.limit)});t?L(r):(K({title:`Query clusters`,target:r.site,status:`info`,summary:r.summary.verdict,metrics:[{label:`Scope`,value:r.scope??`All pages`},{label:`Clusters`,value:U(r.summary.clusters)},{label:`Queries`,value:U(r.summary.queries)},{label:`Impressions`,value:U(r.summary.impressions)},{label:`Clicks`,value:U(r.summary.clicks)},{label:`Minimum impressions`,value:U(r.summary.minImpressions)},{label:`Result limit`,value:U(r.summary.limit)},{label:`High opportunity`,value:U(r.summary.highOpportunityClusters)},{label:`Brand queries`,value:r.summary.brandFiltering}]}),Y(`Why this matters`,[`Clusters show repeated demand themes, so they are better inputs for page sections, hubs, and templates than one-off query exports.`,`Prioritise clusters with high impressions and low clicks; they usually reveal unclear intent coverage or weak SERP framing.`]),Y(`Recommended actions`,r.recommendations),Y(`Report caveats`,r.caveats),q([`Cluster`,`Intent`,`Queries`,`Impr`,`CTR`,`Expected`,`Lift`,`Action`],r.clusters.map(e=>[G(e.label,32),e.intent,e.queries.length,U(e.totals?.impressions??0),W(e.totals?.ctr??0),W(e.benchmark?.expectedCtr??0),e.estimatedClickLift===void 0?`-`:U(e.estimatedClickLift),G(e.recommendation??``,72)])),J(`Top cluster actions`,r.clusters.map(e=>({label:e.label,context:`${U(e.totals?.impressions??0)} impressions, ${e.estimatedClickLift===void 0?`ranking opportunity`:`${U(e.estimatedClickLift)} estimated click lift`}`,action:e.recommendation??``}))))}}),_c=b({meta:{name:`quick-wins`,description:`Find high-ranking low-CTR query/page opportunities`},args:{site:{type:`string`},...X([`days`,`limit`,`includeBrand`,`minImpressions`,`verifyContent`,`verifyLimit`,`js`,`fetchConcurrency`,`fetchIntervalCap`,`fetchIntervalMs`,`refresh`]),"brand-terms":{type:`string`,description:`Comma-separated brand terms to exclude.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=j(e),n=O(e[`verify-limit`]),r=E(e[`verify-content`])===!0||n!==void 0,i=M(T(e.site)),a=await et({site:i,days:O(e.days),limit:O(e.limit),minImpressions:O(e[`min-impressions`]),brandTerms:[...A(e[`brand-terms`])??[]],includeBrand:E(e[`include-brand`]),verifyContent:r,verifyLimit:n,js:E(e.js)?!0:void 0,rate:hi(e),refresh:E(e.refresh)});t?L(a):(K({title:`Quick wins`,target:a.site,status:a.source.possiblyTruncated?`unknown`:`info`,summary:a.summary.verdict,metrics:[{label:`Eligible rows`,value:U(a.summary.eligibleRows)},{label:`Returned rows`,value:U(a.summary.returnedRows)},{label:`Query clusters`,value:U(a.summary.repeatedQueryGroups)},{label:`Template patterns`,value:U(a.summary.templatePatterns)},{label:`CTR click shortfall`,value:U(a.summary.returnedEstimatedCtrClickShortfall)},{label:`Brand queries`,value:a.summary.brandFiltering},{label:`Evidence`,value:a.dataStatus},{label:`Verification`,value:Ya(a)}]}),Y(`Why this matters`,[`These rows have GSC average positions from 4 to 10 and observed CTR below a site-peer or versioned fallback target.`,`Page evidence is needed before treating a CTR shortfall as a title, content, or technical finding.`]),Y(`Recommended actions`,a.recommendations),Y(`Report caveats`,a.caveats),a.items.length&&(a.templateRecommendations.length&&(q([`Template`,`URLs`,`Rows`,`Shortfall`,`Impr`,`Action`],a.templateRecommendations.map(e=>[G(e.templateLabel,34),U(e.urlCount),U(e.rowCount),U(e.totalEstimatedCtrClickShortfall),U(e.totalImpressions),G(e.action,72)])),J(`Top template actions`,a.templateRecommendations.map(e=>({label:e.templateLabel,context:`${U(e.urlCount)} URLs, ${U(e.totalEstimatedCtrClickShortfall)} heuristic CTR click shortfall`,action:`${e.action} ${e.evidence}`})))),a.groups.length&&(q([`Cluster`,`URLs`,`Rows`,`Shortfall`,`Impr`,`Sample URL`,`Action`],a.groups.map(e=>[G(e.label,44),U(e.urlCount),U(e.rowCount),U(e.totalEstimatedCtrClickShortfall),U(e.totalImpressions),G(e.sampleUrls[0]??`-`,46),G(e.recommendation,72)])),J(`Top cluster actions`,a.groups.map(e=>({label:e.label,context:`${U(e.urlCount)} URLs, ${U(e.totalEstimatedCtrClickShortfall)} heuristic CTR click shortfall`,action:e.recommendation})))),q([`Query`,`Template`,`URL`,`Pos`,`Impr`,`CTR / target`,`Shortfall`,`Fetch`,`Check`,`Action`],a.items.map(e=>[G(e.query,36),G(e.template.label,24),G(e.url,48),Ja(e.position),U(e.impressions),`${W(e.ctr)} / ${W(e.targetCtr)}`,U(e.estimatedCtrClickShortfall),Co(e.contentVerification?.fetchDiagnostics),wo(e.contentVerification?.classification),G(e.recommendation.action,64)])),J(`Top opportunity actions`,a.items.map(e=>({label:e.query,context:`${e.template.label}, pos ${Ja(e.position)}, ${U(e.impressions)} impressions`,action:e.recommendation.action})))))}}),vc=b({meta:{name:`second-page`,description:`Find URLs averaging positions above 10 through 20 in GSC`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},...X([`range`,`minImpressions`,`limit`,`includeBrand`,`verifyContent`,`verifyLimit`,`js`,`fetchConcurrency`,`fetchIntervalCap`,`fetchIntervalMs`,`refresh`]),json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=O(e[`verify-limit`]),r=E(e[`verify-content`])===!0||n!==void 0,i=M(T(e.site)),o=await a({site:i,range:O(e.days),minImpressions:O(e[`min-impressions`]),limit:T(e.limit)?Number(T(e.limit)):10,includeBrand:E(e[`include-brand`]),verifyContent:r,verifyLimit:n,js:E(e.js)?!0:void 0,rate:hi(e),refresh:E(e.refresh)});t?L(o):(K({title:`Second-page opportunities`,target:o.site,status:o.source.possiblyTruncated||o.summary.fetchFailures>0?`unknown`:`info`,summary:o.summary.verdict,metrics:[{label:`Eligible pages`,value:U(o.summary.eligiblePages)},{label:`Returned pages`,value:U(o.summary.returnedPages)},{label:`Eligible queries`,value:U(o.summary.eligibleQueries)},{label:`Impressions`,value:U(o.summary.eligibleImpressions)},{label:`Content issues`,value:U(o.summary.contentIssues)},{label:`Technical issues`,value:U(o.summary.technicalIssues)},{label:`Fetch failures`,value:U(o.summary.fetchFailures)},{label:`Brand queries`,value:o.summary.brandFiltering},{label:`Evidence`,value:o.dataStatus},{label:`Verification`,value:Ya(o)}]}),Y(`Recommended actions`,o.recommendations),Y(`Report caveats`,o.caveats),o.items.length?(q([`Query`,`Template`,`Pos`,`Impr`,`CTR`,`Queries`,`Fetch`,`Check`,`Action`],o.items.map(e=>[e.primaryQuery,e.template.label,e.position.toFixed(1),Math.round(e.impressions),e.ctr.toFixed(3),U(e.queryCount),Co(e.fetchDiagnostics),wo(e.contentVerification?.classification),e.recommendation.action])),J(`Top second-page actions`,o.items.map(e=>({label:e.primaryQuery,context:`${e.template.label}, pos ${Ja(e.position)}, ${U(e.impressions)} impressions`,action:e.recommendation.action}))),process.stdout.write(`${o.ledgerSummary}\n`)):(process.stdout.write(`No average-position candidates matched this report.
80
+ `),process.stdout.write(`${o.ledgerSummary}\n`)))}}),yc=b({meta:{name:`audit-page`,description:`Fetch one page and audit title, metadata, content, links, and schema`},args:{url:{type:`string`,required:!0,description:`URL to audit.`},site:{type:`string`,description:`Optional GSC property for page search metrics, for example sc-domain:example.com.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering for extraction.`},refresh:{type:`boolean`,default:!1,description:`Bypass local HTTP cache.`}},run:async({args:e})=>{let t=await Qn({url:T(e.url)??``,site:T(e.site),js:E(e.js)?!0:`auto`,refresh:E(e.refresh)});j(e)?L(t):K({title:`Page audit`,target:t.url,status:t.issues.some(e=>e.severity===`high`)?`fail`:t.issues.length>0?`warning`:`pass`,summary:t.issues.length>0?`${t.issues.length} ${t.issues.length===1?`issue needs`:`issues need`} review.`:`No page issues were found by the available checks.`,metrics:[{label:`Final URL`,value:t.page.finalUrl},{label:`Title`,value:t.page.title||(t.page.title===``?`Empty`:`Missing`)},{label:`Meta description`,value:t.page.metaDescription||(t.page.metaDescription===``?`Empty`:`Missing`)},{label:`Words`,value:t.page.wordCount},{label:`Fetch`,value:Co(t.fetchDiagnostics)}],diagnostics:[{title:`Issues`,items:t.issues.map(e=>({status:e.severity===`high`?`fail`:`warning`,title:`${e.code}: ${e.principle}`,explanation:e.detail}))}]})}});function bc(e){let t=T(e)??`mobile`;if(t!==`mobile`&&t!==`desktop`)throw new g(`INVALID_INPUT`,`Invalid --strategy. Use mobile or desktop.`);return t}function xc(e){return e?e.displayValue??String(e.value??`not measured`):`not measured`}function Sc(e){if(!e)return`not available`;let t=e.unit===`milliseconds`?`ms`:``;return`${e.p75}${t} (${e.rating})`}const Cc=b({meta:{name:`perf`,description:`Audit lab performance and field Core Web Vitals`},subCommands:{audit:b({meta:{name:`audit`,description:`Run a Lighthouse/Core Web Vitals performance audit`},args:{url:{type:`string`,description:`URL to audit.`},strategy:{type:`string`,default:`mobile`,description:`Audit strategy: mobile or desktop.`},"lighthouse-bin":{type:`string`,description:`Custom Lighthouse binary path. Defaults to the bundled version.`},"crux-key":{type:`string`,description:`CrUX API key. Prefer SEO_CRUX_API_KEY to avoid shell history.`},raw:{type:`boolean`,default:!1,description:`Include the full Lighthouse JSON in JSON output.`},refresh:{type:`boolean`,default:!1,description:`Bypass the local performance report cache.`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=j(e),n=T(e.url);if(!n)throw new g(`INVALID_INPUT`,`No URL given. Pass --url.`);let r=await Ee({url:n,strategy:bc(e.strategy),lighthouseBin:T(e[`lighthouse-bin`]),cruxApiKey:T(e[`crux-key`]),refresh:E(e.refresh),includeRaw:t&&E(e.raw)});t?L(r):(K({title:`Performance report`,target:r.url,status:r.dataStatus===`partial`?`unknown`:r.grade===`good`?`pass`:`warning`,summary:r.headline,metrics:[{label:`Source`,value:r.source},{label:`Strategy`,value:r.strategy},{label:`Lab score`,value:r.score===void 0?`Unknown`:`${r.score}/100`},{label:`Lab grade`,value:r.grade},{label:`Evidence`,value:r.dataStatus},{label:`FCP`,value:xc(r.metrics.firstContentfulPaint)},{label:`LCP`,value:xc(r.metrics.largestContentfulPaint)},{label:`TBT`,value:xc(r.metrics.totalBlockingTime)},{label:`CLS`,value:xc(r.metrics.cumulativeLayoutShift)},{label:`INP`,value:xc(r.metrics.interactionToNextPaint)},{label:`Server response`,value:xc(r.metrics.serverResponseTime)},{label:`Fallback fetch`,value:xc(r.metrics.fallbackFetchDuration)},{label:`Field data`,value:r.fieldData?`${r.fieldData.scope} ${r.fieldData.formFactor.toLowerCase()} (${r.fieldData.assessment.status})`:r.fieldDataStatus.status},{label:`Field LCP p75`,value:Sc(r.fieldData?.metrics.largestContentfulPaint)},{label:`Field INP p75`,value:Sc(r.fieldData?.metrics.interactionToNextPaint)},{label:`Field CLS p75`,value:Sc(r.fieldData?.metrics.cumulativeLayoutShift)},{label:`Field period`,value:r.fieldData?.collectionPeriod?.firstDate&&r.fieldData.collectionPeriod.lastDate?`${r.fieldData.collectionPeriod.firstDate} to ${r.fieldData.collectionPeriod.lastDate}`:`Unavailable`}]}),J(`Top actions`,r.topActions.map(e=>({label:e.title,action:e.action,context:e.plainEnglish}))),Y(`Caveats`,r.caveats))}})}}),wc=b({meta:{name:`doctor`,description:`Check local auth, scopes, config, and defaults`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Un();j(e)?(L(t),t.ok||(process.exitCode=1)):(ea(`SEO doctor`,Gi(t.checks)),process.stdout.write(`
81
+ `),ta(t.checks),process.stdout.write(`\nChecked ${t.generatedAt}\n`),t.ok||(process.exitCode=1))}}),Tc=e=>{let t=T(e)??`page`;if(t!==`page`&&t!==`query`&&t!==`country`&&t!==`device`)throw Error(`Invalid --dimension. Use page, query, country, or device.`);return t},Ec=b({meta:{name:`segment-impact`,description:`Compare GSC movement by page, query, device, or country`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},dimension:{type:`string`,default:`page`,description:`Segment by page, query, country, or device.`},...X([`days`,`limit`,`refresh`],{days:{description:`Recent window length from 1 to 240 days. Defaults to 28.`},limit:{description:`Maximum segment rows to print. Defaults to 25.`},refresh:{description:`Bypass local cache and fetch fresh GSC data.`}}),compare:{type:`string`,description:`Legacy 1-240 day comparison length. Must equal the current window.`},"start-date":{type:`string`,description:`Exact current-window start date in YYYY-MM-DD format.`},"end-date":{type:`string`,description:`Exact current-window end date in YYYY-MM-DD format.`},"max-rows":{type:`string`,description:`Maximum retained GSC rows per window. Defaults to 100000.`},"unmatched-limit":{type:`string`,description:`Maximum one-window evidence rows to return. Defaults to 25.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=Tc(e.dimension),r=k(e.days,`--days`),i=k(e.compare,`--compare`),a=k(e.limit,`--limit`),o=k(e[`unmatched-limit`],`--unmatched-limit`),s=k(e[`max-rows`],`--max-rows`),c=M(T(e.site)),l=await rt({site:c,dimension:n,days:r,compareDays:i,startDate:T(e[`start-date`]),endDate:T(e[`end-date`]),limit:a,unmatchedLimit:o,maxRows:s,refresh:E(e.refresh)});t?L(l):(K({title:`Segment impact`,target:l.site,status:l.dataStatus===`complete`?`info`:`unknown`,summary:l.summary.verdict,metrics:[{label:`Dimension`,value:l.dimension},{label:`Evidence`,value:l.dataStatus},{label:`Before`,value:`${l.before.startDate} to ${l.before.endDate}`},{label:`After`,value:`${l.after.startDate} to ${l.after.endDate}`}]}),z([`Segment`,`Clicks before`,`Clicks after`,`Delta`,`Pos delta`],l.items.map(e=>[e.key,e.beforeClicks,e.afterClicks,e.clickDelta,e.positionDelta??`-`])),l.unmatchedSegments.length&&(process.stdout.write(`
82
82
  One-window evidence (not treated as zero)
83
- `),R([`Segment`,`Retained in`,`Clicks`,`Impressions`],l.unmatchedSegments.map(e=>[e.key,e.retainedIn,e.clicks,e.impressions]))),Y(`Warnings`,l.warnings))}});function Ec(e){return`${U(e)} ${e===1?`row`:`rows`}`}const Dc=b({meta:{name:`striking-distance`,description:`Find query/page rows averaging positions above 10 through 20`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},...X([`days`,`minImpressions`,`limit`,`includeBrand`,`verifyContent`,`verifyLimit`,`js`,`fetchConcurrency`,`fetchIntervalCap`,`fetchIntervalMs`,`refresh`]),json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=D(e[`verify-limit`]),r=T(e[`verify-content`])===!0||n!==void 0,i=j(w(e.site)),a=await nr({site:i,days:D(e.days),minImpressions:D(e[`min-impressions`]),limit:D(e.limit),includeBrand:T(e[`include-brand`]),verifyContent:r,verifyLimit:n,js:T(e.js)?!0:void 0,rate:hi(e),refresh:T(e.refresh)});t?I(a):(K({title:`Striking-distance opportunities`,target:a.site,status:a.source.possiblyTruncated?`unknown`:`info`,summary:a.summary.verdict,metrics:[{label:`Eligible rows`,value:U(a.summary.eligibleRows)},{label:`Returned rows`,value:U(a.summary.returnedRows)},{label:`Template groups`,value:U(a.summary.groups)},{label:`Impressions`,value:U(a.summary.eligibleImpressions)},{label:`Brand queries`,value:a.summary.brandFiltering},{label:`Verification`,value:a.verification.requested?`Requested`:`Off`},{label:`Evidence`,value:a.dataStatus}]}),Y(`Why this matters`,[`These rows have a GSC average position above 10 and at most 20. Treat them as candidates for investigation, not guaranteed page-two rankings.`,`Groups use all eligible rows. A shared-template label is only a candidate until recurring evidence is verified across distinct URLs.`]),Y(`Recommended actions`,a.recommendations),Y(`Report caveats`,a.caveats),a.groups.length&&(q([`Group`,`Rows`,`URLs`,`Impr`,`Avg pos`,`Scope`,`Action`],a.groups.map(e=>[G(e.label,36),U(e.rowCount),U(e.uniqueUrls),U(e.totalImpressions),qa(e.impressionWeightedPosition),e.actionScope,G(e.recommendation.action,72)])),J(`Top striking-distance group actions`,a.groups.map(e=>({label:e.label,context:`${Ec(e.rowCount)}, ${U(e.totalImpressions)} impressions`,action:e.recommendation.action})))),a.items.length?(q([`Query`,`Template`,`URL`,`Impr`,`CTR`,`Pos`,`Score`,`Fetch`,`Check`,`Action`],a.items.map(e=>[G(e.query,36),G(e.template.label,24),G(e.url,48),U(e.impressions),W(e.ctr),qa(e.position),e.priority.score,So(e.contentVerification?.fetchDiagnostics),Co(e.contentVerification?.classification),G(e.recommendation.action,72)])),J(`Top striking-distance actions`,a.items.map(e=>({label:e.query,context:`${e.template.label}, pos ${qa(e.position)}, ${U(e.impressions)} impressions`,action:e.recommendation.action})))):process.stdout.write(`No striking-distance opportunities matched these filters.
84
- `))}});function Oc(e){return e===`keychain`?`system keychain`:e===`file`?`private local file`:e??`missing`}function kc(e){return e===null?`unavailable`:`$${(e/1e6).toFixed(2)}`}function Ac(e){return e.limit===null?e.used===null?`not reported`:`${e.used.toLocaleString(`en-US`)} used; no key limit`:`${(e.remaining??0).toLocaleString(`en-US`)} of ${e.limit.toLocaleString(`en-US`)} remaining`}const jc=b({meta:{name:`connect`,description:`Validate and save an Ahrefs API v3 key`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{if(!F({json:A(e)}))throw new g(`AUTH_REQUIRED`,`Run \`seo providers ahrefs connect\` in a terminal. Agents and CI can set ${Qe}.`);Vr(`Connect Ahrefs`),x(`Paste an API v3 key from Ahrefs Account settings. The validation check and Domain Rating are free; other research can consume API units.`,`API key`);let t=B(await Gr({message:`Ahrefs API v3 key`,validate:e=>e?.trim()?void 0:`API key is required`})),n=await new bn({apiKey:t}).limitsAndUsage(),r=await tt(t);x(`${n.subscription} subscription. API key units: ${Ac(n.apiKeyUnits)}.`,`Connection verified`),S(`Saved in the ${Oc(r)}. Run seo providers ahrefs status --check to verify it again.`)}}),Mc=b({meta:{name:`status`,description:`Show the local Ahrefs connection`},args:{check:{type:`boolean`,default:!1,description:`Verify the API key with the free account endpoint.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Ye(),n=!!e.check,r=n&&t?await new bn().limitsAndUsage():void 0,i={connected:!!t,apiVersion:t?3:null,credentialSource:t?.source,liveCheck:r?{status:`passed`,subscription:r.subscription,apiKeyExpiresAt:r.apiKeyExpiresAt,usageResetsAt:r.usageResetsAt,apiKeyUnits:r.apiKeyUnits,workspaceUnits:r.workspaceUnits,observedAt:r.observedAt,requestCostUnits:r.requestCostUnits}:{status:n?`unavailable`:`not-requested`}};A(e)?I(i):L([[`Connected`,i.connected?`yes`:`no`],[`API version`,i.apiVersion?`Version 3`:`not connected`],[`Credential`,Oc(i.credentialSource)],[`Live check`,i.liveCheck.status===`passed`?`passed at ${i.liveCheck.observedAt}`:i.liveCheck.status===`unavailable`?`not available without credentials`:`not requested; pass --check to verify`],...r?[[`Subscription`,r.subscription],[`API key units`,Ac(r.apiKeyUnits)],[`Workspace units`,Ac(r.workspaceUnits)],[`Usage resets`,r.usageResetsAt],[`Key expires`,r.apiKeyExpiresAt]]:[]])}}),Nc=b({meta:{name:`disconnect`,description:`Remove the saved Ahrefs API v3 key`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{await le();let t=!!process.env[Qe]?.trim(),n={savedCredentialRemoved:!0,environmentCredential:t?`active`:`missing`,note:t?`The environment variable was not changed. Clear ${Qe} to fully disconnect.`:`Ahrefs is disconnected.`};A(e)?I(n):process.stdout.write(`${n.note}\n`)}}),Pc=b({meta:{name:`ahrefs`,description:`Connect Ahrefs for optional search and link data`},subCommands:{connect:jc,status:Mc,disconnect:Nc}}),Fc="Run `seo providers bing connect` with a new API key.",Ic=b({meta:{name:`connect`,description:`Check and save a Bing Webmaster Tools API key`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{if(!F({json:A(e)}))throw new g(`AUTH_REQUIRED`,`Run \`seo providers bing connect\` in a terminal. Agents and CI can set ${xe}.`);Vr(`Connect Bing Webmaster Tools`),x(`In Bing Webmaster Tools, open Settings, then API access, and copy the API key. Bing data is free.`,`API key`);let t=B(await Gr({message:`Bing Webmaster API key`,validate:e=>e?.trim()?void 0:`API key is required`})),n=await Un(ht({apiKey:t},{signIn:Fc})),r=await o(t);x(`${n.filter(e=>e.verified).length} verified sites.`,`Connection verified`),S(`Saved in the ${Oc(r)}.`)}}),Lc=b({meta:{name:`status`,description:`Show the Bing Webmaster connection`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await er(),n=t?await Un(ht({apiKey:t.apiKey},{signIn:Fc})):null,r={connected:!!t,credentialSource:t?.source??null,sites:n?.filter(e=>e.verified).map(e=>e.url)??[]};A(e)?I(r):process.stdout.write(t?`Connected (${Oc(t.source)}). Verified sites: ${r.sites.join(`, `)||`none`}.\n`:"Not connected. Run `seo providers bing connect`.\n")}}),Rc=b({meta:{name:`disconnect`,description:`Remove the saved Bing Webmaster API key`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{await ze();let t=!!process.env[xe]?.trim(),n=t?`The saved key is removed. Clear ${xe} to fully disconnect.`:`Bing is disconnected.`;A(e)?I({removed:!0,environment:t}):process.stdout.write(`${n}\n`)}}),zc=b({meta:{name:`bing`,description:`Connect Bing Webmaster Tools for Bing search data`},subCommands:{connect:Ic,status:Lc,disconnect:Rc}}),Bc=b({meta:{name:`connect`,description:`Validate and save DataForSEO API credentials`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{if(!F({json:A(e)}))throw new g(`AUTH_REQUIRED`,`Run \`seo providers dataforseo connect\` in a terminal. Agents and CI can set ${we} and ${De}.`);Vr(`Connect DataForSEO`),x(`Use the API login and API password from DataForSEO API Access. The validation call is free; later research reports can make charged requests.`,`API credentials`);let t=B(await qr({message:`DataForSEO API login`,validate:e=>e?.trim()?void 0:`API login is required`})),n=B(await Gr({message:`DataForSEO API password`,validate:e=>e?void 0:`API password is required`})),r={login:t.trim(),password:n},i=await new It({credentials:()=>r}).userData(),a=await fe(r);x(`${i.login} is connected with an account balance of ${kc(i.balanceMicros)}.`,`Connection verified`),S(`Saved in the ${Oc(a)}. Run seo providers dataforseo status --check to verify it again.`)}}),Vc=b({meta:{name:`status`,description:`Show the local DataForSEO connection`},args:{check:{type:`boolean`,default:!1,description:`Verify credentials with the free account endpoint.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await re(),n=!!e.check,r=n&&t?await new It().userData():void 0,i={connected:!!t,credentialSource:t?.source,migratedLegacyCredentials:t?.migrated??!1,liveCheck:r?{status:`passed`,account:r.login,timezone:r.timezone,balanceMicros:r.balanceMicros,observedAt:r.observedAt,requestCostMicros:r.requestCostMicros}:{status:n?`unavailable`:`not-requested`}};A(e)?I(i):L([[`Connected`,i.connected?`yes`:`no`],[`Credential`,Oc(i.credentialSource)],[`Live check`,i.liveCheck.status===`passed`?`passed at ${i.liveCheck.observedAt}`:i.liveCheck.status===`unavailable`?`not available without credentials`:`not requested; pass --check to verify`],...r?[[`Account`,r.login],[`Balance`,kc(r.balanceMicros)]]:[]])}}),Hc=b({meta:{name:`disconnect`,description:`Remove saved DataForSEO credentials`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{await Ne();let t=!!process.env[we]?.trim(),n=!!process.env[De],r={savedCredentialsRemoved:!0,environmentCredentials:t&&n?`active`:t||n?`partial`:`missing`,note:t||n?`Environment variables were not changed. Clear ${we} and ${De} to fully disconnect.`:`DataForSEO is disconnected.`};A(e)?I(r):process.stdout.write(`${r.note}\n`)}}),Uc=b({meta:{name:`dataforseo`,description:`Connect DataForSEO for optional search data`},subCommands:{connect:Bc,status:Vc,disconnect:Hc}});function Wc(e){if(!e)return{};let t;try{t=JSON.parse(e)}catch{throw new g(`INVALID_INPUT`,`--account must be a JSON object.`)}if(!t||Array.isArray(t)||typeof t!=`object`)throw new g(`INVALID_INPUT`,`--account must be a JSON object.`);let n=Object.entries(t);if(n.length>20||n.some(([e,t])=>!e||e.length>64||typeof t!=`string`||t.length>4096))throw new g(`INVALID_INPUT`,`--account must contain at most 20 bounded string fields.`);return Object.fromEntries(n.map(([e,t])=>[e,t.trim()]))}function Gc(e){if(!e)return{};let t;try{t=JSON.parse(e)}catch{throw new g(`INVALID_INPUT`,`--params must be a JSON object.`)}if(!t||Array.isArray(t)||typeof t!=`object`)throw new g(`INVALID_INPUT`,`--params must be a JSON object.`);let n=Object.entries(t);if(n.length>100||n.some(([e])=>!/^[A-Za-z][A-Za-z0-9-]{0,63}$/u.test(e))||Buffer.byteLength(e)>65536)throw new g(`INVALID_INPUT`,`--params must be a JSON object smaller than 64 KiB.`);return Object.fromEntries(n)}async function Kc(e){let t=await ot(),n=t.registry.get(e);if(n)return n;let r=t.failures.find(t=>t.id===e);throw r?new g(`INVALID_INPUT`,`Provider ${e} could not load: ${r.message}`):new g(`INVALID_INPUT`,`Provider ${e} is not installed.`)}function qc(e,t){let n=new Set(e.connection.fields.filter(e=>e.kind===`account`).map(e=>e.id)),r=Object.keys(t).find(e=>!n.has(e));if(r)throw new g(`INVALID_INPUT`,`${r} is not an account field for ${e.displayName}.`);let i=e.connection.normalizeAccount?e.connection.normalizeAccount(t):{...t};if(Object.keys(i).find(e=>!n.has(e)))throw new g(`PROVIDER_UNAVAILABLE`,`${e.displayName} returned an unknown account field.`);for(let t of e.connection.fields.filter(e=>e.kind===`account`&&e.required!==!1))if(!i[t.id]?.trim())throw new g(`INVALID_INPUT`,`${t.label} is required.`);return i}async function Jc(){return Vn()}const Yc={list:b({meta:{name:`list`,description:`List installed provider packages`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Jc();A(e)?I(t):t.installed.length>0?R([`Installed provider`,`Package`,`Version`,`Status`],t.installed.map(e=>[e.id,e.package,e.version,e.loadStatus])):process.stdout.write(`No provider extensions are installed.
85
- `)}}),describe:b({meta:{name:`describe`,description:`Describe one installed provider for an agent`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.id)??``,n=await pt(t);A(e)?I(n):L([[`Provider`,n.displayName],[`Package`,`${n.package}@${n.version}`],[`Description`,n.description],[`Capabilities`,n.capabilities.map(e=>e.id).join(`, `)||`none`],[`Actions`,n.actions.map(e=>e.id).join(`, `)||`none`]])}}),install:b({meta:{name:`install`,description:`Install one provider package from npm`},args:{package:{type:`positional`,required:!0,description:`npm package name with an optional exact version.`},yes:{type:`boolean`,default:!1,description:`Approve third-party code installation without prompting.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.package)??``,n=A(e),r=T(e.yes),i=F({json:n});if(!r&&!i)throw new g(`INVALID_INPUT`,`Provider installation runs third-party code. Review the package, then pass --yes to approve it in JSON or CI mode.`);let a;try{a=await ln(t)}catch(e){throw new g(`INVALID_INPUT`,e instanceof Error?e.message:String(e))}if(!r&&(Vr(`Install provider package`),x([`${a.package}@${a.version}`,`Publisher: ${a.publisher??`not supplied by npm`}`,`Repository: ${a.repository??`not supplied by npm`}`,`Integrity: ${a.integrity}`].join(`
86
- `),`Package`),x(`This package can read files, use the network, and run with the same local permissions as seo. Installing it is not a security review or endorsement.`,`Third-party code`),!B(await Br({message:`Install ${a.package}@${a.version}?`,initialValue:!1})))){S(`Provider was not installed.`);return}let o;try{o=await Wt(a)}catch(e){throw new g(`PROVIDER_UNAVAILABLE`,e instanceof Error?e.message:String(e))}n?I({installed:o,package:a}):S(`Installed provider ${o.id} from ${o.package}@${o.version}.`)}}),remove:b({meta:{name:`remove`,description:`Remove one installed provider extension`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},yes:{type:`boolean`,default:!1,description:`Remove the package without prompting.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.id)??``,n=A(e),r=T(e.yes),i=F({json:n});if(!r&&!i)throw new g(`INVALID_INPUT`,`Pass --yes to remove a provider extension in JSON or CI mode.`);if(!r&&!B(await Br({message:`Remove provider ${t}?`,initialValue:!1})))return;let a=await Bt(t);if(!a)throw new g(`INVALID_INPUT`,`Provider ${t} is not installed.`);n?I({removed:a}):process.stdout.write(`Removed ${t}.\n`)}}),connect:b({meta:{name:`connect`,description:`Validate and save one installed provider connection`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},account:{type:`string`,description:`Account fields as JSON. Secret fields must use their environment variables.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.id)??``,r=A(e),i=F({json:r}),a=await Kc(t),o={...xt(t),...Wc(w(e.account))},s={};if(i){Vr(`Connect ${a.displayName}`),a.connection.verificationNotice&&x(a.connection.verificationNotice,`Connection check`);for(let e of a.connection.fields){if(e.kind===`account`){let t=B(await qr({message:e.label,placeholder:e.description,initialValue:o[e.id],validate:t=>e.required!==!1&&!t?.trim()?`${e.label} is required`:void 0}));t?.trim()&&(o[e.id]=t.trim());continue}if(e.envVar&&process.env[e.envVar]?.trim())continue;let t=B(await Gr({message:e.label,validate:t=>e.required!==!1&&!t?.trim()?`${e.label} is required`:void 0}));t?.trim()&&(s[e.id]=t.trim())}}o=qc(a,o);let c={...s};Object.keys(c).length===0&&(c=await Re({providerId:t,account:o,fields:a.connection.fields})),await lt({providerId:t,account:o,credentials:c}),Object.keys(s).length>0&&await n({providerId:t,account:o,credentials:s}),Jn(t,o);let l={provider:t,connected:!0,account:o,capabilities:(a.capabilities??[]).map(e=>e.id),actions:(a.actions??[]).map(e=>e.id)};r?I(l):S(`Connected ${a.displayName}. Run seo providers describe ${t} to see what it can do.`)}}),status:b({meta:{name:`status`,description:`Show one installed provider connection`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},check:{type:`boolean`,default:!1,description:`Run the provider connection check.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.id)??``,n=await Kc(t),r=xt(t),i;try{i=await Re({providerId:t,account:r,fields:n.connection.fields})}catch{i=void 0}let a=n.connection.fields.filter(e=>e.kind===`account`&&e.required!==!1).every(e=>!!r[e.id]?.trim())&&i!==void 0,o=T(e.check);if(o&&!a)throw new g(`AUTH_REQUIRED`,`Provider ${t} is not connected. Run \`seo providers connect ${t}\`.`);o&&i&&await lt({providerId:t,account:r,credentials:i});let s={provider:t,installed:!0,connected:a,account:r,capabilities:(n.capabilities??[]).map(e=>e.id),actions:(n.actions??[]).map(e=>e.id),check:o?`passed`:`not-requested`};A(e)?I(s):R([`Provider`,`Connected`,`Capabilities`,`Check`],[[n.displayName,a?`yes`:`no`,s.capabilities.join(`, `),s.check]])}}),disconnect:b({meta:{name:`disconnect`,description:`Remove one saved provider connection`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.id)??``;await Kc(t);let n=xt(t);await ft({providerId:t,account:n}),Qn(t);let r={provider:t,disconnected:!0,note:`Environment variables were not changed.`};A(e)?I(r):process.stdout.write(`Disconnected ${t}. ${r.note}\n`)}}),run:b({meta:{name:`run`,description:`Run one action supplied by an installed provider`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},action:{type:`positional`,required:!0,description:`Provider action id.`},account:{type:`string`,description:`Account fields as JSON. Defaults to the saved connection.`},params:{type:`string`,description:`Action parameters as a JSON object.`},refresh:{type:`boolean`,default:!1,description:`Bypass the local provider cache.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.id)??``,n=w(e.action)??``;await Kc(t);let r={...xt(t),...Wc(w(e.account))};I(await He({providerId:t,actionId:n,account:r,params:Gc(w(e.params)),refresh:T(e.refresh)}))}}),doctor:b({meta:{name:`doctor`,description:`Validate installed provider packages and entry points`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Jc(),n={ok:t.installed.every(e=>e.loadStatus===`ready`),installed:t.installed};A(e)?I(n):n.installed.length===0?process.stdout.write(`No provider extensions are installed.
87
- `):R([`Provider`,`Package`,`Version`,`Status`],n.installed.map(e=>[e.id,e.package,e.version,e.error??e.loadStatus]))}})},Xc=b({meta:{name:`providers`,description:`Connect optional data providers`},subCommands:{...Yc,ahrefs:Pc,bing:zc,dataforseo:Uc}}),Zc={"ai-search":`AI search`,crawl:`Crawling and technical checks`,diagnosis:`Diagnosis`,monitoring:`Monitoring`,opportunities:`Opportunities`,reporting:`Reporting`,setup:`Setup`,workflows:`Workflows`};function Qc(e){let t=w(e);if(t){if(!Nn.includes(t))throw new g(`INVALID_INPUT`,`Unknown report category: ${t}. Choose ${Nn.join(`, `)}.`);return t}}async function $c(e,t){let n=w(e),r=w(t);if(n&&r)throw new g(`INVALID_INPUT`,`Use either --params or --params-file, not both.`);if(!n&&!r)return{};let i=n??await y(r??``,`utf8`),a;try{a=JSON.parse(i)}catch{throw new g(`INVALID_INPUT`,`Report parameters must be valid JSON.`)}if(!a||typeof a!=`object`||Array.isArray(a))throw new g(`INVALID_INPUT`,`Report parameters must be an object.`);return a}function el(e){if(!e.isError)return;let t=e.structuredContent;throw new g(t?.error?.code??`INTERNAL_ERROR`,t?.error?.message??`The report could not be completed.`)}function tl(e){if(!e||typeof e!=`object`)return[];let t=e,n=new Set(t.required??[]);return Object.entries(t.properties??{}).map(([e,t])=>{let r=Array.isArray(t.type)?t.type.join(` | `):t.type??`any`;return{name:e,type:r===`array`&&t.items?.type?`${t.items.type}[]`:t.format?`${r} (${t.format})`:r,required:n.has(e),description:t.description}})}const nl=b({meta:{name:`list`,description:`List report ids available to the CLI and MCP server`},args:{category:{type:`string`,description:`Filter by category: ${Nn.join(`, `)}.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=Qc(e.category),n=gn(t);A(e)?I({reports:n,categories:Nn}):(na(n,{noun:`report`,categoryLabels:Zc}),process.stdout.write(`
88
- `),z({title:`Need the detail?`,body:`Describe one report to see when to use it, what it needs, and what the result can prove.`,command:`seo reports describe <id>`}))}}),rl=b({meta:{name:`describe`,description:`Show the purpose and parameter schema for one report`},args:{id:{type:`positional`,required:!0,description:"Report id from `seo reports list`."},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=Jt(w(e.id)??``);A(e)?I({report:t}):($i(t.name,t.description),process.stdout.write(`
89
- `),L([[`ID`,t.id],[`Category`,t.category],[`Outcome`,t.outcome]]),process.stdout.write(`
90
- `),aa(`Use when`,t.useWhen),process.stdout.write(`
91
- `),aa(`Avoid when`,t.avoidWhen),process.stdout.write(`
92
- `),aa(`Read in order`,t.readOrder),process.stdout.write(`
93
- `),aa(`Do not claim`,t.doNotClaim),process.stdout.write(`
94
- `),ia(`Verify`,t.verify),process.stdout.write(`
95
- `),z({title:`Implement findings`,body:t.agentWorkflow.completion,command:t.agentWorkflow.actionView.cli}),t.related.length>0&&(process.stdout.write(`
96
- `),aa(`Related reports`,t.related.map(e=>`${e.id}: ${e.reason}`))),process.stdout.write(`
83
+ `),z([`Segment`,`Retained in`,`Clicks`,`Impressions`],l.unmatchedSegments.map(e=>[e.key,e.retainedIn,e.clicks,e.impressions]))),Y(`Warnings`,l.warnings))}});function Dc(e){return`${U(e)} ${e===1?`row`:`rows`}`}const Oc=b({meta:{name:`striking-distance`,description:`Find query/page rows averaging positions above 10 through 20`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},...X([`days`,`minImpressions`,`limit`,`includeBrand`,`verifyContent`,`verifyLimit`,`js`,`fetchConcurrency`,`fetchIntervalCap`,`fetchIntervalMs`,`refresh`]),json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=O(e[`verify-limit`]),r=E(e[`verify-content`])===!0||n!==void 0,i=M(T(e.site)),a=await c({site:i,days:O(e.days),minImpressions:O(e[`min-impressions`]),limit:O(e.limit),includeBrand:E(e[`include-brand`]),verifyContent:r,verifyLimit:n,js:E(e.js)?!0:void 0,rate:hi(e),refresh:E(e.refresh)});t?L(a):(K({title:`Striking-distance opportunities`,target:a.site,status:a.source.possiblyTruncated?`unknown`:`info`,summary:a.summary.verdict,metrics:[{label:`Eligible rows`,value:U(a.summary.eligibleRows)},{label:`Returned rows`,value:U(a.summary.returnedRows)},{label:`Template groups`,value:U(a.summary.groups)},{label:`Impressions`,value:U(a.summary.eligibleImpressions)},{label:`Brand queries`,value:a.summary.brandFiltering},{label:`Verification`,value:a.verification.requested?`Requested`:`Off`},{label:`Evidence`,value:a.dataStatus}]}),Y(`Why this matters`,[`These rows have a GSC average position above 10 and at most 20. Treat them as candidates for investigation, not guaranteed page-two rankings.`,`Groups use all eligible rows. A shared-template label is only a candidate until recurring evidence is verified across distinct URLs.`]),Y(`Recommended actions`,a.recommendations),Y(`Report caveats`,a.caveats),a.groups.length&&(q([`Group`,`Rows`,`URLs`,`Impr`,`Avg pos`,`Scope`,`Action`],a.groups.map(e=>[G(e.label,36),U(e.rowCount),U(e.uniqueUrls),U(e.totalImpressions),Ja(e.impressionWeightedPosition),e.actionScope,G(e.recommendation.action,72)])),J(`Top striking-distance group actions`,a.groups.map(e=>({label:e.label,context:`${Dc(e.rowCount)}, ${U(e.totalImpressions)} impressions`,action:e.recommendation.action})))),a.items.length?(q([`Query`,`Template`,`URL`,`Impr`,`CTR`,`Pos`,`Score`,`Fetch`,`Check`,`Action`],a.items.map(e=>[G(e.query,36),G(e.template.label,24),G(e.url,48),U(e.impressions),W(e.ctr),Ja(e.position),e.priority.score,Co(e.contentVerification?.fetchDiagnostics),wo(e.contentVerification?.classification),G(e.recommendation.action,72)])),J(`Top striking-distance actions`,a.items.map(e=>({label:e.query,context:`${e.template.label}, pos ${Ja(e.position)}, ${U(e.impressions)} impressions`,action:e.recommendation.action})))):process.stdout.write(`No striking-distance opportunities matched these filters.
84
+ `))}});function kc(e){return e===`keychain`?`system keychain`:e===`file`?`private local file`:e??`missing`}function Ac(e){return e===null?`unavailable`:`$${(e/1e6).toFixed(2)}`}function jc(e){return e.limit===null?e.used===null?`not reported`:`${e.used.toLocaleString(`en-US`)} used; no key limit`:`${(e.remaining??0).toLocaleString(`en-US`)} of ${e.limit.toLocaleString(`en-US`)} remaining`}const Mc=b({meta:{name:`connect`,description:`Validate and save an Ahrefs API v3 key`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{if(!I({json:j(e)}))throw new g(`AUTH_REQUIRED`,`Run \`seo providers ahrefs connect\` in a terminal. Agents and CI can set ${Qe}.`);x(`Connect Ahrefs`),S(`Paste an API v3 key from Ahrefs Account settings. The validation check and Domain Rating are free; other research can consume API units.`,`API key`);let t=B(await Gr({message:`Ahrefs API v3 key`,validate:e=>e?.trim()?void 0:`API key is required`})),n=await new vn({apiKey:t}).limitsAndUsage(),r=await tt(t);S(`${n.subscription} subscription. API key units: ${jc(n.apiKeyUnits)}.`,`Connection verified`),C(`Saved in the ${kc(r)}. Run seo providers ahrefs status --check to verify it again.`)}}),Nc=b({meta:{name:`status`,description:`Show the local Ahrefs connection`},args:{check:{type:`boolean`,default:!1,description:`Verify the API key with the free account endpoint.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Ye(),n=!!e.check,r=n&&t?await new vn().limitsAndUsage():void 0,i={connected:!!t,apiVersion:t?3:null,credentialSource:t?.source,liveCheck:r?{status:`passed`,subscription:r.subscription,apiKeyExpiresAt:r.apiKeyExpiresAt,usageResetsAt:r.usageResetsAt,apiKeyUnits:r.apiKeyUnits,workspaceUnits:r.workspaceUnits,observedAt:r.observedAt,requestCostUnits:r.requestCostUnits}:{status:n?`unavailable`:`not-requested`}};j(e)?L(i):R([[`Connected`,i.connected?`yes`:`no`],[`API version`,i.apiVersion?`Version 3`:`not connected`],[`Credential`,kc(i.credentialSource)],[`Live check`,i.liveCheck.status===`passed`?`passed at ${i.liveCheck.observedAt}`:i.liveCheck.status===`unavailable`?`not available without credentials`:`not requested; pass --check to verify`],...r?[[`Subscription`,r.subscription],[`API key units`,jc(r.apiKeyUnits)],[`Workspace units`,jc(r.workspaceUnits)],[`Usage resets`,r.usageResetsAt],[`Key expires`,r.apiKeyExpiresAt]]:[]])}}),Pc=b({meta:{name:`disconnect`,description:`Remove the saved Ahrefs API v3 key`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{await le();let t=!!process.env[Qe]?.trim(),n={savedCredentialRemoved:!0,environmentCredential:t?`active`:`missing`,note:t?`The environment variable was not changed. Clear ${Qe} to fully disconnect.`:`Ahrefs is disconnected.`};j(e)?L(n):process.stdout.write(`${n.note}\n`)}}),Fc=b({meta:{name:`ahrefs`,description:`Connect Ahrefs for optional search and link data`},subCommands:{connect:Mc,status:Nc,disconnect:Pc}}),Ic="Run `seo providers bing connect` with a new API key.",Lc=b({meta:{name:`connect`,description:`Check and save a Bing Webmaster Tools API key`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{if(!I({json:j(e)}))throw new g(`AUTH_REQUIRED`,`Run \`seo providers bing connect\` in a terminal. Agents and CI can set ${xe}.`);x(`Connect Bing Webmaster Tools`),S(`In Bing Webmaster Tools, open Settings, then API access, and copy the API key. Bing data is free.`,`API key`);let t=B(await Gr({message:`Bing Webmaster API key`,validate:e=>e?.trim()?void 0:`API key is required`})),n=await $n(Vn({apiKey:t},{signIn:Ic})),r=await o(t);S(`${n.filter(e=>e.verified).length} verified sites.`,`Connection verified`),C(`Saved in the ${kc(r)}.`)}}),Rc=b({meta:{name:`status`,description:`Show the Bing Webmaster connection`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await tr(),n=t?await $n(Vn({apiKey:t.apiKey},{signIn:Ic})):null,r={connected:!!t,credentialSource:t?.source??null,sites:n?.filter(e=>e.verified).map(e=>e.url)??[]};j(e)?L(r):process.stdout.write(t?`Connected (${kc(t.source)}). Verified sites: ${r.sites.join(`, `)||`none`}.\n`:"Not connected. Run `seo providers bing connect`.\n")}}),zc=b({meta:{name:`disconnect`,description:`Remove the saved Bing Webmaster API key`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{await ze();let t=!!process.env[xe]?.trim(),n=t?`The saved key is removed. Clear ${xe} to fully disconnect.`:`Bing is disconnected.`;j(e)?L({removed:!0,environment:t}):process.stdout.write(`${n}\n`)}}),Bc=b({meta:{name:`bing`,description:`Connect Bing Webmaster Tools for Bing search data`},subCommands:{connect:Lc,status:Rc,disconnect:zc}}),Vc=b({meta:{name:`connect`,description:`Validate and save DataForSEO API credentials`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{if(!I({json:j(e)}))throw new g(`AUTH_REQUIRED`,`Run \`seo providers dataforseo connect\` in a terminal. Agents and CI can set ${we} and ${De}.`);x(`Connect DataForSEO`),S(`Use the API login and API password from DataForSEO API Access. The validation call is free; later research reports can make charged requests.`,`API credentials`);let t=B(await qr({message:`DataForSEO API login`,validate:e=>e?.trim()?void 0:`API login is required`})),n=B(await Gr({message:`DataForSEO API password`,validate:e=>e?void 0:`API password is required`})),r={login:t.trim(),password:n},i=await new Pt({credentials:()=>r}).userData(),a=await fe(r);S(`${i.login} is connected with an account balance of ${Ac(i.balanceMicros)}.`,`Connection verified`),C(`Saved in the ${kc(a)}. Run seo providers dataforseo status --check to verify it again.`)}}),Hc=b({meta:{name:`status`,description:`Show the local DataForSEO connection`},args:{check:{type:`boolean`,default:!1,description:`Verify credentials with the free account endpoint.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await re(),n=!!e.check,r=n&&t?await new Pt().userData():void 0,i={connected:!!t,credentialSource:t?.source,migratedLegacyCredentials:t?.migrated??!1,liveCheck:r?{status:`passed`,account:r.login,timezone:r.timezone,balanceMicros:r.balanceMicros,observedAt:r.observedAt,requestCostMicros:r.requestCostMicros}:{status:n?`unavailable`:`not-requested`}};j(e)?L(i):R([[`Connected`,i.connected?`yes`:`no`],[`Credential`,kc(i.credentialSource)],[`Live check`,i.liveCheck.status===`passed`?`passed at ${i.liveCheck.observedAt}`:i.liveCheck.status===`unavailable`?`not available without credentials`:`not requested; pass --check to verify`],...r?[[`Account`,r.login],[`Balance`,Ac(r.balanceMicros)]]:[]])}}),Uc=b({meta:{name:`disconnect`,description:`Remove saved DataForSEO credentials`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{await Ne();let t=!!process.env[we]?.trim(),n=!!process.env[De],r={savedCredentialsRemoved:!0,environmentCredentials:t&&n?`active`:t||n?`partial`:`missing`,note:t||n?`Environment variables were not changed. Clear ${we} and ${De} to fully disconnect.`:`DataForSEO is disconnected.`};j(e)?L(r):process.stdout.write(`${r.note}\n`)}}),Wc=b({meta:{name:`dataforseo`,description:`Connect DataForSEO for optional search data`},subCommands:{connect:Vc,status:Hc,disconnect:Uc}});function Gc(e){if(!e)return{};let t;try{t=JSON.parse(e)}catch{throw new g(`INVALID_INPUT`,`--account must be a JSON object.`)}if(!t||Array.isArray(t)||typeof t!=`object`)throw new g(`INVALID_INPUT`,`--account must be a JSON object.`);let n=Object.entries(t);if(n.length>20||n.some(([e,t])=>!e||e.length>64||typeof t!=`string`||t.length>4096))throw new g(`INVALID_INPUT`,`--account must contain at most 20 bounded string fields.`);return Object.fromEntries(n.map(([e,t])=>[e,t.trim()]))}function Kc(e){if(!e)return{};let t;try{t=JSON.parse(e)}catch{throw new g(`INVALID_INPUT`,`--params must be a JSON object.`)}if(!t||Array.isArray(t)||typeof t!=`object`)throw new g(`INVALID_INPUT`,`--params must be a JSON object.`);let n=Object.entries(t);if(n.length>100||n.some(([e])=>!/^[A-Za-z][A-Za-z0-9-]{0,63}$/u.test(e))||Buffer.byteLength(e)>65536)throw new g(`INVALID_INPUT`,`--params must be a JSON object smaller than 64 KiB.`);return Object.fromEntries(n)}async function qc(e){let t=await ft(),n=t.registry.get(e);if(n)return n;let r=t.failures.find(t=>t.id===e);throw r?new g(`INVALID_INPUT`,`Provider ${e} could not load: ${r.message}`):new g(`INVALID_INPUT`,`Provider ${e} is not installed.`)}function Jc(e,t){let n=new Set(e.connection.fields.filter(e=>e.kind===`account`).map(e=>e.id)),r=Object.keys(t).find(e=>!n.has(e));if(r)throw new g(`INVALID_INPUT`,`${r} is not an account field for ${e.displayName}.`);let i=e.connection.normalizeAccount?e.connection.normalizeAccount(t):{...t};if(Object.keys(i).find(e=>!n.has(e)))throw new g(`PROVIDER_UNAVAILABLE`,`${e.displayName} returned an unknown account field.`);for(let t of e.connection.fields.filter(e=>e.kind===`account`&&e.required!==!1))if(!i[t.id]?.trim())throw new g(`INVALID_INPUT`,`${t.label} is required.`);return i}async function Yc(){return zn()}const Xc={list:b({meta:{name:`list`,description:`List installed provider packages`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Yc();j(e)?L(t):t.installed.length>0?z([`Installed provider`,`Package`,`Version`,`Status`],t.installed.map(e=>[e.id,e.package,e.version,e.loadStatus])):process.stdout.write(`No provider extensions are installed.
85
+ `)}}),describe:b({meta:{name:`describe`,description:`Describe one installed provider for an agent`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.id)??``,n=await pt(t);j(e)?L(n):R([[`Provider`,n.displayName],[`Package`,`${n.package}@${n.version}`],[`Description`,n.description],[`Capabilities`,n.capabilities.map(e=>e.id).join(`, `)||`none`],[`Actions`,n.actions.map(e=>e.id).join(`, `)||`none`]])}}),install:b({meta:{name:`install`,description:`Install one provider package from npm`},args:{package:{type:`positional`,required:!0,description:`npm package name with an optional exact version.`},yes:{type:`boolean`,default:!1,description:`Approve third-party code installation without prompting.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.package)??``,n=j(e),r=E(e.yes),i=I({json:n});if(!r&&!i)throw new g(`INVALID_INPUT`,`Provider installation runs third-party code. Review the package, then pass --yes to approve it in JSON or CI mode.`);let a;try{a=await sn(t)}catch(e){throw new g(`INVALID_INPUT`,e instanceof Error?e.message:String(e))}if(!r&&(x(`Install provider package`),S([`${a.package}@${a.version}`,`Publisher: ${a.publisher??`not supplied by npm`}`,`Repository: ${a.repository??`not supplied by npm`}`,`Integrity: ${a.integrity}`].join(`
86
+ `),`Package`),S(`This package can read files, use the network, and run with the same local permissions as seo. Installing it is not a security review or endorsement.`,`Third-party code`),!B(await Vr({message:`Install ${a.package}@${a.version}?`,initialValue:!1})))){C(`Provider was not installed.`);return}let o;try{o=await Ht(a)}catch(e){throw new g(`PROVIDER_UNAVAILABLE`,e instanceof Error?e.message:String(e))}n?L({installed:o,package:a}):C(`Installed provider ${o.id} from ${o.package}@${o.version}.`)}}),remove:b({meta:{name:`remove`,description:`Remove one installed provider extension`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},yes:{type:`boolean`,default:!1,description:`Remove the package without prompting.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.id)??``,n=j(e),r=E(e.yes),i=I({json:n});if(!r&&!i)throw new g(`INVALID_INPUT`,`Pass --yes to remove a provider extension in JSON or CI mode.`);if(!r&&!B(await Vr({message:`Remove provider ${t}?`,initialValue:!1})))return;let a=await Rt(t);if(!a)throw new g(`INVALID_INPUT`,`Provider ${t} is not installed.`);n?L({removed:a}):process.stdout.write(`Removed ${t}.\n`)}}),connect:b({meta:{name:`connect`,description:`Validate and save one installed provider connection`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},account:{type:`string`,description:`Account fields as JSON. Secret fields must use their environment variables.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.id)??``,r=j(e),i=I({json:r}),a=await qc(t),o={...xt(t),...Gc(T(e.account))},s={};if(i){x(`Connect ${a.displayName}`),a.connection.verificationNotice&&S(a.connection.verificationNotice,`Connection check`);for(let e of a.connection.fields){if(e.kind===`account`){let t=B(await qr({message:e.label,placeholder:e.description,initialValue:o[e.id],validate:t=>e.required!==!1&&!t?.trim()?`${e.label} is required`:void 0}));t?.trim()&&(o[e.id]=t.trim());continue}if(e.envVar&&process.env[e.envVar]?.trim())continue;let t=B(await Gr({message:e.label,validate:t=>e.required!==!1&&!t?.trim()?`${e.label} is required`:void 0}));t?.trim()&&(s[e.id]=t.trim())}}o=Jc(a,o);let c={...s};Object.keys(c).length===0&&(c=await n({providerId:t,account:o,fields:a.connection.fields})),await ot({providerId:t,account:o,credentials:c}),Object.keys(s).length>0&&await jt({providerId:t,account:o,credentials:s}),qn(t,o);let l={provider:t,connected:!0,account:o,capabilities:(a.capabilities??[]).map(e=>e.id),actions:(a.actions??[]).map(e=>e.id)};r?L(l):C(`Connected ${a.displayName}. Run seo providers describe ${t} to see what it can do.`)}}),status:b({meta:{name:`status`,description:`Show one installed provider connection`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},check:{type:`boolean`,default:!1,description:`Run the provider connection check.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.id)??``,r=await qc(t),i=xt(t),a;try{a=await n({providerId:t,account:i,fields:r.connection.fields})}catch{a=void 0}let o=r.connection.fields.filter(e=>e.kind===`account`&&e.required!==!1).every(e=>!!i[e.id]?.trim())&&a!==void 0,s=E(e.check);if(s&&!o)throw new g(`AUTH_REQUIRED`,`Provider ${t} is not connected. Run \`seo providers connect ${t}\`.`);s&&a&&await ot({providerId:t,account:i,credentials:a});let c={provider:t,installed:!0,connected:o,account:i,capabilities:(r.capabilities??[]).map(e=>e.id),actions:(r.actions??[]).map(e=>e.id),check:s?`passed`:`not-requested`};j(e)?L(c):z([`Provider`,`Connected`,`Capabilities`,`Check`],[[r.displayName,o?`yes`:`no`,c.capabilities.join(`, `),c.check]])}}),disconnect:b({meta:{name:`disconnect`,description:`Remove one saved provider connection`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.id)??``;await qc(t);let n=xt(t);await Re({providerId:t,account:n}),Zn(t);let r={provider:t,disconnected:!0,note:`Environment variables were not changed.`};j(e)?L(r):process.stdout.write(`Disconnected ${t}. ${r.note}\n`)}}),run:b({meta:{name:`run`,description:`Run one action supplied by an installed provider`},args:{id:{type:`positional`,required:!0,description:`Installed provider id.`},action:{type:`positional`,required:!0,description:`Provider action id.`},account:{type:`string`,description:`Account fields as JSON. Defaults to the saved connection.`},params:{type:`string`,description:`Action parameters as a JSON object.`},refresh:{type:`boolean`,default:!1,description:`Bypass the local provider cache.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.id)??``,n=T(e.action)??``;await qc(t);let r={...xt(t),...Gc(T(e.account))};L(await He({providerId:t,actionId:n,account:r,params:Kc(T(e.params)),refresh:E(e.refresh)}))}}),doctor:b({meta:{name:`doctor`,description:`Validate installed provider packages and entry points`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=await Yc(),n={ok:t.installed.every(e=>e.loadStatus===`ready`),installed:t.installed};j(e)?L(n):n.installed.length===0?process.stdout.write(`No provider extensions are installed.
87
+ `):z([`Provider`,`Package`,`Version`,`Status`],n.installed.map(e=>[e.id,e.package,e.version,e.error??e.loadStatus]))}})},Zc=b({meta:{name:`providers`,description:`Connect optional data providers`},subCommands:{...Xc,ahrefs:Fc,bing:Bc,dataforseo:Wc}}),Qc={"ai-search":`AI search`,crawl:`Crawling and technical checks`,diagnosis:`Diagnosis`,monitoring:`Monitoring`,opportunities:`Opportunities`,reporting:`Reporting`,setup:`Setup`,workflows:`Workflows`};function $c(e){let t=T(e);if(t){if(!jn.includes(t))throw new g(`INVALID_INPUT`,`Unknown report category: ${t}. Choose ${jn.join(`, `)}.`);return t}}async function el(e,t){let n=T(e),r=T(t);if(n&&r)throw new g(`INVALID_INPUT`,`Use either --params or --params-file, not both.`);if(!n&&!r)return{};let i=n??await y(r??``,`utf8`),a;try{a=JSON.parse(i)}catch{throw new g(`INVALID_INPUT`,`Report parameters must be valid JSON.`)}if(!a||typeof a!=`object`||Array.isArray(a))throw new g(`INVALID_INPUT`,`Report parameters must be an object.`);return a}function tl(e){if(!e.isError)return;let t=e.structuredContent;throw new g(t?.error?.code??`INTERNAL_ERROR`,t?.error?.message??`The report could not be completed.`)}function nl(e){if(!e||typeof e!=`object`)return[];let t=e,n=new Set(t.required??[]);return Object.entries(t.properties??{}).map(([e,t])=>{let r=Array.isArray(t.type)?t.type.join(` | `):t.type??`any`;return{name:e,type:r===`array`&&t.items?.type?`${t.items.type}[]`:t.format?`${r} (${t.format})`:r,required:n.has(e),description:t.description}})}const rl=b({meta:{name:`list`,description:`List report ids available to the CLI and MCP server`},args:{category:{type:`string`,description:`Filter by category: ${jn.join(`, `)}.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=$c(e.category),n=mn(t);j(e)?L({reports:n,categories:jn}):(ra(n,{noun:`report`,categoryLabels:Qc}),process.stdout.write(`
88
+ `),$i({title:`Need the detail?`,body:`Describe one report to see when to use it, what it needs, and what the result can prove.`,command:`seo reports describe <id>`}))}}),il=b({meta:{name:`describe`,description:`Show the purpose and parameter schema for one report`},args:{id:{type:`positional`,required:!0,description:"Report id from `seo reports list`."},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=Kt(T(e.id)??``);j(e)?L({report:t}):(ea(t.name,t.description),process.stdout.write(`
89
+ `),R([[`ID`,t.id],[`Category`,t.category],[`Outcome`,t.outcome]]),process.stdout.write(`
90
+ `),oa(`Use when`,t.useWhen),process.stdout.write(`
91
+ `),oa(`Avoid when`,t.avoidWhen),process.stdout.write(`
92
+ `),oa(`Read in order`,t.readOrder),process.stdout.write(`
93
+ `),oa(`Do not claim`,t.doNotClaim),process.stdout.write(`
94
+ `),aa(`Verify`,t.verify),process.stdout.write(`
95
+ `),$i({title:`Implement findings`,body:t.agentWorkflow.completion,command:t.agentWorkflow.actionView.cli}),t.related.length>0&&(process.stdout.write(`
96
+ `),oa(`Related reports`,t.related.map(e=>`${e.id}: ${e.reason}`))),process.stdout.write(`
97
97
  Parameters
98
- `),oa(tl(t.inputSchema)),process.stdout.write(`
99
- `),z({title:`Need the exact schema?`,command:`seo reports describe ${t.id} --json`}))}}),il=b({meta:{name:`run`,description:`Run one report with JSON parameters`},args:{id:{type:`positional`,required:!0,description:"Report id from `seo reports list`."},params:{type:`string`,description:`Report parameters as a JSON object.`},"params-file":{type:`string`,description:`Read report parameters from a JSON file.`},"actions-only":{type:`boolean`,default:!1,description:`Return findings, verification, and retained inventories without the full report body.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.id)??``,n=await $c(e.params,e[`params-file`]),r=await _t(t,n,{view:T(e[`actions-only`])?`actions`:`full`});if(el(r),A(e))I(r.structuredContent??{});else for(let e of r.content)process.stdout.write(`${e.text}\n`)}}),al=b({meta:{name:`reports`,description:`Discover and run the complete report catalog`},subCommands:{list:nl,describe:rl,run:il}}),ol=b({meta:{name:`rules`,description:`List crawler rule ids and default guidance`},args:{category:{type:`string`,description:`Filter rules by category.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.category),n=d().filter(e=>!t||e.category===t);A(e)?I({rules:n}):na(n.map(e=>({id:e.id,category:e.category,name:`${e.title} · ${e.defaultSeverity}`})),{noun:`rule`})}}),sl=b({meta:{name:`explain`,description:`Explain one crawler rule in plain English`},args:{rule:{type:`string`,required:!0,description:`Rule id to explain, for example missing_title.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=w(e.rule)??``,n=We(t);if(!n)throw Error(`Unknown rule: ${t}`);A(e)?I(n):(L([[`Rule`,n.id],[`Title`,n.title],[`Category`,n.category],[`Severity`,n.defaultSeverity]]),process.stdout.write(`
100
- `),ia(`Why it matters`,n.whyItMatters),process.stdout.write(`
101
- `),ia(`How to fix`,n.howToFix),process.stdout.write(`
102
- `),ia(`Impact if ignored`,n.impactIfIgnored),process.stdout.write(`
103
- `),ia(`How to verify`,n.howToVerify))}});function cl(){return dl(`skills`,process.env.SEO_SKILLS_DIR)}function ll(){return dl(`evals`,process.env.SEO_EVALS_DIR)}function ul(){return dl(`site-kit`)}function dl(e,t){if(t)return dr(t);let n=cr(Tr(import.meta.url));for(let t=0;t<6;t+=1){let t=_(n,e);if(v(_(t,`README.md`)))return t;n=cr(n)}throw new g(`INTERNAL_ERROR`,`Packaged ${e} could not be found. Reinstall \`seo\` and try again.`)}function fl(e){return vr(_(e,`SKILL.md`),`utf8`).match(/^description:\s*(.+)$/m)?.[1]?.trim()??``}const pl=[[`setup`,`Sign in to the accounts the site needs, one at a time`],[`market`,`Choose a market with real demand and data you can own`],[`data`,`Get, clean and keep the dataset`],[`plan`,`Plan the pages from what people search for`],[`build`,`Start the site and deploy a private preview`],[`golden-page`,`Build one page that is the best answer on the web`],[`writing`,`Write copy people enjoy and AI answers can quote`],[`scale`,`Turn the golden page into every page, with gates`],[`launch`,`Go live and get the important pages indexed`],[`grow`,`Measure with your own data and improve every week`]],ml=/* @__PURE__ */ new Set([`node_modules`,`dist`,`.astro`,`.wrangler`]);function hl(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,`-`).replace(/^-|-$/g,``)}function gl(e){return hl(sr(e)).split(`-`).filter(Boolean).map(e=>`${e[0]?.toUpperCase()}${e.slice(1)}`).join(` `)}function _l(e){let t=(e??`example.com`).trim().toLowerCase().replace(/^https?:\/\//,``).replace(/\/.*$/,``);if(!/^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/.test(t))throw new g(`INVALID_INPUT`,`--domain must be a domain such as example.com, not ${e}.`);return t}async function vl(e,t){let n=JSON.parse(await y(e,`utf8`));t(n),await Mr(e,`${JSON.stringify(n,null,2)}\n`)}function yl(){return Xo(`pnpm`,[`--version`],{stdio:`ignore`}).status===0?`pnpm`:`npm`}const bl=b({meta:{name:`new`,description:`Copy the Astro on Cloudflare starter into a new folder`},args:{folder:{type:`positional`,required:!0,description:`Folder to create. It must not exist or must be empty.`},name:{type:`string`,description:`Site name. Defaults to the folder name.`},domain:{type:`string`,description:`Production domain. Defaults to example.com until you buy one.`},install:{type:`boolean`,default:!0,description:`Install packages after copying.`,negativeDescription:`Copy the files only.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=dr(String(e.folder));if(v(n)&&(await kr(n)).some(e=>e!==`.git`))throw new g(`INVALID_INPUT`,`${n} is not empty. Choose a new folder for the site.`);let r=w(e.name)?.trim()||gl(n),i=_l(w(e.domain)),a=hl(sr(n))||`site`,o=rr(16).toString(`hex`),s=_(ul(),`starter`);await Er(s,n,{recursive:!0,filter:e=>!ur(s,e).split(fr).some(e=>ml.has(e))}),await Ar(_(n,`gitignore`),_(n,`.gitignore`)),await vl(_(n,`site.config.json`),e=>{e.name=r,e.domain=i,e.indexNowKey=o}),await Mr(_(n,`public`,`${o}.txt`),o),await vl(_(n,`package.json`),e=>{e.name=a;let t=e.devDependencies;t.seo=`^${sn}`});let c=_(n,`wrangler.jsonc`);await Mr(c,(await y(c,`utf8`)).replace(`"name": "example-site"`,`"name": ${JSON.stringify(a)}`));let l=_(n,`README.md`);await Mr(l,(await y(l,`utf8`)).replace(/^# .*$/m,`# ${r}`));let u=E(e,`install`)===!0?void 0:yl(),d=!1;u&&(t||process.stdout.write(`Installing packages with ${u}...\n`),d=Xo(u,[`install`],{cwd:n,stdio:t?`ignore`:`inherit`}).status===0);let f=[`cd ${ur(process.cwd(),n)||`.`}`,`seo site guide setup`];t?I({folder:n,name:r,domain:i,installed:d,packageManager:u??null,next:f}):($i(`Created ${r}`,n),L([[`Domain`,i],[`Packages`,d?`installed with ${u}`:u?`${u} install failed. Run it in the folder.`:`not installed`]]),process.stdout.write(`
104
- `),ia(`What is in it`,"An example site about parks, built from src/data. It already passes `seo site check`: titles, canonicals, JSON-LD, sharing images, Markdown copies, llms.txt and sitemaps are done for every page.",`Later steps replace the example data and pages with yours.`),z({title:`Next`,body:`Read the first step. Each step ends with the next one.`,command:f.join(` && `)}))}});function xl(e){let t=hl(e.join(` `));return pl.find(([e])=>e===t)?.[0]}const Sl=b({meta:{name:`guide`,description:`Print the instructions for one step of building the site`},args:{step:{type:`positional`,required:!1,description:pl.map(([e])=>e).join(`, `)},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=e._?.length?e._:[w(e.step)??``];if(!n.join(``).trim()){if(t){I({steps:pl.map(([e,t])=>({step:e,summary:t}))});return}$i(`Build a site, one step at a time`,"Run `seo site guide <step>` and follow it before the next step."),aa(`Steps`,pl.map(([e,t])=>`${e}: ${t}`)),z({title:`Start here`,command:`seo site guide setup`});return}let r=xl(n);if(!r)throw new g(`INVALID_INPUT`,`Unknown step "${n.join(` `)}". Steps: ${pl.map(([e])=>e).join(`, `)}.`);let i=await y(_(ul(),`guide`,`${r}.md`),`utf8`),a=pl.findIndex(([e])=>e===r),o=pl[a+1]?.[0];t?I({step:r,number:a+1,of:pl.length,markdown:i,next:o?`seo site guide ${o}`:null}):process.stdout.write(i.endsWith(`
105
- `)?i:`${i}\n`)}});async function Cl(e){return(await kr(e,{withFileTypes:!0,recursive:!0})).filter(e=>e.isFile()).map(t=>ur(e,_(t.parentPath,t.name)).split(fr).join(`/`))}function wl(e){$i(e.status===`passed`?`Site check passed`:e.status===`partial`?`Site check stopped early`:`Site check failed`,e.origin),L([[`Pages`,e.pagesChecked===e.pagesFound?String(e.pagesFound):`${e.pagesChecked} of ${e.pagesFound} checked`],[`Failures`,String(e.failures)],[`Warnings`,String(e.warnings)]]),process.stdout.write(`
106
- `);for(let t of e.rules.slice(0,40)){let n=e.issues.filter(e=>e.rule===t.rule).slice(0,3).map(e=>e.found?`${e.file}: ${e.found}`:e.file);aa(`${t.level===`fail`?`Fail`:`Warn`} ${t.rule} (${t.group}, ${t.count}). ${t.fix}`,t.count>n.length?[...n,`and ${t.count-n.length} more`]:n)}e.rules.length>40&&process.stdout.write(`${e.rules.length-40} more rules failed. Use --json for all of them.\n\n`),e.status===`partial`&&ia(`Not every page was checked`,`The check stops at ${e.pagesChecked} pages, so it cannot say the rest pass. Split the site or build fewer, stronger pages.`),z(e.status===`passed`?{title:`Next`,body:`Deploy the build, or read the next step of the guide.`,command:`seo site guide`}:{title:`Next`,body:`Fix the template or the data behind the first rule, then build again.`,command:`pnpm build`})}const Tl=b({meta:{name:`check`,description:`Check the built site and fail on any rule it breaks`},args:{dir:{type:`string`,description:`Site folder with site.config.json. Defaults to this folder.`},dist:{type:`string`,description:`Build folder. Defaults to dist inside the site folder.`},domain:{type:`string`,description:`Production domain. Defaults to the one in site.config.json.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=dr(w(e.dir)??`.`),r=dr(n,w(e.dist)??`dist`),i=_(n,`site.config.json`),a=v(i)?JSON.parse(await y(i,`utf8`)):{},o=w(e.domain)??a.domain;if(!o)throw new g(`INVALID_INPUT`,"No domain. Run this in a folder made by `seo site new`, or pass --domain.");if(!v(r))throw new g(`INVALID_INPUT`,`${r} does not exist. Build the site first with \`pnpm build\`.`);let s=await kt({origin:`https://${_l(o)}`,files:await Cl(r),readText:e=>y(_(r,e),`utf8`),readBytes:e=>y(_(r,e)),allowWords:a.allowWords??[],sources:v(_(n,`src`))?{files:(await Cl(_(n,`src`))).map(e=>`src/${e}`),readText:e=>y(_(n,e),`utf8`)}:void 0});t?I(s):wl(s),s.status!==`passed`&&(process.exitCode=1)}}),El=b({meta:{name:`site`,description:`Build a data-driven site on Astro and Cloudflare`},subCommands:{new:bl,guide:Sl,check:Tl}}),Dl=`claude -p`;function Ol(){let e=ll();return yr(e,{withFileTypes:!0}).filter(e=>e.isFile()&&e.name.endsWith(`.json`)).map(t=>({subject:sr(t.name,`.json`),path:_(e,t.name)})).sort((e,t)=>e.subject<t.subject?-1:+(e.subject>t.subject))}function kl(e,t){let n;try{n=vr(t,`utf8`)}catch{throw new g(`INVALID_INPUT`,`Cannot read eval file for ${e}.`)}let r;try{r=JSON.parse(n)}catch{throw new g(`INVALID_INPUT`,`Eval file ${e}.json is not valid JSON.`)}if(!r||typeof r!=`object`||Array.isArray(r))throw new g(`INVALID_INPUT`,`Eval file ${e}.json must be an object.`);let i=r;if(i.subject!==e)throw new g(`INVALID_INPUT`,`Eval file ${e}.json must set subject to "${e}".`);if(!Array.isArray(i.evals)||i.evals.length===0)throw new g(`INVALID_INPUT`,`Eval file ${e}.json must have a non-empty evals array.`);let a=/* @__PURE__ */ new Set;return{subject:e,evals:i.evals.map((t,n)=>{let r=t,i=`${e}.json eval ${n+1}`;if(!Number.isInteger(r?.id))throw new g(`INVALID_INPUT`,`${i}: id must be an integer.`);let o=r.id;if(a.has(o))throw new g(`INVALID_INPUT`,`${i}: duplicate id ${o}.`);if(a.add(o),typeof r.prompt!=`string`||r.prompt.trim().length===0)throw new g(`INVALID_INPUT`,`${i}: prompt must be a non-empty string.`);if(typeof r.expected_output!=`string`||r.expected_output.trim().length===0)throw new g(`INVALID_INPUT`,`${i}: expected_output must be a non-empty string.`);if(!Array.isArray(r.assertions)||r.assertions.length===0||r.assertions.some(e=>typeof e!=`string`||e.trim().length===0))throw new g(`INVALID_INPUT`,`${i}: assertions must be a non-empty array of non-empty strings.`);if(r.files!==void 0&&!Array.isArray(r.files))throw new g(`INVALID_INPUT`,`${i}: files must be an array.`);return{id:o,prompt:r.prompt,expected_output:r.expected_output,assertions:r.assertions,files:r.files??[]}})}}function Al(){let e=_(cl(),`seo`,`SKILL.md`);if(!v(e))throw new g(`INTERNAL_ERROR`,"The seo router skill could not be found. Reinstall `seo` or use --no-skill.");return vr(e,`utf8`)}function jl(e){let t=[],n=``,r=!1,i=!1,a=!1;for(let o=0;o<e.length;o+=1){let s=e[o];i?(s===`'`?i=!1:n+=s,r=!0):a?(s===`"`?a=!1:s===`\\`&&(e[o+1]===`"`||e[o+1]===`\\`)?(n+=e[o+1],o+=1):n+=s,r=!0):s===`'`?(i=!0,r=!0):s===`"`?(a=!0,r=!0):s===`\\`?o+1<e.length&&(n+=e[o+1],o+=1,r=!0):s===` `||s===` `||s===`
107
- `||s===`\r`?r&&=(t.push(n),n=``,!1):(n+=s,r=!0)}if(i||a)throw new g(`INVALID_INPUT`,`Unbalanced quote in agent command.`);return r&&t.push(n),t}function Ml(e,t){let n=jl(e);if(n.length===0)throw new g(`INVALID_INPUT`,`Agent command must not be empty.`);let r=n.some(e=>e.includes(`{prompt}`));return{argv:r?n.map(e=>e.split(`{prompt}`).join(t)):n,usesPlaceholder:r}}function Nl(e,t){return e?`${e.trim()}\n\n---\n\nUser request:\n${t}`:t}function Pl(e,t,n,r=!1){let i=[`You are judging whether an AI agent response satisfies a list of assertions.`,`Judge only against the assertions. Be strict but fair.`,``,`Original request:`,e,``,`Agent response:`,t,``,`Assertions to evaluate, in this exact order:`,`<assertions>`,n.map((e,t)=>`${t+1}. ${e}`).join(`
98
+ `),sa(nl(t.inputSchema)),process.stdout.write(`
99
+ `),$i({title:`Need the exact schema?`,command:`seo reports describe ${t.id} --json`}))}}),al=b({meta:{name:`run`,description:`Run one report with JSON parameters`},args:{id:{type:`positional`,required:!0,description:"Report id from `seo reports list`."},params:{type:`string`,description:`Report parameters as a JSON object.`},"params-file":{type:`string`,description:`Read report parameters from a JSON file.`},"actions-only":{type:`boolean`,default:!1,description:`Return findings, verification, and retained inventories without the full report body.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.id)??``,n=await el(e.params,e[`params-file`]),r=await _t(t,n,{view:E(e[`actions-only`])?`actions`:`full`});if(tl(r),j(e))L(r.structuredContent??{});else for(let e of r.content)process.stdout.write(`${e.text}\n`)}}),ol=b({meta:{name:`reports`,description:`Discover and run the complete report catalog`},subCommands:{list:rl,describe:il,run:al}}),sl=b({meta:{name:`rules`,description:`List crawler rule ids and default guidance`},args:{category:{type:`string`,description:`Filter rules by category.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.category),n=Gn().filter(e=>!t||e.category===t);j(e)?L({rules:n}):ra(n.map(e=>({id:e.id,category:e.category,name:`${e.title} · ${e.defaultSeverity}`})),{noun:`rule`})}}),cl=b({meta:{name:`explain`,description:`Explain one crawler rule in plain English`},args:{rule:{type:`string`,required:!0,description:`Rule id to explain, for example missing_title.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=T(e.rule)??``,n=d(t);if(!n)throw Error(`Unknown rule: ${t}`);j(e)?L(n):(R([[`Rule`,n.id],[`Title`,n.title],[`Category`,n.category],[`Severity`,n.defaultSeverity]]),process.stdout.write(`
100
+ `),aa(`Why it matters`,n.whyItMatters),process.stdout.write(`
101
+ `),aa(`How to fix`,n.howToFix),process.stdout.write(`
102
+ `),aa(`Impact if ignored`,n.impactIfIgnored),process.stdout.write(`
103
+ `),aa(`How to verify`,n.howToVerify))}});function ll(){return fl(`skills`,process.env.SEO_SKILLS_DIR)}function ul(){return fl(`evals`,process.env.SEO_EVALS_DIR)}function dl(){return fl(`site-kit`)}function fl(e,t){if(t)return fr(t);let n=lr(Er(import.meta.url));for(let t=0;t<6;t+=1){let t=_(n,e);if(v(_(t,`README.md`)))return t;n=lr(n)}throw new g(`INTERNAL_ERROR`,`Packaged ${e} could not be found. Reinstall \`seo\` and try again.`)}function pl(e){return yr(_(e,`SKILL.md`),`utf8`).match(/^description:\s*(.+)$/m)?.[1]?.trim()??``}const ml=[[`setup`,`Sign in to the accounts the site needs, one at a time`],[`market`,`Choose a market with real demand and data you can own`],[`data`,`Get, clean and keep the dataset`],[`plan`,`Plan the pages from what people search for`],[`build`,`Start the site and deploy a private preview`],[`golden-page`,`Build one page that is the best answer on the web`],[`writing`,`Write copy people enjoy and AI answers can quote`],[`scale`,`Turn the golden page into every page, with gates`],[`launch`,`Go live and get the important pages indexed`],[`grow`,`Measure with your own data and improve every week`]],hl=/* @__PURE__ */ new Set([`node_modules`,`dist`,`.astro`,`.wrangler`]);function gl(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,`-`).replace(/^-|-$/g,``)}function _l(e){return gl(cr(e)).split(`-`).filter(Boolean).map(e=>`${e[0]?.toUpperCase()}${e.slice(1)}`).join(` `)}function vl(e){let t=(e??`example.com`).trim().toLowerCase().replace(/^https?:\/\//,``).replace(/\/.*$/,``);if(!/^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/.test(t))throw new g(`INVALID_INPUT`,`--domain must be a domain such as example.com, not ${e}.`);return t}async function yl(e,t){let n=JSON.parse(await y(e,`utf8`));t(n),await Nr(e,`${JSON.stringify(n,null,2)}\n`)}function bl(){return Zo(`pnpm`,[`--version`],{stdio:`ignore`}).status===0?`pnpm`:`npm`}const xl=b({meta:{name:`new`,description:`Copy the Astro on Cloudflare starter into a new folder`},args:{folder:{type:`positional`,required:!0,description:`Folder to create. It must not exist or must be empty.`},name:{type:`string`,description:`Site name. Defaults to the folder name.`},domain:{type:`string`,description:`Production domain. Defaults to example.com until you buy one.`},install:{type:`boolean`,default:!0,description:`Install packages after copying.`,negativeDescription:`Copy the files only.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=fr(String(e.folder));if(v(n)&&(await Ar(n)).some(e=>e!==`.git`))throw new g(`INVALID_INPUT`,`${n} is not empty. Choose a new folder for the site.`);let r=T(e.name)?.trim()||_l(n),i=vl(T(e.domain)),a=gl(cr(n))||`site`,o=ir(16).toString(`hex`),s=_(dl(),`starter`);await Dr(s,n,{recursive:!0,filter:e=>!dr(s,e).split(pr).some(e=>hl.has(e))}),await jr(_(n,`gitignore`),_(n,`.gitignore`)),await yl(_(n,`site.config.json`),e=>{e.name=r,e.domain=i,e.indexNowKey=o}),await Nr(_(n,`public`,`${o}.txt`),o),await yl(_(n,`package.json`),e=>{e.name=a;let t=e.devDependencies;t.seo=`^${Ln}`});let c=_(n,`wrangler.jsonc`);await Nr(c,(await y(c,`utf8`)).replace(`"name": "example-site"`,`"name": ${JSON.stringify(a)}`));let l=_(n,`README.md`);await Nr(l,(await y(l,`utf8`)).replace(/^# .*$/m,`# ${r}`));let u=D(e,`install`)===!0?void 0:bl(),d=!1;u&&(t||process.stdout.write(`Installing packages with ${u}...\n`),d=Zo(u,[`install`],{cwd:n,stdio:t?`ignore`:`inherit`}).status===0);let f=[`cd ${dr(process.cwd(),n)||`.`}`,`seo site guide setup`];t?L({folder:n,name:r,domain:i,installed:d,packageManager:u??null,next:f}):(ea(`Created ${r}`,n),R([[`Domain`,i],[`Packages`,d?`installed with ${u}`:u?`${u} install failed. Run it in the folder.`:`not installed`]]),process.stdout.write(`
104
+ `),aa(`What is in it`,"An example site about parks, built from src/data. It already passes `seo site check`: titles, canonicals, JSON-LD, sharing images, Markdown copies, llms.txt and sitemaps are done for every page.",`Later steps replace the example data and pages with yours.`),$i({title:`Next`,body:`Read the first step. Each step ends with the next one.`,command:f.join(` && `)}))}});function Sl(e){let t=gl(e.join(` `));return ml.find(([e])=>e===t)?.[0]}const Cl=b({meta:{name:`guide`,description:`Print the instructions for one step of building the site`},args:{step:{type:`positional`,required:!1,description:ml.map(([e])=>e).join(`, `)},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=e._?.length?e._:[T(e.step)??``];if(!n.join(``).trim()){if(t){L({steps:ml.map(([e,t])=>({step:e,summary:t}))});return}ea(`Build a site, one step at a time`,"Run `seo site guide <step>` and follow it before the next step."),oa(`Steps`,ml.map(([e,t])=>`${e}: ${t}`)),$i({title:`Start here`,command:`seo site guide setup`});return}let r=Sl(n);if(!r)throw new g(`INVALID_INPUT`,`Unknown step "${n.join(` `)}". Steps: ${ml.map(([e])=>e).join(`, `)}.`);let i=await y(_(dl(),`guide`,`${r}.md`),`utf8`),a=ml.findIndex(([e])=>e===r),o=ml[a+1]?.[0];t?L({step:r,number:a+1,of:ml.length,markdown:i,next:o?`seo site guide ${o}`:null}):process.stdout.write(i.endsWith(`
105
+ `)?i:`${i}\n`)}});async function wl(e){return(await Ar(e,{withFileTypes:!0,recursive:!0})).filter(e=>e.isFile()).map(t=>dr(e,_(t.parentPath,t.name)).split(pr).join(`/`))}function Tl(e){ea(e.status===`passed`?`Site check passed`:e.status===`partial`?`Site check stopped early`:`Site check failed`,e.origin),R([[`Pages`,e.pagesChecked===e.pagesFound?String(e.pagesFound):`${e.pagesChecked} of ${e.pagesFound} checked`],[`Failures`,String(e.failures)],[`Warnings`,String(e.warnings)]]),process.stdout.write(`
106
+ `);for(let t of e.rules.slice(0,40)){let n=e.issues.filter(e=>e.rule===t.rule).slice(0,3).map(e=>e.found?`${e.file}: ${e.found}`:e.file);oa(`${t.level===`fail`?`Fail`:`Warn`} ${t.rule} (${t.group}, ${t.count}). ${t.fix}`,t.count>n.length?[...n,`and ${t.count-n.length} more`]:n)}e.rules.length>40&&process.stdout.write(`${e.rules.length-40} more rules failed. Use --json for all of them.\n\n`),e.status===`partial`&&aa(`Not every page was checked`,`The check stops at ${e.pagesChecked} pages, so it cannot say the rest pass. Split the site or build fewer, stronger pages.`),$i(e.status===`passed`?{title:`Next`,body:`Deploy the build, or read the next step of the guide.`,command:`seo site guide`}:{title:`Next`,body:`Fix the template or the data behind the first rule, then build again.`,command:`pnpm build`})}const El=b({meta:{name:`check`,description:`Check the built site and fail on any rule it breaks`},args:{dir:{type:`string`,description:`Site folder with site.config.json. Defaults to this folder.`},dist:{type:`string`,description:`Build folder. Defaults to dist inside the site folder.`},domain:{type:`string`,description:`Production domain. Defaults to the one in site.config.json.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=fr(T(e.dir)??`.`),r=fr(n,T(e.dist)??`dist`),i=_(n,`site.config.json`),a=v(i)?JSON.parse(await y(i,`utf8`)):{},o=T(e.domain)??a.domain;if(!o)throw new g(`INVALID_INPUT`,"No domain. Run this in a folder made by `seo site new`, or pass --domain.");if(!v(r))throw new g(`INVALID_INPUT`,`${r} does not exist. Build the site first with \`pnpm build\`.`);let s=await Dt({origin:`https://${vl(o)}`,files:await wl(r),readText:e=>y(_(r,e),`utf8`),readBytes:e=>y(_(r,e)),allowWords:a.allowWords??[],sources:v(_(n,`src`))?{files:(await wl(_(n,`src`))).map(e=>`src/${e}`),readText:e=>y(_(n,e),`utf8`)}:void 0});t?L(s):Tl(s),s.status!==`passed`&&(process.exitCode=1)}}),Dl=b({meta:{name:`site`,description:`Build a data-driven site on Astro and Cloudflare`},subCommands:{new:xl,guide:Cl,check:El}}),Ol=`claude -p`;function kl(){let e=ul();return br(e,{withFileTypes:!0}).filter(e=>e.isFile()&&e.name.endsWith(`.json`)).map(t=>({subject:cr(t.name,`.json`),path:_(e,t.name)})).sort((e,t)=>e.subject<t.subject?-1:+(e.subject>t.subject))}function Al(e,t){let n;try{n=yr(t,`utf8`)}catch{throw new g(`INVALID_INPUT`,`Cannot read eval file for ${e}.`)}let r;try{r=JSON.parse(n)}catch{throw new g(`INVALID_INPUT`,`Eval file ${e}.json is not valid JSON.`)}if(!r||typeof r!=`object`||Array.isArray(r))throw new g(`INVALID_INPUT`,`Eval file ${e}.json must be an object.`);let i=r;if(i.subject!==e)throw new g(`INVALID_INPUT`,`Eval file ${e}.json must set subject to "${e}".`);if(!Array.isArray(i.evals)||i.evals.length===0)throw new g(`INVALID_INPUT`,`Eval file ${e}.json must have a non-empty evals array.`);let a=/* @__PURE__ */ new Set;return{subject:e,evals:i.evals.map((t,n)=>{let r=t,i=`${e}.json eval ${n+1}`;if(!Number.isInteger(r?.id))throw new g(`INVALID_INPUT`,`${i}: id must be an integer.`);let o=r.id;if(a.has(o))throw new g(`INVALID_INPUT`,`${i}: duplicate id ${o}.`);if(a.add(o),typeof r.prompt!=`string`||r.prompt.trim().length===0)throw new g(`INVALID_INPUT`,`${i}: prompt must be a non-empty string.`);if(typeof r.expected_output!=`string`||r.expected_output.trim().length===0)throw new g(`INVALID_INPUT`,`${i}: expected_output must be a non-empty string.`);if(!Array.isArray(r.assertions)||r.assertions.length===0||r.assertions.some(e=>typeof e!=`string`||e.trim().length===0))throw new g(`INVALID_INPUT`,`${i}: assertions must be a non-empty array of non-empty strings.`);if(r.files!==void 0&&!Array.isArray(r.files))throw new g(`INVALID_INPUT`,`${i}: files must be an array.`);return{id:o,prompt:r.prompt,expected_output:r.expected_output,assertions:r.assertions,files:r.files??[]}})}}function jl(){let e=_(ll(),`seo`,`SKILL.md`);if(!v(e))throw new g(`INTERNAL_ERROR`,"The seo router skill could not be found. Reinstall `seo` or use --no-skill.");return yr(e,`utf8`)}function Ml(e){let t=[],n=``,r=!1,i=!1,a=!1;for(let o=0;o<e.length;o+=1){let s=e[o];i?(s===`'`?i=!1:n+=s,r=!0):a?(s===`"`?a=!1:s===`\\`&&(e[o+1]===`"`||e[o+1]===`\\`)?(n+=e[o+1],o+=1):n+=s,r=!0):s===`'`?(i=!0,r=!0):s===`"`?(a=!0,r=!0):s===`\\`?o+1<e.length&&(n+=e[o+1],o+=1,r=!0):s===` `||s===` `||s===`
107
+ `||s===`\r`?r&&=(t.push(n),n=``,!1):(n+=s,r=!0)}if(i||a)throw new g(`INVALID_INPUT`,`Unbalanced quote in agent command.`);return r&&t.push(n),t}function Nl(e,t){let n=Ml(e);if(n.length===0)throw new g(`INVALID_INPUT`,`Agent command must not be empty.`);let r=n.some(e=>e.includes(`{prompt}`));return{argv:r?n.map(e=>e.split(`{prompt}`).join(t)):n,usesPlaceholder:r}}function Pl(e,t){return e?`${e.trim()}\n\n---\n\nUser request:\n${t}`:t}function Fl(e,t,n,r=!1){let i=[`You are judging whether an AI agent response satisfies a list of assertions.`,`Judge only against the assertions. Be strict but fair.`,``,`Original request:`,e,``,`Agent response:`,t,``,`Assertions to evaluate, in this exact order:`,`<assertions>`,n.map((e,t)=>`${t+1}. ${e}`).join(`
108
108
  `),`</assertions>`,``,`Judge outcomes, not narration. An assertion about reading, checking, or`,`following fields or steps passes when the reply demonstrably used that`,`information or produced the result it protects, even if the reply never`,`names the field or step. Fail it only when the reply contradicts it or`,`shows the work was not done.`,``,`Reply with STRICT JSON only. No prose, no markdown, no code fence:`,`{"results":[{"assertion":"<assertion text>","pass":true,"reason":"<short reason>"}]}`,`Return exactly ${n.length} result${n.length===1?``:`s`}, one per assertion, in the same order.`].join(`
109
- `);return r?`${i}\n\nYour previous reply could not be parsed as the required JSON with one result per assertion. Reply again with STRICT JSON only, in the exact shape shown, and nothing else.`:i}function Fl(e){let t=e.indexOf(`{`);if(t===-1)return;let n=0,r=!1,i=!1;for(let a=t;a<e.length;a+=1){let o=e[a];if(r)i?i=!1:o===`\\`?i=!0:o===`"`&&(r=!1);else if(o===`"`)r=!0;else if(o===`{`)n+=1;else if(o===`}`&&(--n,n===0))try{return JSON.parse(e.slice(t,a+1))}catch{return}}}function Il(e,t){let n=Fl(e);if(!n||typeof n!=`object`)return{error:`Judge reply did not contain a JSON object.`};let r=n.results;return Array.isArray(r)?r.length===t.length?{verdicts:t.map((e,t)=>{let n=r[t];return{assertion:e,verdict:n?.pass===!0?`pass`:`fail`,reason:typeof n?.reason==`string`&&n.reason.trim().length>0?n.reason.trim():`No reason given.`,source:`judge verdict`}})}:{error:`Judge returned ${r.length} results for ${t.length} assertions.`}:{error:`Judge reply had no results array.`}}function Ll(e,t){return new Promise(n=>{let r=Date.now(),[i,...a]=e,o;try{o=Fr(i,a,{stdio:[`pipe`,`pipe`,`pipe`]})}catch(e){n({stdout:``,stderr:``,code:null,timedOut:!1,spawnError:e instanceof Error?e.message:String(e),durationMs:Date.now()-r});return}let s=``,c=``,l=!1,u=!1,d,f=setTimeout(()=>{u=!0,o.kill(`SIGTERM`),d=setTimeout(()=>{try{o.kill(`SIGKILL`)}catch{}},2e3),d.unref()},t.timeoutMs),ee=e=>{l||(l=!0,clearTimeout(f),d&&clearTimeout(d),n({...e,durationMs:Date.now()-r}))};o.stdout?.on(`data`,e=>{s+=e}),o.stderr?.on(`data`,e=>{c+=e}),o.on(`error`,e=>{ee({stdout:s,stderr:c,code:null,timedOut:u,spawnError:e.message})}),o.on(`close`,e=>{ee({stdout:s,stderr:c,code:e,timedOut:u})}),o.stdin&&(t.input===void 0?o.stdin.end():o.stdin.end(t.input))})}async function Rl(e,t,n){let{argv:r,usesPlaceholder:i}=Ml(e,t);return Ll(r,{input:i?void 0:t,timeoutMs:n})}async function zl(e,t){let n=Date.now(),r=Nl(t.skillBody,e.prompt),i=await Rl(t.agent,r,t.timeoutMs),a=i.stdout.trim(),o={id:e.id,prompt:e.prompt,reply:a,agentDurationMs:i.durationMs};if(i.spawnError)return{...o,status:`errored`,error:`Agent command failed to start: ${i.spawnError}`,durationMs:Date.now()-n,assertions:Bl(e.assertions)};if(i.timedOut)return{...o,status:`errored`,error:`Agent timed out after ${Math.round(t.timeoutMs/1e3)}s.`,durationMs:Date.now()-n,assertions:Bl(e.assertions)};if(a.length===0)return{...o,status:`errored`,error:`Agent returned no output on stdout.`,durationMs:Date.now()-n,assertions:Bl(e.assertions)};if(!t.judge)return{...o,status:`unjudged`,durationMs:Date.now()-n,assertions:Bl(e.assertions)};let s=0,c=``;for(let r=0;r<2;r+=1){let i=Pl(e.prompt,a,e.assertions,r>0),l=await Rl(t.judge,i,t.timeoutMs);if(s+=l.durationMs,l.spawnError)return{...o,status:`errored`,error:`Judge command failed to start: ${l.spawnError}`,judgeDurationMs:s,durationMs:Date.now()-n,assertions:Bl(e.assertions)};if(l.timedOut)return{...o,status:`errored`,error:`Judge timed out after ${Math.round(t.timeoutMs/1e3)}s.`,judgeDurationMs:s,durationMs:Date.now()-n,assertions:Bl(e.assertions)};let u=Il(l.stdout,e.assertions);if(`verdicts`in u){let e=u.verdicts.some(e=>e.verdict===`fail`);return{...o,status:e?`failed`:`passed`,judgeDurationMs:s,durationMs:Date.now()-n,assertions:u.verdicts}}c=u.error}return{...o,status:`errored`,error:`Judge output could not be parsed after a retry: ${c}`,judgeDurationMs:s,durationMs:Date.now()-n,assertions:Bl(e.assertions)}}function Bl(e){return e.map(e=>({assertion:e,verdict:`unjudged`}))}async function Vl(e,t,n){let r=Array(e.length),i=0,a=async()=>{for(;;){let t=i;if(i+=1,t>=e.length)return;r[t]=await n(e[t],t)}},o=Math.max(1,Math.min(t,e.length));return await Promise.all(Array.from({length:o},()=>a())),r}function Hl(e){let t={passed:0,failed:0,errored:0,unjudged:0};for(let n of e)t[n.status]+=1;return t}function Ul(e){let t=[],n=w(e.subject);n&&t.push(n);let r=e._;if(Array.isArray(r))for(let e of r)typeof e==`string`&&e.length>0&&t.push(e);return[...new Set(t)]}function Wl(e){let t=Ol();if(e.length===0)return t;let n=new Map(t.map(e=>[e.subject,e]));return e.map(e=>{let r=n.get(e);if(!r){let n=t.map(e=>e.subject).join(`, `);throw new g(`INVALID_INPUT`,`Unknown eval subject: ${e}.${n?` Known subjects: ${n}.`:``}`)}return r})}function Gl(e){let t=Ol().map(e=>{let t=kl(e.subject,e.path);return{subject:e.subject,evals:t.evals.length}});if(e){I({schemaVersion:1,subjects:t});return}if(t.length===0){process.stdout.write(`No eval subjects found.
109
+ `);return r?`${i}\n\nYour previous reply could not be parsed as the required JSON with one result per assertion. Reply again with STRICT JSON only, in the exact shape shown, and nothing else.`:i}function Il(e){let t=e.indexOf(`{`);if(t===-1)return;let n=0,r=!1,i=!1;for(let a=t;a<e.length;a+=1){let o=e[a];if(r)i?i=!1:o===`\\`?i=!0:o===`"`&&(r=!1);else if(o===`"`)r=!0;else if(o===`{`)n+=1;else if(o===`}`&&(--n,n===0))try{return JSON.parse(e.slice(t,a+1))}catch{return}}}function Ll(e,t){let n=Il(e);if(!n||typeof n!=`object`)return{error:`Judge reply did not contain a JSON object.`};let r=n.results;return Array.isArray(r)?r.length===t.length?{verdicts:t.map((e,t)=>{let n=r[t];return{assertion:e,verdict:n?.pass===!0?`pass`:`fail`,reason:typeof n?.reason==`string`&&n.reason.trim().length>0?n.reason.trim():`No reason given.`,source:`judge verdict`}})}:{error:`Judge returned ${r.length} results for ${t.length} assertions.`}:{error:`Judge reply had no results array.`}}function Rl(e,t){return new Promise(n=>{let r=Date.now(),[i,...a]=e,o;try{o=Ir(i,a,{stdio:[`pipe`,`pipe`,`pipe`]})}catch(e){n({stdout:``,stderr:``,code:null,timedOut:!1,spawnError:e instanceof Error?e.message:String(e),durationMs:Date.now()-r});return}let s=``,c=``,l=!1,u=!1,d,f=setTimeout(()=>{u=!0,o.kill(`SIGTERM`),d=setTimeout(()=>{try{o.kill(`SIGKILL`)}catch{}},2e3),d.unref()},t.timeoutMs),ee=e=>{l||(l=!0,clearTimeout(f),d&&clearTimeout(d),n({...e,durationMs:Date.now()-r}))};o.stdout?.on(`data`,e=>{s+=e}),o.stderr?.on(`data`,e=>{c+=e}),o.on(`error`,e=>{ee({stdout:s,stderr:c,code:null,timedOut:u,spawnError:e.message})}),o.on(`close`,e=>{ee({stdout:s,stderr:c,code:e,timedOut:u})}),o.stdin&&(t.input===void 0?o.stdin.end():o.stdin.end(t.input))})}async function zl(e,t,n){let{argv:r,usesPlaceholder:i}=Nl(e,t);return Rl(r,{input:i?void 0:t,timeoutMs:n})}async function Bl(e,t){let n=Date.now(),r=Pl(t.skillBody,e.prompt),i=await zl(t.agent,r,t.timeoutMs),a=i.stdout.trim(),o={id:e.id,prompt:e.prompt,reply:a,agentDurationMs:i.durationMs};if(i.spawnError)return{...o,status:`errored`,error:`Agent command failed to start: ${i.spawnError}`,durationMs:Date.now()-n,assertions:Vl(e.assertions)};if(i.timedOut)return{...o,status:`errored`,error:`Agent timed out after ${Math.round(t.timeoutMs/1e3)}s.`,durationMs:Date.now()-n,assertions:Vl(e.assertions)};if(a.length===0)return{...o,status:`errored`,error:`Agent returned no output on stdout.`,durationMs:Date.now()-n,assertions:Vl(e.assertions)};if(!t.judge)return{...o,status:`unjudged`,durationMs:Date.now()-n,assertions:Vl(e.assertions)};let s=0,c=``;for(let r=0;r<2;r+=1){let i=Fl(e.prompt,a,e.assertions,r>0),l=await zl(t.judge,i,t.timeoutMs);if(s+=l.durationMs,l.spawnError)return{...o,status:`errored`,error:`Judge command failed to start: ${l.spawnError}`,judgeDurationMs:s,durationMs:Date.now()-n,assertions:Vl(e.assertions)};if(l.timedOut)return{...o,status:`errored`,error:`Judge timed out after ${Math.round(t.timeoutMs/1e3)}s.`,judgeDurationMs:s,durationMs:Date.now()-n,assertions:Vl(e.assertions)};let u=Ll(l.stdout,e.assertions);if(`verdicts`in u){let e=u.verdicts.some(e=>e.verdict===`fail`);return{...o,status:e?`failed`:`passed`,judgeDurationMs:s,durationMs:Date.now()-n,assertions:u.verdicts}}c=u.error}return{...o,status:`errored`,error:`Judge output could not be parsed after a retry: ${c}`,judgeDurationMs:s,durationMs:Date.now()-n,assertions:Vl(e.assertions)}}function Vl(e){return e.map(e=>({assertion:e,verdict:`unjudged`}))}async function Hl(e,t,n){let r=Array(e.length),i=0,a=async()=>{for(;;){let t=i;if(i+=1,t>=e.length)return;r[t]=await n(e[t],t)}},o=Math.max(1,Math.min(t,e.length));return await Promise.all(Array.from({length:o},()=>a())),r}function Ul(e){let t={passed:0,failed:0,errored:0,unjudged:0};for(let n of e)t[n.status]+=1;return t}function Wl(e){let t=[],n=T(e.subject);n&&t.push(n);let r=e._;if(Array.isArray(r))for(let e of r)typeof e==`string`&&e.length>0&&t.push(e);return[...new Set(t)]}function Gl(e){let t=kl();if(e.length===0)return t;let n=new Map(t.map(e=>[e.subject,e]));return e.map(e=>{let r=n.get(e);if(!r){let n=t.map(e=>e.subject).join(`, `);throw new g(`INVALID_INPUT`,`Unknown eval subject: ${e}.${n?` Known subjects: ${n}.`:``}`)}return r})}function Kl(e){let t=kl().map(e=>{let t=Al(e.subject,e.path);return{subject:e.subject,evals:t.evals.length}});if(e){L({schemaVersion:1,subjects:t});return}if(t.length===0){process.stdout.write(`No eval subjects found.
110
110
  `);return}let n=Math.max(...t.map(e=>e.subject.length));process.stdout.write(`Eval subjects
111
- `);for(let e of t){let t=e.evals===1?`eval`:`evals`;process.stdout.write(` ${e.subject.padEnd(n)} ${e.evals} ${t}\n`)}}function Kl(e){return e===`passed`?`PASS`:e===`failed`?`FAIL`:e===`errored`?`ERROR`:`UNJUDGED`}function ql(e,t){t.judged?process.stdout.write(`Assertion verdicts below are judge verdicts, model judgments from the judge agent, not ground truth.
111
+ `);for(let e of t){let t=e.evals===1?`eval`:`evals`;process.stdout.write(` ${e.subject.padEnd(n)} ${e.evals} ${t}\n`)}}function ql(e){return e===`passed`?`PASS`:e===`failed`?`FAIL`:e===`errored`?`ERROR`:`UNJUDGED`}function Jl(e,t){t.judged?process.stdout.write(`Assertion verdicts below are judge verdicts, model judgments from the judge agent, not ground truth.
112
112
 
113
113
  `):process.stdout.write(`Judging was skipped. Review each agent reply against the assertion checklist by hand.
114
114
 
115
- `);for(let n of e){process.stdout.write(`# ${n.subject}\n`);for(let e of n.evals)if(process.stdout.write(`\n[${Kl(e.status)}] eval ${e.id} (${e.durationMs}ms)\n`),process.stdout.write(` prompt: ${Yl(e.prompt,100)}\n`),e.error&&process.stdout.write(` error: ${e.error}\n`),!t.judged){process.stdout.write(` reply: ${Yl(e.reply,200)}\n`),process.stdout.write(` assertions to review:
116
- `);for(let t of e.assertions)process.stdout.write(` - [ ] ${t.assertion}\n`)}else for(let t of e.assertions){let e=t.verdict===`pass`?`pass`:t.verdict===`fail`?`fail`:`unjudged`;process.stdout.write(` - ${e}: ${t.assertion}\n`),t.reason&&process.stdout.write(` judge verdict: ${t.reason}\n`)}let e=Hl(n.evals);process.stdout.write(`\nSubject ${n.subject}: ${e.passed} passed, ${e.failed} failed, ${e.errored} errored${e.unjudged?`, ${e.unjudged} unjudged`:``}.\n\n`)}let n=Jl(e);process.stdout.write(`Totals: ${n.passed} passed, ${n.failed} failed, ${n.errored} errored${n.unjudged?`, ${n.unjudged} unjudged`:``} across ${n.evals} evals.\n`)}function Jl(e){let t={passed:0,failed:0,errored:0,unjudged:0,evals:0};for(let n of e)t.passed+=n.passed,t.failed+=n.failed,t.errored+=n.errored,t.unjudged+=n.unjudged,t.evals+=n.evals.length;return t}function Yl(e,t){let n=e.replace(/\s+/g,` `).trim();return n.length>t?`${n.slice(0,t-1)}…`:n}const Xl=b({meta:{name:`eval`,description:`Run behaviour evals for the seo skill against a local agent and judge the results`},args:{subject:{type:`positional`,required:!1,description:`Eval subject (report id or job). Omit to run every subject.`},list:{type:`boolean`,default:!1,description:`List eval subjects with their eval counts and exit.`},id:{type:`string`,description:`Run one eval id within a single subject.`},agent:{type:`string`,default:Dl,description:`Agent command. Use {prompt} to place the prompt in argv, otherwise it is sent on stdin.`},"judge-agent":{type:`string`,description:`Judge command. Defaults to the same command as --agent.`},judge:ui(`Judge assertions with an agent.`,`Skip judging and print replies with an assertion checklist.`),skill:ui(`Send the seo router skill as context before the prompt.`,`Omit the seo router skill context.`),timeout:{type:`string`,default:`300`,description:`Timeout in seconds per agent or judge call.`},concurrency:{type:`string`,default:`1`,description:`Evals to run in parallel (max 4).`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e);if(e.list===!0){Gl(t);return}let n=Wl(Ul(e)),r=D(e.id);if(r!==void 0&&n.length!==1)throw new g(`INVALID_INPUT`,`--id needs exactly one subject. Name the subject to filter.`);let i=D(e.timeout)??300;if(!(i>0))throw new g(`INVALID_INPUT`,`--timeout must be a positive number of seconds.`);let a=Math.max(1,Math.min(4,D(e.concurrency)??1)),o=w(e.agent)??Dl,s=e.judge===!1?null:w(e[`judge-agent`])??o,c=e.skill===!1?null:Al(),l={agent:o,judge:s,skillBody:c,timeoutMs:i*1e3},u=[];for(let e of n){let n=kl(e.subject,e.path).evals;if(r!==void 0&&(n=n.filter(e=>e.id===r),n.length===0))throw new g(`INVALID_INPUT`,`No eval with id ${r} in subject ${e.subject}.`);t||process.stderr.write(`Running ${n.length} eval${n.length===1?``:`s`} for ${e.subject}...\n`);let i=await Vl(n,a,e=>zl(e,l)),o=Hl(i);u.push({subject:e.subject,...o,evals:i})}let d=Jl(u),f=d.failed>0||d.errored>0;t?I({schemaVersion:1,agent:o,judge:s,judged:s!==null,skillContext:c!==null,timeoutSeconds:i,concurrency:a,subjects:u,totals:d}):ql(u,{judged:s!==null,agent:o,judge:s}),f&&(process.exitCode=1)}});function Zl(){let e=cl();return yr(e,{withFileTypes:!0}).filter(t=>t.isDirectory()&&v(_(e,t.name,`SKILL.md`))).map(t=>({name:t.name,description:fl(_(e,t.name)),path:_(e,t.name)})).sort((e,t)=>e.name<t.name?-1:+(e.name>t.name))}function Ql(e){let t=Zl();if(!e)return t;let n=t.find(t=>t.name===e);if(!n)throw new g(`INVALID_INPUT`,`Unknown skill: ${e}.`);return[n]}const $l=b({meta:{name:`list`,description:`Show the packaged SEO skill`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:({args:e})=>{let t=Zl();if(A(e))I({skills:t});else{$i(`SEO skill`,`Packaged with this CLI.`),process.stdout.write(`
117
- `);for(let e of t)L([[`Name`,e.name],[`Purpose`,`Helps agents choose and run the right SEO audit or report.`],[`Path`,e.path]]);process.stdout.write(`
118
- `),z({title:`Add it to an agent`,body:`Run setup to install the skill and connect your first site.`,command:`seo start`})}}}),eu=b({meta:{name:`path`,description:`Print a packaged agent skill path`},args:{name:{type:`positional`,description:`Skill name. Omit it for the skill directory.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:({args:e})=>{let t=w(e.name),n=t?Ql(t)[0]?.path:cl();A(e)?I({name:t,path:n}):process.stdout.write(`${n}\n`)}}),tu=b({meta:{name:`install`,description:`Install the SEO skill for coding agents`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:({args:e})=>{let t=A(e),n=Ps({quiet:t});if(t)I(n),n.status===`failed`&&(process.exitCode=1);else{if(n.status===`failed`)throw new g(`INTERNAL_ERROR`,`Skill install failed: ${n.error}`);process.stdout.write(`SEO skill installed. Agents that support skills can discover it now.
119
- `)}}}),nu=b({meta:{name:`skill`,description:`Inspect the packaged SEO skill and run its evals`},subCommands:{list:$l,path:eu,install:tu,eval:Xl}}),ru=b({meta:{name:`privacy`,description:`Show local config, token, cache, and log storage paths`},run:async()=>{let e=Se(),t=$t();L(e.map(e=>[e.label,`${e.path} · ${sa(e.sizeBytes)} · ${e.mode}`])),process.stdout.write(`
120
- `),L(Object.entries(t.counts).map(([e,t])=>[e,String(t)]))}}),iu=b({meta:{name:`reset`,description:`Delete saved credentials and all local data`},args:{yes:{type:`boolean`,default:!1,description:`Confirm deletion without prompting.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e);if(!e.yes&&!F({json:t}))throw new g(`INVALID_INPUT`,`Cannot prompt here. Pass --yes to confirm reset.`);e.yes||B(await Br({message:`Delete saved credentials and all local data?`,initialValue:!1}))?(await Ln(),t?I({reset:!0}):process.stdout.write(`Reset complete.
121
- `)):zr(`Reset aborted.`)}}),au=b({meta:{name:`sites`,description:`List Search Console properties available to this Google login`},args:{json:{type:`boolean`,default:!1},refresh:{type:`boolean`,default:!1},account:{type:`string`,description:`Saved Google account email.`}},run:async({args:e})=>{let t=await $n(T(e.refresh),w(e.account));A(e)?I({sites:t}):R([`Property`,`Permission`],t.map(e=>[e.siteUrl,e.permissionLevel??`unknown`]))}}),ou=b({meta:{name:`updates`,description:`List official Google Search Status ranking updates`},args:{product:{type:`string`,default:`Ranking`},limit:{type:`string`,default:`10`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=await Ve({product:w(e.product),limit:D(e.limit)});A(e)?I({updates:t}):ra(t.map(e=>({title:e.name,meta:[e.type,e.status,`${e.start.slice(0,10)} to ${e.end?.slice(0,10)??`present`}`]})),{empty:`No matching Google Search updates.`})}}),su={ci:`disabled automatically in CI or tests`,do_not_track:`disabled by DO_NOT_TRACK`,environment:`disabled by a telemetry environment variable`,local_setting:`disabled in local settings`,invalid_state:`disabled because the local state file is invalid`};function cu(e){return{enabled:e.enabled,...e.reason?{reason:e.reason}:{},stateFile:e.stateFile,...e.state?{firstRunAt:e.state.firstRunAt,cohort:e.state.cohort,sentMilestones:e.state.sentMilestones}:{}}}function lu(e,t){let n=cu(e);t?I(n):(L([[`Anonymous telemetry`,e.enabled?`enabled`:`disabled`],[`Reason`,e.reason?su[e.reason]:`local setting`],[`State file`,e.stateFile],[`Install cohort`,e.state?.cohort??`not created`]]),process.stdout.write(`
115
+ `);for(let n of e){process.stdout.write(`# ${n.subject}\n`);for(let e of n.evals)if(process.stdout.write(`\n[${ql(e.status)}] eval ${e.id} (${e.durationMs}ms)\n`),process.stdout.write(` prompt: ${Xl(e.prompt,100)}\n`),e.error&&process.stdout.write(` error: ${e.error}\n`),!t.judged){process.stdout.write(` reply: ${Xl(e.reply,200)}\n`),process.stdout.write(` assertions to review:
116
+ `);for(let t of e.assertions)process.stdout.write(` - [ ] ${t.assertion}\n`)}else for(let t of e.assertions){let e=t.verdict===`pass`?`pass`:t.verdict===`fail`?`fail`:`unjudged`;process.stdout.write(` - ${e}: ${t.assertion}\n`),t.reason&&process.stdout.write(` judge verdict: ${t.reason}\n`)}let e=Ul(n.evals);process.stdout.write(`\nSubject ${n.subject}: ${e.passed} passed, ${e.failed} failed, ${e.errored} errored${e.unjudged?`, ${e.unjudged} unjudged`:``}.\n\n`)}let n=Yl(e);process.stdout.write(`Totals: ${n.passed} passed, ${n.failed} failed, ${n.errored} errored${n.unjudged?`, ${n.unjudged} unjudged`:``} across ${n.evals} evals.\n`)}function Yl(e){let t={passed:0,failed:0,errored:0,unjudged:0,evals:0};for(let n of e)t.passed+=n.passed,t.failed+=n.failed,t.errored+=n.errored,t.unjudged+=n.unjudged,t.evals+=n.evals.length;return t}function Xl(e,t){let n=e.replace(/\s+/g,` `).trim();return n.length>t?`${n.slice(0,t-1)}…`:n}const Zl=b({meta:{name:`eval`,description:`Run behaviour evals for the seo skill against a local agent and judge the results`},args:{subject:{type:`positional`,required:!1,description:`Eval subject (report id or job). Omit to run every subject.`},list:{type:`boolean`,default:!1,description:`List eval subjects with their eval counts and exit.`},id:{type:`string`,description:`Run one eval id within a single subject.`},agent:{type:`string`,default:Ol,description:`Agent command. Use {prompt} to place the prompt in argv, otherwise it is sent on stdin.`},"judge-agent":{type:`string`,description:`Judge command. Defaults to the same command as --agent.`},judge:ui(`Judge assertions with an agent.`,`Skip judging and print replies with an assertion checklist.`),skill:ui(`Send the seo router skill as context before the prompt.`,`Omit the seo router skill context.`),timeout:{type:`string`,default:`300`,description:`Timeout in seconds per agent or judge call.`},concurrency:{type:`string`,default:`1`,description:`Evals to run in parallel (max 4).`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e);if(e.list===!0){Kl(t);return}let n=Gl(Wl(e)),r=O(e.id);if(r!==void 0&&n.length!==1)throw new g(`INVALID_INPUT`,`--id needs exactly one subject. Name the subject to filter.`);let i=O(e.timeout)??300;if(!(i>0))throw new g(`INVALID_INPUT`,`--timeout must be a positive number of seconds.`);let a=Math.max(1,Math.min(4,O(e.concurrency)??1)),o=T(e.agent)??Ol,s=e.judge===!1?null:T(e[`judge-agent`])??o,c=e.skill===!1?null:jl(),l={agent:o,judge:s,skillBody:c,timeoutMs:i*1e3},u=[];for(let e of n){let n=Al(e.subject,e.path).evals;if(r!==void 0&&(n=n.filter(e=>e.id===r),n.length===0))throw new g(`INVALID_INPUT`,`No eval with id ${r} in subject ${e.subject}.`);t||process.stderr.write(`Running ${n.length} eval${n.length===1?``:`s`} for ${e.subject}...\n`);let i=await Hl(n,a,e=>Bl(e,l)),o=Ul(i);u.push({subject:e.subject,...o,evals:i})}let d=Yl(u),f=d.failed>0||d.errored>0;t?L({schemaVersion:1,agent:o,judge:s,judged:s!==null,skillContext:c!==null,timeoutSeconds:i,concurrency:a,subjects:u,totals:d}):Jl(u,{judged:s!==null,agent:o,judge:s}),f&&(process.exitCode=1)}});function Ql(){let e=ll();return br(e,{withFileTypes:!0}).filter(t=>t.isDirectory()&&v(_(e,t.name,`SKILL.md`))).map(t=>({name:t.name,description:pl(_(e,t.name)),path:_(e,t.name)})).sort((e,t)=>e.name<t.name?-1:+(e.name>t.name))}function $l(e){let t=Ql();if(!e)return t;let n=t.find(t=>t.name===e);if(!n)throw new g(`INVALID_INPUT`,`Unknown skill: ${e}.`);return[n]}const eu=b({meta:{name:`list`,description:`Show the packaged SEO skill`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:({args:e})=>{let t=Ql();if(j(e))L({skills:t});else{ea(`SEO skill`,`Packaged with this CLI.`),process.stdout.write(`
117
+ `);for(let e of t)R([[`Name`,e.name],[`Purpose`,`Helps agents choose and run the right SEO audit or report.`],[`Path`,e.path]]);process.stdout.write(`
118
+ `),$i({title:`Add it to an agent`,body:`Run setup to install the skill and connect your first site.`,command:`seo start`})}}}),tu=b({meta:{name:`path`,description:`Print a packaged agent skill path`},args:{name:{type:`positional`,description:`Skill name. Omit it for the skill directory.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:({args:e})=>{let t=T(e.name),n=t?$l(t)[0]?.path:ll();j(e)?L({name:t,path:n}):process.stdout.write(`${n}\n`)}}),nu=b({meta:{name:`install`,description:`Install the SEO skill for coding agents`},args:{json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:({args:e})=>{let t=j(e),n=Fs({quiet:t});if(t)L(n),n.status===`failed`&&(process.exitCode=1);else{if(n.status===`failed`)throw new g(`INTERNAL_ERROR`,`Skill install failed: ${n.error}`);process.stdout.write(`SEO skill installed. Agents that support skills can discover it now.
119
+ `)}}}),ru=b({meta:{name:`skill`,description:`Inspect the packaged SEO skill and run its evals`},subCommands:{list:eu,path:tu,install:nu,eval:Zl}}),iu=b({meta:{name:`privacy`,description:`Show local config, token, cache, and log storage paths`},run:async()=>{let e=Be(),t=Te();R(e.map(e=>[e.label,`${e.path} · ${ca(e.sizeBytes)} · ${e.mode}`])),process.stdout.write(`
120
+ `),R(Object.entries(t.counts).map(([e,t])=>[e,String(t)]))}}),au=b({meta:{name:`reset`,description:`Delete saved credentials and all local data`},args:{yes:{type:`boolean`,default:!1,description:`Confirm deletion without prompting.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e);if(!e.yes&&!I({json:t}))throw new g(`INVALID_INPUT`,`Cannot prompt here. Pass --yes to confirm reset.`);e.yes||B(await Vr({message:`Delete saved credentials and all local data?`,initialValue:!1}))?(await Fn(),t?L({reset:!0}):process.stdout.write(`Reset complete.
121
+ `)):Br(`Reset aborted.`)}}),ou=b({meta:{name:`sites`,description:`List Search Console properties available to this Google login`},args:{json:{type:`boolean`,default:!1},refresh:{type:`boolean`,default:!1},account:{type:`string`,description:`Saved Google account email.`}},run:async({args:e})=>{let t=await St(E(e.refresh),T(e.account));j(e)?L({sites:t}):z([`Property`,`Permission`],t.map(e=>[e.siteUrl,e.permissionLevel??`unknown`]))}}),su=b({meta:{name:`updates`,description:`List official Google Search Status ranking updates`},args:{product:{type:`string`,default:`Ranking`},limit:{type:`string`,default:`10`},json:{type:`boolean`,default:!1}},run:async({args:e})=>{let t=await rr({product:T(e.product),limit:O(e.limit)});j(e)?L({updates:t}):ia(t.map(e=>({title:e.name,meta:[e.type,e.status,`${e.start.slice(0,10)} to ${e.end?.slice(0,10)??`present`}`]})),{empty:`No matching Google Search updates.`})}}),cu={ci:`disabled automatically in CI or tests`,do_not_track:`disabled by DO_NOT_TRACK`,environment:`disabled by a telemetry environment variable`,local_setting:`disabled in local settings`,invalid_state:`disabled because the local state file is invalid`};function lu(e){return{enabled:e.enabled,...e.reason?{reason:e.reason}:{},stateFile:e.stateFile,...e.state?{firstRunAt:e.state.firstRunAt,cohort:e.state.cohort,sentMilestones:e.state.sentMilestones}:{}}}function uu(e,t){let n=lu(e);t?L(n):(R([[`Anonymous telemetry`,e.enabled?`enabled`:`disabled`],[`Reason`,e.reason?cu[e.reason]:`local setting`],[`State file`,e.stateFile],[`Install cohort`,e.state?.cohort??`not created`]]),process.stdout.write(`
122
122
  Only tool usage is sent. URLs, report data, Google data, and identifiers are never sent.
123
123
  `),process.stdout.write(`Details: https://seoskill.dev/telemetry
124
- `))}const uu={type:`boolean`,default:!1,description:`Print machine-readable JSON.`},du=b({meta:{name:`telemetry`,description:`Check or change anonymous usage telemetry`},subCommands:{status:b({meta:{name:`status`,description:`Show whether anonymous usage telemetry is enabled`},args:{json:uu},run:async({args:e})=>lu(Pt(),A(e))}),enable:b({meta:{name:`enable`,description:`Enable anonymous usage telemetry in local settings`},args:{json:uu},run:async({args:e})=>{lu(On(!0),A(e))}}),disable:b({meta:{name:`disable`,description:`Disable anonymous usage telemetry in local settings`},args:{json:uu},run:async({args:e})=>{lu(On(!1),A(e))}})}});function fu(e){let t=e.toLowerCase();return[`pass`,`passed`,`complete`,`completed`,`joined`].includes(t)?`pass`:[`warn`,`warning`,`partial`,`review`].includes(t)?`warning`:[`fail`,`failed`,`error`].includes(t)?`fail`:[`unknown`,`unavailable`].includes(t)?`unknown`:`info`}function pu(e){let t=e.steps.map(e=>fu(e.status)),n=t.includes(`fail`)?`fail`:t.includes(`warning`)?`warning`:t.includes(`unknown`)?`unknown`:`pass`;ta({title:e.workflow,target:e.site,status:n,summary:e.summary,metrics:[{label:`Completed`,value:t.filter(e=>e===`pass`).length,status:`pass`},{label:`Review`,value:t.filter(e=>e===`warning`).length,status:`warning`},{label:`Unknown`,value:t.filter(e=>e===`unknown`).length,status:`unknown`}],sections:[{title:`Evidence collection`,diagnostics:e.steps.map(e=>({status:fu(e.status),title:e.tool,explanation:e.summary,evidence:[`Status: ${e.status}`]}))},{title:`Priority actions`,diagnostics:e.actions.map(e=>({status:`warning`,title:e.title,explanation:`Confidence: ${e.confidence}.`,fix:e.action}))}]})}const mu=b({meta:{name:`crawl-queue`,description:`Run a crawl and return a ranked implementation queue`},args:{crawlUrl:{type:`positional`,required:!1,description:`Start URL to crawl.`},url:{type:`string`,description:`Start URL to crawl. Defaults to the --site home page.`},site:{type:`string`,description:`GSC property URL for joining page metrics.`},...po,mode:{type:`string`,description:`Crawl mode: site, page, list, or sitemap. Defaults to site.`},urls:{type:`string`,description:`Comma-separated URLs for list mode.`},"urls-file":{type:`string`,description:`File with one URL per line for list mode.`},"max-pages":{type:`string`,description:`Maximum pages to crawl. Defaults to 500.`},"max-depth":{type:`string`,description:`Maximum click depth. Defaults to 16.`},concurrency:{type:`string`,description:`Parallel page fetches. Defaults to 8.`},"fetch-interval-cap":{type:`string`,description:`Maximum page fetches per interval per host.`},"fetch-interval-ms":{type:`string`,description:`Fetch rate interval in milliseconds.`},refresh:{type:`boolean`,default:!1,description:`Bypass local HTTP cache and fetch fresh pages.`},include:{type:`string`,description:`Comma-separated URL patterns to include.`},exclude:{type:`string`,description:`Comma-separated URL patterns to exclude.`},limit:{type:`string`,description:`Maximum queue items to print. Defaults to 25.`},"no-sitemap":{type:`boolean`,default:!1,description:`Do not seed URLs from sitemap.xml.`},"no-robots":{type:`boolean`,default:!1,description:`Do not skip URLs disallowed by robots.txt.`},"no-external":{type:`boolean`,default:!1,description:`Do not check external links.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering when Playwright is installed.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=await _u(e),r=gu(e),i=w(e.site),a=r??(i?xo(i):void 0)??n[0];if(!a)throw new g(`INVALID_INPUT`,"No crawl URL given. Pass a URL, --url or --site. Run `seo sites` to list your Search Console properties.");let o=await Me({url:a,site:i,mode:hu(e.mode)??(n.length?`list`:void 0),urls:n,maxPages:D(e[`max-pages`]),maxDepth:D(e[`max-depth`]),concurrency:D(e.concurrency),refresh:T(e.refresh),fetchRate:hi({...e,"fetch-concurrency":e.concurrency}),include:k(e.include),exclude:k(e.exclude),googleAnalyticsPropertyId:w(e[`google-analytics-property`]),analyticsConnection:mo(e),useSitemap:!E(e,`sitemap`),respectRobots:!E(e,`robots`),checkExternal:!E(e,`external`),js:!!T(e.js),limit:D(e.limit)});t?I(o):(pu(o),R([`Rank`,`Score`,`Rule`,`Severity`,`Affected`,`Target`,`Action`],o.output.queue.map((e,t)=>[t+1,e.score,e.ruleId,e.severity,e.affectedUrls,e.target,e.action])),J(`Implementation queue details`,o.output.queue.map(e=>({label:`${e.ruleId} (${e.affectedUrls} URLs)`,context:`${e.severity}, score ${e.score}`,action:`${e.action} Verify with: ${e.verification.command}`}))))}});function hu(e){let t=w(e);if(t){if([`site`,`page`,`list`,`sitemap`].includes(t))return t;throw Error(`Mode must be one of: site, page, list, sitemap.`)}}function gu(e){let t=w(e.crawlUrl),n=w(e.url);if(t&&n&&t!==n)throw Error(`Use either a URL argument or --url, not both.`);return t??n}async function _u(e){let t=k(e.urls)??[],n=w(e[`urls-file`]);return n&&t.push(...vu(await y(n,`utf8`))),[...new Set(t)]}function vu(e){return e.split(/[\n,]/).map(e=>e.trim()).filter(e=>e&&!e.startsWith(`#`))}const yu=b({meta:{name:`refresh-priorities`,description:`Agent workflow for a ranked SEO action queue`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},...X([`days`,`recentDays`,`limit`,`includeBrand`,`refresh`],{days:{description:`Diagnosis window length in days. Defaults to 90.`},limit:{description:`Maximum queue items to print. Defaults to 25.`}}),...po,"verify-content":ui(`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.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=j(w(e.site)),r=await m({site:n,days:D(e.days),recentDays:D(e.recent),limit:D(e.limit),includeBrand:T(e[`include-brand`]),googleAnalyticsPropertyId:w(e[`google-analytics-property`]),analyticsConnection:mo(e),verifyContent:T(e[`verify-content`]),verifyLimit:D(e[`verify-limit`]),refresh:T(e.refresh)});t?I(r):(pu(r),R([`Source`,`Category`,`Score`,`Findings`,`Template`,`Analytics`,`Target`,`Action`],r.output.queue.map(e=>[e.source,e.category,e.score,e.grouped?.count??1,e.template?.label??`-`,e.analytics?.sessions??`-`,e.target,e.action])),J(`Queue action details`,r.output.queue.map(e=>({label:e.target,context:`${e.category}, score ${e.score}`,action:e.action}))))}});function bu(e){return e===null?`-`:e.toLocaleString(`en-GB`,{maximumFractionDigits:e>=100?0:1})}function xu(e){return[...e.winners.map(e=>({direction:`winner`,item:e})),...e.losers.map(e=>({direction:`loser`,item:e}))]}function Su(e,t){let n=xu(t);n.length&&(process.stdout.write(`\n${e}\n`),R([`Direction`,`Key`,`Clicks`,`Impr.`,`Position`],n.map(({direction:e,item:t})=>[e,t.key,bu(t.clickDelta),bu(t.impressionDelta),bu(t.positionDelta)])))}function Cu(e){process.stdout.write(`
124
+ `))}const du={type:`boolean`,default:!1,description:`Print machine-readable JSON.`},fu=b({meta:{name:`telemetry`,description:`Check or change anonymous usage telemetry`},subCommands:{status:b({meta:{name:`status`,description:`Show whether anonymous usage telemetry is enabled`},args:{json:du},run:async({args:e})=>uu(Nn(),j(e))}),enable:b({meta:{name:`enable`,description:`Enable anonymous usage telemetry in local settings`},args:{json:du},run:async({args:e})=>{uu(Jt(!0),j(e))}}),disable:b({meta:{name:`disable`,description:`Disable anonymous usage telemetry in local settings`},args:{json:du},run:async({args:e})=>{uu(Jt(!1),j(e))}})}});function pu(e){let t=e.toLowerCase();return[`pass`,`passed`,`complete`,`completed`,`joined`].includes(t)?`pass`:[`warn`,`warning`,`partial`,`review`].includes(t)?`warning`:[`fail`,`failed`,`error`].includes(t)?`fail`:[`unknown`,`unavailable`].includes(t)?`unknown`:`info`}function mu(e){let t=e.steps.map(e=>pu(e.status)),n=t.includes(`fail`)?`fail`:t.includes(`warning`)?`warning`:t.includes(`unknown`)?`unknown`:`pass`;na({title:e.workflow,target:e.site,status:n,summary:e.summary,metrics:[{label:`Completed`,value:t.filter(e=>e===`pass`).length,status:`pass`},{label:`Review`,value:t.filter(e=>e===`warning`).length,status:`warning`},{label:`Unknown`,value:t.filter(e=>e===`unknown`).length,status:`unknown`}],sections:[{title:`Evidence collection`,diagnostics:e.steps.map(e=>({status:pu(e.status),title:e.tool,explanation:e.summary,evidence:[`Status: ${e.status}`]}))},{title:`Priority actions`,diagnostics:e.actions.map(e=>({status:`warning`,title:e.title,explanation:`Confidence: ${e.confidence}.`,fix:e.action}))}]})}const hu=b({meta:{name:`crawl-queue`,description:`Run a crawl and return a ranked implementation queue`},args:{crawlUrl:{type:`positional`,required:!1,description:`Start URL to crawl.`},url:{type:`string`,description:`Start URL to crawl. Defaults to the --site home page.`},site:{type:`string`,description:`GSC property URL for joining page metrics.`},...mo,mode:{type:`string`,description:`Crawl mode: site, page, list, or sitemap. Defaults to site.`},urls:{type:`string`,description:`Comma-separated URLs for list mode.`},"urls-file":{type:`string`,description:`File with one URL per line for list mode.`},"max-pages":{type:`string`,description:`Maximum pages to crawl. Defaults to 500.`},"max-depth":{type:`string`,description:`Maximum click depth. Defaults to 16.`},concurrency:{type:`string`,description:`Parallel page fetches. Defaults to 8.`},"fetch-interval-cap":{type:`string`,description:`Maximum page fetches per interval per host.`},"fetch-interval-ms":{type:`string`,description:`Fetch rate interval in milliseconds.`},refresh:{type:`boolean`,default:!1,description:`Bypass local HTTP cache and fetch fresh pages.`},include:{type:`string`,description:`Comma-separated URL patterns to include.`},exclude:{type:`string`,description:`Comma-separated URL patterns to exclude.`},limit:{type:`string`,description:`Maximum queue items to print. Defaults to 25.`},"no-sitemap":{type:`boolean`,default:!1,description:`Do not seed URLs from sitemap.xml.`},"no-robots":{type:`boolean`,default:!1,description:`Do not skip URLs disallowed by robots.txt.`},"no-external":{type:`boolean`,default:!1,description:`Do not check external links.`},js:{type:`boolean`,default:!1,description:`Force JavaScript rendering when Playwright is installed.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=await vu(e),r=_u(e),i=T(e.site),a=r??(i?So(i):void 0)??n[0];if(!a)throw new g(`INVALID_INPUT`,"No crawl URL given. Pass a URL, --url or --site. Run `seo sites` to list your Search Console properties.");let o=await tn({url:a,site:i,mode:gu(e.mode)??(n.length?`list`:void 0),urls:n,maxPages:O(e[`max-pages`]),maxDepth:O(e[`max-depth`]),concurrency:O(e.concurrency),refresh:E(e.refresh),fetchRate:hi({...e,"fetch-concurrency":e.concurrency}),include:A(e.include),exclude:A(e.exclude),googleAnalyticsPropertyId:T(e[`google-analytics-property`]),analyticsConnection:ho(e),useSitemap:!D(e,`sitemap`),respectRobots:!D(e,`robots`),checkExternal:!D(e,`external`),js:!!E(e.js),limit:O(e.limit)});t?L(o):(mu(o),z([`Rank`,`Score`,`Rule`,`Severity`,`Affected`,`Target`,`Action`],o.output.queue.map((e,t)=>[t+1,e.score,e.ruleId,e.severity,e.affectedUrls,e.target,e.action])),J(`Implementation queue details`,o.output.queue.map(e=>({label:`${e.ruleId} (${e.affectedUrls} URLs)`,context:`${e.severity}, score ${e.score}`,action:`${e.action} Verify with: ${e.verification.command}`}))))}});function gu(e){let t=T(e);if(t){if([`site`,`page`,`list`,`sitemap`].includes(t))return t;throw Error(`Mode must be one of: site, page, list, sitemap.`)}}function _u(e){let t=T(e.crawlUrl),n=T(e.url);if(t&&n&&t!==n)throw Error(`Use either a URL argument or --url, not both.`);return t??n}async function vu(e){let t=A(e.urls)??[],n=T(e[`urls-file`]);return n&&t.push(...yu(await y(n,`utf8`))),[...new Set(t)]}function yu(e){return e.split(/[\n,]/).map(e=>e.trim()).filter(e=>e&&!e.startsWith(`#`))}const bu=b({meta:{name:`refresh-priorities`,description:`Agent workflow for a ranked SEO action queue`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},...X([`days`,`recentDays`,`limit`,`includeBrand`,`refresh`],{days:{description:`Diagnosis window length in days. Defaults to 90.`},limit:{description:`Maximum queue items to print. Defaults to 25.`}}),...mo,"verify-content":ui(`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.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=M(T(e.site)),r=await Me({site:n,days:O(e.days),recentDays:O(e.recent),limit:O(e.limit),includeBrand:E(e[`include-brand`]),googleAnalyticsPropertyId:T(e[`google-analytics-property`]),analyticsConnection:ho(e),verifyContent:E(e[`verify-content`]),verifyLimit:O(e[`verify-limit`]),refresh:E(e.refresh)});t?L(r):(mu(r),z([`Source`,`Category`,`Score`,`Findings`,`Template`,`Analytics`,`Target`,`Action`],r.output.queue.map(e=>[e.source,e.category,e.score,e.grouped?.count??1,e.template?.label??`-`,e.analytics?.sessions??`-`,e.target,e.action])),J(`Queue action details`,r.output.queue.map(e=>({label:e.target,context:`${e.category}, score ${e.score}`,action:e.action}))))}});function xu(e){return e===null?`-`:e.toLocaleString(`en-GB`,{maximumFractionDigits:e>=100?0:1})}function Su(e){return[...e.winners.map(e=>({direction:`winner`,item:e})),...e.losers.map(e=>({direction:`loser`,item:e}))]}function Cu(e,t){let n=Su(t);n.length&&(process.stdout.write(`\n${e}\n`),z([`Direction`,`Key`,`Clicks`,`Impr.`,`Position`],n.map(({direction:e,item:t})=>[e,t.key,xu(t.clickDelta),xu(t.impressionDelta),xu(t.positionDelta)])))}function wu(e){process.stdout.write(`
125
125
  Segment coverage
126
- `),R([`Dimension`,`Eligible`,`Returned`,`Limited`,`Result cap`,`Source cap`,`Source capped`],[`page`,`query`,`device`,`country`].map(t=>{let n=e[t].coverage;return[t,n.eligibleRows,n.returnedRows,n.limitedRows,n.resultLimit,n.sourceRowLimit,n.sourcePossiblyTruncated?`yes`:`no`]}))}function wu(e){e.length&&(process.stdout.write(`
126
+ `),z([`Dimension`,`Eligible`,`Returned`,`Limited`,`Result cap`,`Source cap`,`Source capped`],[`page`,`query`,`device`,`country`].map(t=>{let n=e[t].coverage;return[t,n.eligibleRows,n.returnedRows,n.limitedRows,n.resultLimit,n.sourceRowLimit,n.sourcePossiblyTruncated?`yes`:`no`]}))}function Tu(e){e.length&&(process.stdout.write(`
127
127
  Template movement
128
- `),R([`Direction`,`Template`,`Pattern confidence`,`URLs`,`Clicks`,`Returned share`,`Common terms`],e.map(e=>[e.direction,e.signature,e.confidence,e.urlCount,bu(e.clickDelta),`${Math.round(e.movementShare*100)}%`,e.commonTerms.join(`, `)||`-`])),Y(`Template actions`,e.map(e=>e.summary)),Y(`Template evidence scope`,[`Shares use returned directional page click movement. Pattern confidence applies only to returned matched-retained page segments.`]))}const Tu=b({meta:{name:`update-postmortem`,description:`Agent workflow for Google update winner/loser analysis`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},...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.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=A(e),n=j(w(e.site)),r=await ne({site:n,days:D(e.days),recentDays:D(e.recent),limit:D(e.limit),includeBrand:T(e[`include-brand`]),knownConfounders:w(e[`known-change`])?[w(e[`known-change`])??``]:void 0,refresh:T(e.refresh)});t?I(r):(K({title:`Update postmortem`,target:r.site,status:`unknown`,summary:r.summary,metrics:[{label:`Attribution`,value:r.output.update.attribution.replaceAll(`-`,` `)},{label:`Confidence`,value:r.output.update.confidence},{label:`Known confounders`,value:r.output.update.confounders.length}]}),Y(`Recommended actions`,r.actions.map(e=>`${e.title} (${e.confidence}): ${e.action}`)),Y(`Postmortem findings`,r.output.insights.map(e=>e.summary)),Y(`Segment evidence warnings`,[...r.output.segments.page.warnings,...r.output.segments.query.warnings,...r.output.segments.device.warnings,...r.output.segments.country.warnings].filter((e,t,n)=>n.indexOf(e)===t)),wu(r.output.templateMovement),Y(`Update evidence`,r.output.update.evidence),Cu(r.output.segments),Su(`Page winners and losers`,r.output.segments.page),Su(`Query winners and losers`,r.output.segments.query),Su(`Device winners and losers`,r.output.segments.device),Su(`Country winners and losers`,r.output.segments.country),Za(`seo export update-postmortem --site ${JSON.stringify(n)}`))}}),Eu=/* @__PURE__ */ new Set([`database`,`filesystem`,`internal`,`unknown`]);function Du(e){return e.replace(/^v/,``).split(`.`)[0]||`unknown`}function Ou(e){return jt(e[0])?e[0]:void 0}function ku(e,t){return mn(e)?`seo ${e}`:t?`seo ${t}`:`seo command`}function Au(e){let t=zn(e.error);if(!Eu.has(t.errorCategory))return;let n=Ou(e.args),r=ku(e.report,n),i=[Xn(e.error).code,t.errorCategory,t.failureReason,t.failureContext].filter(Boolean).join(` | `),a=new URL(`https://github.com/iannuttall/seo/issues/new`);return a.searchParams.set(`template`,`bug.yml`),a.searchParams.set(`title`,`bug: ${r} failed`),a.searchParams.set(`version`,e.version??sn),a.searchParams.set(`runtime`,`Node ${Du(e.nodeVersion??process.version)} on ${e.processPlatform??or()} (${e.processArch??ir()})`),a.searchParams.set(`diagnostics`,i),a.toString()}const ju={"refresh-priorities":`seo report`,"query-cluster":`seo report`,"quick-wins":`seo report --section quick-wins`,"ctr-underperformers":`seo report --section low-ctr`,"striking-distance":`seo report --section striking-distance`,"second-page":`seo report --section second-page`,decaying:`seo report --section content-decay`,cannibal:`seo report --section cannibalization`,"traffic-anomaly":`seo report --section traffic-drop`,"page-opportunities":`seo page --url <url>`,"audit-page":`seo page --url <url>`,"url-inspect":`seo page --url <url>`,"segment-impact":`seo compare --date <date>`,"update-correlate":`seo compare --date <date>`,"update-postmortem":`seo compare --date <date>`,"link-recover":`seo index`,"gsc-query":`seo data`},Mu={"agent-readiness":null,"ai-readiness":null,"community-intent":null,"entity-readiness":null,llms:null,"monthly-report":`seo report`,okf:null,pseo:null,"report-narrative":`seo report`,"seo-to-ai-query":null,"server-logs":null,links:`seo research backlinks --target <domain>`,"export knowledge":null,"export monthly":`seo report --json`,"export narrative":`seo report --json`,"export pseo":null,indexnow:null,"providers semrush":`seo providers`,"providers limits":null,"providers spend":null,"providers dataforseo limits":null,"providers dataforseo spend":null,"providers ahrefs limits":null,"change-log":null,client:null,project:null,projects:null,"content-groups":null,"crawl-diff":`seo crawl`,"crawl-reports":`seo crawl`,diagnose:`seo report`,"diagnose-property":`seo report`,"export diagnose":`seo report --json`,"index-watch":`seo page --url <url>`,monitoring:`seo crawl`,schedule:null,"technical-watch":`seo crawl`,tests:`seo compare --date <date>`},Z=`It saved history between runs. Your agent keeps its own notes now.`,Nu=`Commands take --site now instead of a saved project.`,Pu=`Spend is controlled in your provider account. Set limits in DataForSEO, Ahrefs or the other provider.`,Fu={"providers limits":Pu,"providers spend":Pu,"providers dataforseo limits":Pu,"providers dataforseo spend":Pu,"providers ahrefs limits":Pu,"change-log":Z,client:Nu,project:Nu,projects:Nu,"content-groups":Z,"crawl-diff":Z,"crawl-reports":Z,diagnose:Z,"diagnose-property":Z,"export diagnose":Z,"index-watch":Z,monitoring:Z,schedule:Z,"technical-watch":Z,tests:Z},Iu=/* @__PURE__ */ new Set([`client`,`project`,`projects`]);function Lu(e){let t=e.findIndex(e=>e.startsWith(`-`)),n=t===-1?e:e.slice(0,t),r=+(n[0]===`help`);for(let e=3;e>=1;--e){let t=n.slice(r,r+e).join(` `);if(n.length-r>=e&&Object.hasOwn(Mu,t))return t}return null}function Ru(e){let t=Object.hasOwn(ju,e)?ju[e]:void 0;return t?`\`seo ${e}\` is now \`${t}\`. The old name stops working in 1.0.0.`:null}function zu(e){let t=Lu(e);if(!t)return null;let n=Mu[t],r=t.split(` `)[0]??``,i=n?`Use \`${n}\` instead.`:Iu.has(r)?"Run `seo sites` to list your Search Console properties.":r===`providers`?"Run `seo providers --help` for the provider commands.":"Run `seo help` for the commands.";return`\`seo ${t}\` was removed. ${Object.hasOwn(Fu,t)?Fu[t]:`SEO Skill now focuses on Search Console, analytics and keyword data.`} ${i}`}const Bu={"internal-links":[`--days`,`--limit`,`--check-limit`,`--min-impressions`,`--brand-terms`,`--include-brand`,`--no-include-brand`,`--js`,`--no-js`,`--fetch-concurrency`,`--fetch-interval-cap`,`--fetch-interval-ms`]},Vu=e=>e.split(`=`)[0]??``;function Hu(e){let t=e.find(e=>[`--project`,`--client`].includes(Vu(e)));if(t)return`\`${Vu(t)}\` was removed. ${Nu} Pass --site with a domain, URL or sc-domain: property, or run \`seo sites\` to list your Search Console properties.`;let n=e[0]??``,r=Object.hasOwn(Bu,n)?e.find(e=>Bu[n]?.includes(Vu(e))):void 0;if(!r)return null;let i=Vu(r);return`\`${i}\` was removed from \`seo ${n}\`. It now sets its own limits, brand terms and page fetching, and shows them in the output.${i===`--days`?` Pass --start-date and --end-date for other dates.`:``}`}function Uu(e){return e.replaceAll(`\\`,`/`).toLowerCase()}function Wu(e,t){return t===`win32`?`${e}.cmd`:e}function Gu(e,t=process.platform){let n=Uu(e);if(!(n.includes(`/_npx/`)||n.includes(`/.npm/_npx/`)||n.includes(`/.cache/pnpm/dlx/`)||n.includes(`/.volta/`))){if(n.includes(`/.pnpm/`)||n.includes(`/pnpm/global/`))return{command:Wu(`pnpm`,t),args:[`add`,`--global`,`seo@latest`]};if(n.includes(`/.bun/install/global/`))return{command:Wu(`bun`,t),args:[`add`,`--global`,`seo@latest`]};if(n.includes(`/.config/yarn/global/`))return{command:Wu(`yarn`,t),args:[`global`,`add`,`seo@latest`]};if(n.includes(`/node_modules/seo/`))return{command:Wu(`npm`,t),args:[`install`,`--global`,`seo@latest`]}}}function Ku(e){return!(!e.stdinIsTty||!e.stdoutIsTty||e.env.CI||e.env.NO_UPDATE_NOTIFIER||e.env.NODE_ENV===`test`||e.env.npm_lifecycle_event||e.env.npm_execpath||e.env.TERM===`dumb`||e.argv.includes(`--json`)||e.argv.some(e=>[`--help`,`-h`,`--version`,`-v`].includes(e))||[`help`,`mcp`].includes(e.argv[0]??``))}function qu(e){if(e)try{return br(e)}catch{return e}}async function Ju(e){let t=await Kr({message:`seo ${e.update.latest} is available. You have ${e.update.current}.`,options:[{value:`update`,label:`Update now`,hint:[e.command.command,...e.command.args].join(` `)},{value:`later`,label:`Later`}],initialValue:`later`});return Hr(t)?`later`:t}function Yu(e){return Xo(e.command,e.args,{stdio:`inherit`}).status}async function Xu(e,t={}){if(!Ku({argv:t.argv??process.argv.slice(2),env:t.env??process.env,stdinIsTty:t.stdinIsTty??!!process.stdin.isTTY,stdoutIsTty:t.stdoutIsTty??!!process.stdout.isTTY}))return;let n=qu(t.cliPath??process.argv[1]),r=n?Gu(n,t.platform??process.platform):void 0;if(!r)return;let i=(t.createNotifier??(e=>ni({pkg:e,shouldNotifyInNpmScript:!1})))(e).update;if(i&&Tt(i.current,i.latest)&&await(t.prompt??Ju)({command:r,update:i})!==`later`)return(t.run??Yu)(r)===0?(Ur.success(`Updated seo to ${i.latest}. Run your command again.`),0):(Ur.error(`The update failed. Your current version is unchanged.`),1)}function Zu(e){if(process.stderr.isTTY&&!process.env.CI&&!(e.includes(`--json`)||e[0]===`mcp`))try{let{configDir:e,configFile:t}=Sn();if(!v(t))return;let n=_(e,`notices.json`),r=v(n)?JSON.parse(vr(n,`utf8`)):[];if(r.includes(`0.4`))return;process.stderr.write(`
128
+ `),z([`Direction`,`Template`,`Pattern confidence`,`URLs`,`Clicks`,`Returned share`,`Common terms`],e.map(e=>[e.direction,e.signature,e.confidence,e.urlCount,xu(e.clickDelta),`${Math.round(e.movementShare*100)}%`,e.commonTerms.join(`, `)||`-`])),Y(`Template actions`,e.map(e=>e.summary)),Y(`Template evidence scope`,[`Shares use returned directional page click movement. Pattern confidence applies only to returned matched-retained page segments.`]))}const Eu=b({meta:{name:`update-postmortem`,description:`Agent workflow for Google update winner/loser analysis`},args:{site:{type:`string`,description:`GSC property URL, for example sc-domain:example.com.`},...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.`},json:{type:`boolean`,default:!1,description:`Print machine-readable JSON.`}},run:async({args:e})=>{let t=j(e),n=M(T(e.site)),r=await te({site:n,days:O(e.days),recentDays:O(e.recent),limit:O(e.limit),includeBrand:E(e[`include-brand`]),knownConfounders:T(e[`known-change`])?[T(e[`known-change`])??``]:void 0,refresh:E(e.refresh)});t?L(r):(K({title:`Update postmortem`,target:r.site,status:`unknown`,summary:r.summary,metrics:[{label:`Attribution`,value:r.output.update.attribution.replaceAll(`-`,` `)},{label:`Confidence`,value:r.output.update.confidence},{label:`Known confounders`,value:r.output.update.confounders.length}]}),Y(`Recommended actions`,r.actions.map(e=>`${e.title} (${e.confidence}): ${e.action}`)),Y(`Postmortem findings`,r.output.insights.map(e=>e.summary)),Y(`Segment evidence warnings`,[...r.output.segments.page.warnings,...r.output.segments.query.warnings,...r.output.segments.device.warnings,...r.output.segments.country.warnings].filter((e,t,n)=>n.indexOf(e)===t)),Tu(r.output.templateMovement),Y(`Update evidence`,r.output.update.evidence),wu(r.output.segments),Cu(`Page winners and losers`,r.output.segments.page),Cu(`Query winners and losers`,r.output.segments.query),Cu(`Device winners and losers`,r.output.segments.device),Cu(`Country winners and losers`,r.output.segments.country),Qa(`seo export update-postmortem --site ${JSON.stringify(n)}`))}}),Du=/* @__PURE__ */ new Set([`database`,`filesystem`,`internal`,`unknown`]);function Ou(e){return e.replace(/^v/,``).split(`.`)[0]||`unknown`}function ku(e){return It(e[0])?e[0]:void 0}function Au(e,t){return En(e)?`seo ${e}`:t?`seo ${t}`:`seo command`}function ju(e){let t=kt(e.error);if(!Du.has(t.errorCategory))return;let n=ku(e.args),r=Au(e.report,n),i=[We(e.error).code,t.errorCategory,t.failureReason,t.failureContext].filter(Boolean).join(` | `),a=new URL(`https://github.com/iannuttall/seo/issues/new`);return a.searchParams.set(`template`,`bug.yml`),a.searchParams.set(`title`,`bug: ${r} failed`),a.searchParams.set(`version`,e.version??Ln),a.searchParams.set(`runtime`,`Node ${Ou(e.nodeVersion??process.version)} on ${e.processPlatform??sr()} (${e.processArch??ar()})`),a.searchParams.set(`diagnostics`,i),a.toString()}const Mu={"refresh-priorities":`seo report`,"query-cluster":`seo report`,"quick-wins":`seo report --section quick-wins`,"ctr-underperformers":`seo report --section low-ctr`,"striking-distance":`seo report --section striking-distance`,"second-page":`seo report --section second-page`,decaying:`seo report --section content-decay`,cannibal:`seo report --section cannibalization`,"traffic-anomaly":`seo report --section traffic-drop`,"page-opportunities":`seo page --url <url>`,"audit-page":`seo page --url <url>`,"url-inspect":`seo page --url <url>`,"segment-impact":`seo compare --date <date>`,"update-correlate":`seo compare --date <date>`,"update-postmortem":`seo compare --date <date>`,"link-recover":`seo index`,"gsc-query":`seo data`},Nu={"agent-readiness":null,"ai-readiness":null,"community-intent":null,"entity-readiness":null,llms:null,"monthly-report":`seo report`,okf:null,pseo:null,"report-narrative":`seo report`,"seo-to-ai-query":null,"server-logs":null,links:`seo research backlinks --target <domain>`,"export knowledge":null,"export monthly":`seo report --json`,"export narrative":`seo report --json`,"export pseo":null,indexnow:null,"providers semrush":`seo providers`,"providers limits":null,"providers spend":null,"providers dataforseo limits":null,"providers dataforseo spend":null,"providers ahrefs limits":null,"change-log":null,client:null,project:null,projects:null,"content-groups":null,"crawl-diff":`seo crawl`,"crawl-reports":`seo crawl`,diagnose:`seo report`,"diagnose-property":`seo report`,"export diagnose":`seo report --json`,"index-watch":`seo page --url <url>`,monitoring:`seo crawl`,schedule:null,"technical-watch":`seo crawl`,tests:`seo compare --date <date>`},Z=`It saved history between runs. Your agent keeps its own notes now.`,Pu=`Commands take --site now instead of a saved project.`,Fu=`Spend is controlled in your provider account. Set limits in DataForSEO, Ahrefs or the other provider.`,Iu={"providers limits":Fu,"providers spend":Fu,"providers dataforseo limits":Fu,"providers dataforseo spend":Fu,"providers ahrefs limits":Fu,"change-log":Z,client:Pu,project:Pu,projects:Pu,"content-groups":Z,"crawl-diff":Z,"crawl-reports":Z,diagnose:Z,"diagnose-property":Z,"export diagnose":Z,"index-watch":Z,monitoring:Z,schedule:Z,"technical-watch":Z,tests:Z},Lu=/* @__PURE__ */ new Set([`client`,`project`,`projects`]);function Ru(e){let t=e.findIndex(e=>e.startsWith(`-`)),n=t===-1?e:e.slice(0,t),r=+(n[0]===`help`);for(let e=3;e>=1;--e){let t=n.slice(r,r+e).join(` `);if(n.length-r>=e&&Object.hasOwn(Nu,t))return t}return null}function zu(e){let t=Object.hasOwn(Mu,e)?Mu[e]:void 0;return t?`\`seo ${e}\` is now \`${t}\`. The old name stops working in 1.0.0.`:null}function Bu(e){let t=Ru(e);if(!t)return null;let n=Nu[t],r=t.split(` `)[0]??``,i=n?`Use \`${n}\` instead.`:Lu.has(r)?"Run `seo sites` to list your Search Console properties.":r===`providers`?"Run `seo providers --help` for the provider commands.":"Run `seo help` for the commands.";return`\`seo ${t}\` was removed. ${Object.hasOwn(Iu,t)?Iu[t]:`SEO Skill now focuses on Search Console, analytics and keyword data.`} ${i}`}const Vu={"internal-links":[`--days`,`--limit`,`--check-limit`,`--min-impressions`,`--brand-terms`,`--include-brand`,`--no-include-brand`,`--js`,`--no-js`,`--fetch-concurrency`,`--fetch-interval-cap`,`--fetch-interval-ms`]},Hu=e=>e.split(`=`)[0]??``;function Uu(e){let t=e.find(e=>[`--project`,`--client`].includes(Hu(e)));if(t)return`\`${Hu(t)}\` was removed. ${Pu} Pass --site with a domain, URL or sc-domain: property, or run \`seo sites\` to list your Search Console properties.`;let n=e[0]??``,r=Object.hasOwn(Vu,n)?e.find(e=>Vu[n]?.includes(Hu(e))):void 0;if(!r)return null;let i=Hu(r);return`\`${i}\` was removed from \`seo ${n}\`. It now sets its own limits, brand terms and page fetching, and shows them in the output.${i===`--days`?` Pass --start-date and --end-date for other dates.`:``}`}function Wu(e){return e.replaceAll(`\\`,`/`).toLowerCase()}function Gu(e,t){return t===`win32`?`${e}.cmd`:e}function Ku(e,t=process.platform){let n=Wu(e);if(!(n.includes(`/_npx/`)||n.includes(`/.npm/_npx/`)||n.includes(`/.cache/pnpm/dlx/`)||n.includes(`/.volta/`))){if(n.includes(`/.pnpm/`)||n.includes(`/pnpm/global/`))return{command:Gu(`pnpm`,t),args:[`add`,`--global`,`seo@latest`]};if(n.includes(`/.bun/install/global/`))return{command:Gu(`bun`,t),args:[`add`,`--global`,`seo@latest`]};if(n.includes(`/.config/yarn/global/`))return{command:Gu(`yarn`,t),args:[`global`,`add`,`seo@latest`]};if(n.includes(`/node_modules/seo/`))return{command:Gu(`npm`,t),args:[`install`,`--global`,`seo@latest`]}}}function qu(e){return!(!e.stdinIsTty||!e.stdoutIsTty||e.env.CI||e.env.NO_UPDATE_NOTIFIER||e.env.NODE_ENV===`test`||e.env.npm_lifecycle_event||e.env.npm_execpath||e.env.TERM===`dumb`||e.argv.includes(`--json`)||e.argv.some(e=>[`--help`,`-h`,`--version`,`-v`].includes(e))||[`help`,`mcp`].includes(e.argv[0]??``))}function Ju(e){if(e)try{return xr(e)}catch{return e}}async function Yu(e){let t=await Kr({message:`seo ${e.update.latest} is available. You have ${e.update.current}.`,options:[{value:`update`,label:`Update now`,hint:[e.command.command,...e.command.args].join(` `)},{value:`later`,label:`Later`}],initialValue:`later`});return Hr(t)?`later`:t}function Xu(e){return Zo(e.command,e.args,{stdio:`inherit`}).status}async function Zu(e,t={}){if(!qu({argv:t.argv??process.argv.slice(2),env:t.env??process.env,stdinIsTty:t.stdinIsTty??!!process.stdin.isTTY,stdoutIsTty:t.stdoutIsTty??!!process.stdout.isTTY}))return;let n=Ju(t.cliPath??process.argv[1]),r=n?Ku(n,t.platform??process.platform):void 0;if(!r)return;let i=(t.createNotifier??(e=>ni({pkg:e,shouldNotifyInNpmScript:!1})))(e).update;if(i&&Ct(i.current,i.latest)&&await(t.prompt??Yu)({command:r,update:i})!==`later`)return(t.run??Xu)(r)===0?(Ur.success(`Updated seo to ${i.latest}. Run your command again.`),0):(Ur.error(`The update failed. Your current version is unchanged.`),1)}function Qu(e){if(process.stderr.isTTY&&!process.env.CI&&!(e.includes(`--json`)||e[0]===`mcp`))try{let{configDir:e,configFile:t}=ln();if(!v(t))return;let n=_(e,`notices.json`),r=v(n)?JSON.parse(yr(n,`utf8`)):[];if(r.includes(`0.4`))return;process.stderr.write(`
129
129
  SEO Skill 0.4: eight commands replace the old reports, and projects are gone: every command takes --site (run \`seo sites\` to list yours). Old command names print what to use instead.
130
130
  Free hosted MCP server, nothing to install: https://seoskill.dev/docs/mcp
131
131
  To stay on the old version: npm i -g seo@0.2.41
132
132
  What changed: https://github.com/iannuttall/seo/blob/main/CHANGELOG.md
133
133
 
134
- `),wr(n,JSON.stringify([...r,`0.4`]),{mode:384})}catch{}}const Qu={name:`seo`,version:sn},$u=[{title:`Start here`,commands:[[`seo start`,`Sign in with Google, then add the MCP server and skill`],[`seo report`,`Last week across your audited sites (beta, after seo login)`],[`seo report --site <site>`,`One site: overview and every section`],[`seo report --site <site> --section quick-wins`,`One section of the report, with live page checks`]]},{title:`Commands`,commands:[[`seo page --url <url>`,`One page: live facts, index status, queries`],[`seo compare --date <date>`,`Before and after a date`],[`seo index`,`Pages that lost most of their clicks`],[`seo research <action>`,`Keyword, SERP, competitor and backlink data from your key`],[`seo data`,`Raw Search Console, Bing or Clicky rows`],[`seo crawl`,`Crawl a site and list technical issues`],[`seo internal-links`,`Pages that could link to a target page`],[`seo lint <files>`,`Check copy for AI writing patterns and length`],[`seo site new <folder>`,`Start a data-driven site on Astro and Cloudflare`]]},{title:`Setup`,commands:[[`seo login`,`Use your seoskill.dev account on this machine`],[`seo sites`,`List Search Console properties`],[`seo mcp install`,`Add the local MCP server to your agents`],[`seo providers`,`Connect DataForSEO or Ahrefs`],[`seo doctor`,`Check local auth and config`]]}],ed=[...$u,{title:`More commands`,commands:[[`seo crawl-queue`,`Run a crawl and rank the fix queue`],[`seo rules`,`List crawler rule ids`],[`seo explain --rule missing_title`,`Explain a crawler rule`],[`seo redirect-trace`,`Trace redirects`],[`seo perf audit`,`Run a page performance audit`],[`seo index-coverage`,`Choose pages for URL Inspection`],[`seo ai-referrals`,`AI referral traffic in Google Analytics`],[`seo analytics google report`,`Google Analytics landing pages for visits from search`],[`seo updates`,`List official Google ranking updates`],[`seo export`,`Export report data to CSV`],[`seo reports list`,`List every structured report`],[`seo skill list`,`Show the packaged SEO skill`],[`seo auth`,`Manage Google auth`],[`seo cache`,`Manage local cache`],[`seo privacy`,`Show local storage paths`],[`seo telemetry status`,`Check anonymous usage telemetry`],[`seo reset`,`Delete saved credentials and all local data`]]}];function td(e){$i(`seo v${Qu.version}`,`Your own Search Console, Google Analytics and Bing data, as facts for your agent.`),process.stdout.write(`
135
- `),na(e.flatMap(e=>e.commands.map(([t,n])=>({category:e.title,id:t,name:n}))),{noun:`command`,categoryLabels:Object.fromEntries(e.map(e=>[e.title,e.title]))}),process.stdout.write(`
136
- `),ia(`More help`,"Use `seo help <command>` or `seo <command> --help` for command help.","Use `seo help all` for the longer command list.")}const nd=b({meta:{name:`seo`,version:Qu.version,description:`Local-first SEO CLI and MCP server`},subCommands:{init:Xs,analytics:Ia,auth:Ua,mcp:ec,doctor:Cc,status:Cc,privacy:ru,telemetry:du,reset:iu,cache:Ka,logs:Zs,crawl:To,"crawl-queue":mu,content:Qa,providers:Xc,export:Yo,"index-coverage":tc,"link-recover":nc,rules:ol,explain:sl,"refresh-priorities":yu,"redirect-trace":rc,skill:nu,start:Ys,setup:Js,site:El,login:As,logout:Ns,report:ga,page:_a,compare:va,index:ya,data:ba,research:xa,lint:Sa,reports:al,"segment-impact":Tc,"striking-distance":Dc,"update-postmortem":Tu,sites:au,"gsc-query":Po,"url-inspect":Fo,updates:ou,"traffic-anomaly":zo,"update-correlate":Bo,"ai-referrals":cc,"audit-page":vc,"second-page":_c,cannibal:lc,decaying:fc,"quick-wins":gc,"page-opportunities":mc,perf:Sc,"internal-links":pc,"ctr-underperformers":uc,"query-cluster":hc},run:async()=>{process.argv.slice(2).length===0&&process.stdout.write("Use `seo start` to set up, then `seo report` for the main report.\n")}}),rd={cannibal:`cannibalisation`,crawl:`site-crawl`,"crawl-queue":`top-fixes`,decaying:`decaying-pages`,doctor:`setup-check`,status:`setup-check`,explain:`explain-crawl-issue`,"link-recover":`link-recovery`,"query-cluster":`query-clusters`,report:`search-performance-overview`,rules:`crawler-rules`,"update-correlate":`update-correlation`},id=/* @__PURE__ */ new Set([`ai-referrals`,`audit-page`,`ctr-underperformers`,`index-coverage`,`internal-links`,`page-opportunities`,`quick-wins`,`redirect-trace`,`refresh-priorities`,`second-page`,`segment-impact`,`striking-distance`,`traffic-anomaly`,`update-postmortem`]),ad=new Set(gn().map(e=>e.id));function od(e,t){let n=e.indexOf(t);return n>=0?e[n+1]:e.find(e=>e.startsWith(`${t}=`))?.slice(t.length+1)}function sd(e){let[t,n,r]=e;if(t)return t in Ht?ve(t,{section:od(e,`--section`),action:n})??void 0:t===`reports`&&n===`run`?r&&ad.has(r)?r:void 0:t===`content`&&n===`optimize`?`content-optimization`:t===`perf`&&n===`audit`?`performance-audit`:rd[t]??(id.has(t)?t:void 0)}const cd=process.argv.slice(2);function ld(e){let t=e[1];return t&&[`list`,`describe`,`run`,`-h`,`--help`].includes(t)?e:t===void 0?[`reports`,`list`]:t.startsWith(`-`)?(process.stderr.write("Running `seo report`: `seo reports` lists reports.\n"),[`report`,...e.slice(1)]):[`reports`,`run`,...e.slice(1)]}const Q=cd[0]===`mcp`&&(cd[1]===void 0||/^-(?!h$|-help$)/.test(cd[1]))&&!process.stdin.isTTY?[`mcp`,`serve`,...cd.slice(1)]:cd[0]===`reports`?ld(cd):cd;Zu(Q);const ud=zu(Q)??Hu(Q);ud&&(process.stderr.write(`${ud}\n`),process.exit(2));const dd=Ru(Q[0]??``);dd&&!Q.some(e=>[`--help`,`-h`].includes(e))&&process.stderr.write(`${dd}\n`);const fd={agent:t()},pd=Q[0]===`telemetry`,md=Q[0]===`mcp`&&Q[1]===`serve`;!pd&&!md&&Fn(fd),md||an();const hd=await Xu(Qu,{argv:Q});hd!==void 0&&process.exit(hd),(Q.length===0||Q.length===1&&[`help`,`--help`,`-h`].includes(Q[0]??``))&&(td($u),process.exit(0)),Q[0]===`help`&&Q[1]===`all`&&(td(ed),process.exit(0));const $=Q[0]===`help`&&Q.length>1?[...Q.slice(1),`--help`]:Q;$!==Q&&(process.argv=[process.argv[0]??`node`,process.argv[1]??`seo`,...$]);const gd=$.some(e=>[`--help`,`-h`].includes(e)),_d=Q.length===1&&[`--version`,`-v`].includes(Q[0]??``),vd=gd||_d?void 0:sd($);if(gd||_d)await Rr(nd);else{vd&&jn(vd,fd);let e=Q.includes(`--json`);try{await Lr(nd,{rawArgs:$}),vd&&yt(vd,fd),[`init`,`setup`,`start`].includes($[0]??``)&&!$.includes(`--dry-run`)&&Dt(fd)}catch(t){if(t?.code===`E_NO_COMMAND`&&await Rr(nd,{rawArgs:[...$,`--help`]}),vd)vn(vd,t,fd);else if(!pd){let e=Ou($);e&&Xt(e,t,fd)}let n=Xn(t);if(e){let e=an();process.stdout.write(`${JSON.stringify({...Ct(n),...e?{updateNotice:e}:{}},null,2)}\n`)}else{process.stderr.write(`Error: ${n.message}\n`);let e=Au({args:$,error:t,report:vd});e&&process.stderr.write(`Report this bug: ${e}\n`)}process.exitCode=n.exitCode}let t=e||md?void 0:an();t&&process.stderr.write(`\n${t}\n`)}export{};
134
+ `),Tr(n,JSON.stringify([...r,`0.4`]),{mode:384})}catch{}}const $u={name:`seo`,version:Ln},ed=[{title:`Start here`,commands:[[`seo start`,`Sign in with Google, then add the MCP server and skill`],[`seo report`,`Last week across your audited sites (beta, after seo login)`],[`seo report --site <site>`,`One site: overview and every section`],[`seo report --site <site> --section quick-wins`,`One section of the report, with live page checks`]]},{title:`Commands`,commands:[[`seo page --url <url>`,`One page: live facts, index status, queries`],[`seo compare --date <date>`,`Before and after a date`],[`seo index`,`Pages that lost most of their clicks`],[`seo research <action>`,`Keyword, SERP, competitor and backlink data from your key`],[`seo data`,`Raw Search Console, Bing or Clicky rows`],[`seo crawl`,`Crawl a site and list technical issues`],[`seo internal-links`,`Pages that could link to a target page`],[`seo lint <files>`,`Check copy for AI writing patterns and length`],[`seo site new <folder>`,`Start a data-driven site on Astro and Cloudflare`]]},{title:`Setup`,commands:[[`seo login`,`Use your seoskill.dev account on this machine`],[`seo sites`,`List Search Console properties`],[`seo mcp install`,`Add the local MCP server to your agents`],[`seo providers`,`Connect DataForSEO or Ahrefs`],[`seo doctor`,`Check local auth and config`]]}],td=[...ed,{title:`More commands`,commands:[[`seo crawl-queue`,`Run a crawl and rank the fix queue`],[`seo rules`,`List crawler rule ids`],[`seo explain --rule missing_title`,`Explain a crawler rule`],[`seo redirect-trace`,`Trace redirects`],[`seo perf audit`,`Run a page performance audit`],[`seo index-coverage`,`Choose pages for URL Inspection`],[`seo ai-referrals`,`AI referral traffic in Google Analytics`],[`seo analytics google report`,`Google Analytics landing pages for visits from search`],[`seo updates`,`List official Google ranking updates`],[`seo export`,`Export report data to CSV`],[`seo reports list`,`List every structured report`],[`seo skill list`,`Show the packaged SEO skill`],[`seo auth`,`Manage Google auth`],[`seo cache`,`Manage local cache`],[`seo privacy`,`Show local storage paths`],[`seo telemetry status`,`Check anonymous usage telemetry`],[`seo reset`,`Delete saved credentials and all local data`]]}];function nd(e){ea(`seo v${$u.version}`,`Your own Search Console, Google Analytics and Bing data, as facts for your agent.`),process.stdout.write(`
135
+ `),ra(e.flatMap(e=>e.commands.map(([t,n])=>({category:e.title,id:t,name:n}))),{noun:`command`,categoryLabels:Object.fromEntries(e.map(e=>[e.title,e.title]))}),process.stdout.write(`
136
+ `),aa(`More help`,"Use `seo help <command>` or `seo <command> --help` for command help.","Use `seo help all` for the longer command list.")}const rd=b({meta:{name:`seo`,version:$u.version,description:`Local-first SEO CLI and MCP server`},subCommands:{init:Zs,analytics:La,auth:Wa,mcp:tc,doctor:wc,status:wc,privacy:iu,telemetry:fu,reset:au,cache:qa,logs:Qs,crawl:Eo,"crawl-queue":hu,content:$a,providers:Zc,export:Xo,"index-coverage":nc,"link-recover":rc,rules:sl,explain:cl,"refresh-priorities":bu,"redirect-trace":ic,skill:ru,start:Xs,setup:Ys,site:Dl,login:js,logout:Ps,report:_a,page:va,compare:ya,index:ba,data:xa,research:Sa,lint:Ca,reports:ol,"segment-impact":Ec,"striking-distance":Oc,"update-postmortem":Eu,sites:ou,"gsc-query":Fo,"url-inspect":Io,updates:su,"traffic-anomaly":Bo,"update-correlate":Vo,"ai-referrals":lc,"audit-page":yc,"second-page":vc,cannibal:uc,decaying:pc,"quick-wins":_c,"page-opportunities":hc,perf:Cc,"internal-links":mc,"ctr-underperformers":dc,"query-cluster":gc},run:async()=>{process.argv.slice(2).length===0&&process.stdout.write("Use `seo start` to set up, then `seo report` for the main report.\n")}}),id={cannibal:`cannibalisation`,crawl:`site-crawl`,"crawl-queue":`top-fixes`,decaying:`decaying-pages`,doctor:`setup-check`,status:`setup-check`,explain:`explain-crawl-issue`,"link-recover":`link-recovery`,"query-cluster":`query-clusters`,report:`search-performance-overview`,rules:`crawler-rules`,"update-correlate":`update-correlation`},ad=/* @__PURE__ */ new Set([`ai-referrals`,`audit-page`,`ctr-underperformers`,`index-coverage`,`internal-links`,`page-opportunities`,`quick-wins`,`redirect-trace`,`refresh-priorities`,`second-page`,`segment-impact`,`striking-distance`,`traffic-anomaly`,`update-postmortem`]),od=new Set(mn().map(e=>e.id));function sd(e,t){let n=e.indexOf(t);return n>=0?e[n+1]:e.find(e=>e.startsWith(`${t}=`))?.slice(t.length+1)}function cd(e){let[t,n,r]=e;if(t)return t in ht?ve(t,{section:sd(e,`--section`),action:n})??void 0:t===`reports`&&n===`run`?r&&od.has(r)?r:void 0:t===`content`&&n===`optimize`?`content-optimization`:t===`perf`&&n===`audit`?`performance-audit`:id[t]??(ad.has(t)?t:void 0)}const ld=process.argv.slice(2);function ud(e){let t=e[1];return t&&[`list`,`describe`,`run`,`-h`,`--help`].includes(t)?e:t===void 0?[`reports`,`list`]:t.startsWith(`-`)?(process.stderr.write("Running `seo report`: `seo reports` lists reports.\n"),[`report`,...e.slice(1)]):[`reports`,`run`,...e.slice(1)]}const Q=ld[0]===`mcp`&&(ld[1]===void 0||/^-(?!h$|-help$)/.test(ld[1]))&&!process.stdin.isTTY?[`mcp`,`serve`,...ld.slice(1)]:ld[0]===`reports`?ud(ld):ld;Qu(Q);const dd=Bu(Q)??Uu(Q);dd&&(process.stderr.write(`${dd}\n`),process.exit(2));const fd=zu(Q[0]??``);fd&&!Q.some(e=>[`--help`,`-h`].includes(e))&&process.stderr.write(`${fd}\n`);const pd={agent:Mt()},md=Q[0]===`telemetry`,hd=Q[0]===`mcp`&&Q[1]===`serve`;!md&&!hd&&fn(pd),hd||nn();const gd=await Zu($u,{argv:Q});gd!==void 0&&process.exit(gd),(Q.length===0||Q.length===1&&[`help`,`--help`,`-h`].includes(Q[0]??``))&&(nd(ed),process.exit(0)),Q[0]===`help`&&Q[1]===`all`&&(nd(td),process.exit(0));const $=Q[0]===`help`&&Q.length>1?[...Q.slice(1),`--help`]:Q;$!==Q&&(process.argv=[process.argv[0]??`node`,process.argv[1]??`seo`,...$]);const _d=$.some(e=>[`--help`,`-h`].includes(e)),vd=Q.length===1&&[`--version`,`-v`].includes(Q[0]??``),yd=_d||vd?void 0:cd($);if(_d||vd)await zr(rd);else{yd&&Tt(yd,pd);let e=Q.includes(`--json`);try{await Rr(rd,{rawArgs:$}),yd&&gn(yd,pd),[`init`,`setup`,`start`].includes($[0]??``)&&!$.includes(`--dry-run`)&&an(pd)}catch(t){if(t?.code===`E_NO_COMMAND`&&await zr(rd,{rawArgs:[...$,`--help`]}),yd)kn(yd,t,pd);else if(!md){let e=ku($);e&&yt(e,t,pd)}let n=We(t);if(e){let e=nn();process.stdout.write(`${JSON.stringify({...Yn(n),...e?{updateNotice:e}:{}},null,2)}\n`)}else{process.stderr.write(`Error: ${n.message}\n`);let e=ju({args:$,error:t,report:yd});e&&process.stderr.write(`Report this bug: ${e}\n`)}process.exitCode=n.exitCode}let t=e||hd?void 0:nn();t&&process.stderr.write(`\n${t}\n`)}export{};
137
137
  //# sourceMappingURL=cli.js.map