seo 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
3
- export{s as startMcpServer}from"./dist-pwGymked.js";
3
+ export{s as startMcpServer}from"./dist-DoPZlCJq.js";
@@ -10,7 +10,7 @@ import{createRequire as e}from"node:module";import t,{createHash as n,randomUUID
10
10
  `),data:{summary:Gn(t.summary)?t.summary:null,path:i?.path??null,total:a.length,columns:u,rows:l,limits:c}}}function ar(e,t,n){let r=qn(e);if(!r)return{total:0,text:``};let i=r.rows.map(Jn);return{total:i.length,text:Zn(i.slice(0,n),Yn(i,6),t)}}async function or(e,t){return(await Ht(e,{signIn:t})).filter(e=>e.permissionLevel!==`siteUnverifiedUser`).map(e=>({siteUrl:e.siteUrl,permissionLevel:e.permissionLevel??``})).sort((e,t)=>e.siteUrl.localeCompare(t.siteUrl))}const sr=e=>{try{return new URL(e.includes(`://`)?e:`https://${e.replace(/^sc-domain:/,``)}`).hostname.replace(/^www\./,``)}catch{return e}};async function cr(e){let t=e.accounts.length>1,[n,r]=await Promise.all([Promise.all(e.accounts.map(async t=>{let[n,r]=await Promise.all([or(t.google,e.signIn).catch(e=>e instanceof Error?e:Error(String(e))),Wt(t.google,{signIn:e.signIn}).catch(()=>null)]);return{email:t.email,properties:n,analytics:r}})),e.bing?Zt(e.bing).catch(e=>e instanceof Error?e:Error(String(e))):Promise.resolve(null)]),i=Array.isArray(r)?r.filter(e=>e.verified):[],a=new Map(i.map(e=>[sr(e.url),e])),o=n.flatMap(e=>e.properties instanceof Error?[]:e.properties.map(n=>({...n,...t?{account:e.email??``}:{},...Array.isArray(r)?{bing:a.get(sr(n.siteUrl))?.url??``}:{}}))),s=new Set(o.map(e=>sr(e.siteUrl))),c=i.filter(e=>!s.has(sr(e.url))),l=n.filter(e=>e.properties instanceof Error),u=n.flatMap(e=>(e.analytics??[]).flatMap(n=>n.propertySummaries.map(r=>({property:r.property.replace(`properties/`,``),name:r.displayName??``,account:n.displayName??``,...t?{signIn:e.email??``}:{}})))),d=[[`dataforseo`,`x-dataforseo-login`],[`ahrefs`,`x-ahrefs-key`]].filter(([,t])=>e.keys.get(t??``)).map(([e])=>e),f=[`siteUrl`,`permissionLevel`,...t?[`account`]:[],...Array.isArray(r)?[`bing`]:[]];return{text:[t?`Google accounts: ${e.accounts.map(e=>e.email).join(`, `)}.`:null,`Search Console properties (${o.length}):`,o.length?Zn(o,f,null):`None. These Google sign-ins have no Search Console access, so the reports cannot run.`,...l.map(e=>`${e.email??`Google`}: could not list properties. ${e.properties.message}`),u.length?`Google Analytics properties (${u.length}):\n${Zn(u,[`property`,`name`,`account`,...t?[`signIn`]:[]],null)}`:`Google Analytics: no properties readable with these sign-ins.`,e.bing===void 0?null:r===null?`Bing Webmaster: not connected.`:r instanceof Error?`Bing Webmaster: could not list sites. ${r.message}`:c.length?`Bing Webmaster sites with no Search Console property here (${c.length}): ${c.map(e=>e.url).join(`, `)}.`:`Bing Webmaster: ${i.length} verified sites; the bing column shows each one.`,`Research keys: ${d.length?d.join(`, `):`none`}.`,e.clicky?.length?`Clicky sites (seo_data with source "clicky"): ${e.clicky.join(`, `)}.`:null,`Add a Google account${e.bing===void 0?``:`, Bing`} or a key: ${e.connect}`,`Next: seo_report with a site.`].filter(Boolean).join(`
11
11
 
12
12
  `),data:{searchConsole:o,googleAnalytics:u,bing:Array.isArray(r)?i:null,research:d,clicky:e.clicky??[]}}}const lr={siteOwner:0,siteFullUser:1,siteRestrictedUser:2};function ur(e){if(e.length===0)return null;let t=e[0];if(e.length===1&&t)return t;let n,r=()=>(n??=Promise.all(e.map(async e=>({call:e,sites:await e(`GetUserSites`).then(e=>Array.isArray(e)?e:[]).catch(()=>[])}))),n);return async(e,n={})=>{let i=await r();return e===`GetUserSites`?i.flatMap(e=>e.sites):(i.find(e=>e.sites.some(e=>e.IsVerified===!0&&e.Url===n.siteUrl))?.call??t)(e,n)}}var dr=class{hint;google;bing;sites=/* @__PURE__ */ new Map;owners;constructor(e,t){this.hint=t,this.google=e.google.map(e=>({email:e.email,google:It(e.token)})),this.bing=ur(e.bing)}accounts(){return this.google.map(e=>({email:e.email,google:e.google}))}sitesOf(e){let t=this.sites.get(e);return t||(t=or(e.google,this.hint.signIn),this.sites.set(e,t)),t}ownerMap(){return this.owners??=(async()=>{let e=await Promise.all(this.google.map(async e=>({linked:e,sites:await this.sitesOf(e).catch(e=>{if(this.google.length===1)throw e;return[]})}))),t=/* @__PURE__ */ new Map;for(let{linked:n,sites:r}of e)for(let e of r){let r=lr[e.permissionLevel]??3,i=t.get(e.siteUrl);(!i||r<i.rank)&&t.set(e.siteUrl,{linked:n,rank:r})}return new Map([...t].map(([e,{linked:t}])=>[e,t]))})(),this.owners}async properties(){return[...(await this.ownerMap()).keys()].sort()}async resolve(e){if(this.google.length===0)throw new I(`NOT_CONNECTED`,`No Google account is connected. ${this.hint.signIn}`);let t=await this.properties(),n=Bn(t,e);if(n)return n;throw new I(`NO_ACCESS`,`None of the connected Google accounts (${this.google.map(e=>e.email).join(`, `)}) has a Search Console property for ${e}. ${zn(t,`they`)} ${this.hint.addGoogle}`)}async owner(e){let t=(await this.ownerMap()).get(await this.resolve(e));if(!t)throw new I(`NO_ACCESS`,`No access to ${e}.`);return t}searchAnalytics(){return async(e,t)=>Vt((await this.owner(e)).google,e,{...t,maxRows:t.maxRows??t.rowLimit??1e5},{signIn:this.hint.signIn})}async inspect(e,t){return Ut((await this.owner(e)).google,e,t,{signIn:this.hint.signIn,languageCode:`en-US`})}};let fr;function pr(e){return e.platform===`win32`?g(e.env.APPDATA??g(e.home,`AppData`,`Roaming`),`seo`,`Config`):g(e.env.XDG_CONFIG_HOME??g(e.home,`.config`),`seo`)}function mr(e){return e.platform===`darwin`?g(e.home,`Library`,`Caches`,`seo`):e.platform===`win32`?g(e.env.LOCALAPPDATA??g(e.home,`AppData`,`Local`),`seo`,`Cache`):g(e.env.XDG_CACHE_HOME??g(e.home,`.cache`),`seo`)}function hr(e){return e.platform===`darwin`?g(e.home,`Library`,`Logs`,`seo`):e.platform===`win32`?g(e.env.LOCALAPPDATA??g(e.home,`AppData`,`Local`),`seo`,`Log`):g(e.env.XDG_STATE_HOME??g(e.home,`.local`,`state`),`seo`)}function gr(e){return{env:e.env??process.env,home:e.home??c(),platform:e.platform??process.platform}}function _r(e={}){let t=gr(e),n=t.env.SEO_CONFIG_DIR??pr(t),r=t.env.SEO_CACHE_DIR??mr(t);return{configDir:n,cacheDir:r,logDir:t.env.SEO_LOG_DIR??hr(t),providerPackagesDir:g(n,`providers`),installedProviderPackagesFile:g(n,`provider-packages.json`),configFile:g(n,`config.json`),tokensFile:g(n,`tokens.json`),oauthClientFile:g(n,`oauth-client.json`),providerSecretsFile:g(n,`provider-secrets.json`),telemetryStateFile:g(n,`telemetry.json`),cacheDbFile:g(r,`cache.db`)}}function vr(e){let t=gr(e);if(!(t.platform!==`darwin`||t.env.SEO_CONFIG_DIR||t.env.XDG_CONFIG_HOME))return g(t.home,`Library`,`Preferences`,`seo`)}function yr(e={}){let t=vr(e);if(!t)return;let n=_r(e);if(S(t)&&!S(n.configDir))try{C(p(n.configDir),{recursive:!0,mode:448}),E(t,n.configDir)}catch(e){let t=e.code;if(t!==`EEXIST`&&t!==`ENOENT`)throw e}}function br(){return fr||(fr=_r(),fr)}function xr(){let e=br();return yr(),C(e.configDir,{recursive:!0,mode:448}),C(e.cacheDir,{recursive:!0,mode:448}),C(e.logDir,{recursive:!0,mode:448}),C(e.providerPackagesDir,{recursive:!0,mode:448}),e}function Sr(e){try{return O(e).size}catch{return 0}}function Cr(e,t=448){C(p(e),{recursive:!0,mode:t})}function wr(e){try{return JSON.parse(w(e,`utf8`))}catch{return}}function Tr(e,t,n){Cr(e);let r=`${e}.${process.pid}.${Date.now()}.tmp`;k(r,JSON.stringify(t,null,2),{encoding:`utf8`,mode:n}),b(r,n),E(r,e),b(e,n)}function Er(e){try{return`0${(O(e).mode&511).toString(8)}`}catch{return`missing`}}function Dr(e){D(e,{recursive:!0,force:!0})}const Or=[`auth`,`crawl_timeout`,`network`,`config`,`data`,`database`,`filesystem`,`internal`,`unknown`],kr=[`access_denied`,`auth_config_required`,`auth_expired`,`auth_required`,`crawl_timeout`,`database_constraint`,`database_corrupt`,`database_locked`,`database_read_only`,`database_unique_constraint`,`filesystem_full`,`filesystem_not_found`,`filesystem_permission`,`insufficient_data`,`internal_error`,`invalid_input`,`network_connection`,`network_dns`,`network_timeout`,`network_tls`,`optional_provider_unavailable`,`property_not_found`,`provider_unavailable`,`rate_limited`,`unknown`],Ar=[`crawl_pages_run_id_url`],jr=/* @__PURE__ */ `analytics.auth.cache.change-log.client.content.content-groups.crawl-reports.diagnose.export.gsc-query.indexnow.init.llms.logs.mcp.monitoring.okf.perf.privacy.project.projects.providers.pseo.reports.reset.schedule.server-logs.setup.skill.sites.start.telemetry.tests.updates.url-inspect`.split(`.`),Mr={ACCESS_DENIED:{errorCategory:`auth`,failureReason:`access_denied`},AUTH_CONFIG_REQUIRED:{errorCategory:`auth`,failureReason:`auth_config_required`},AUTH_EXPIRED:{errorCategory:`auth`,failureReason:`auth_expired`},AUTH_REQUIRED:{errorCategory:`auth`,failureReason:`auth_required`},INSUFFICIENT_DATA:{errorCategory:`data`,failureReason:`insufficient_data`},INTERNAL_ERROR:{errorCategory:`internal`,failureReason:`internal_error`},INVALID_INPUT:{errorCategory:`config`,failureReason:`invalid_input`},OPTIONAL_PROVIDER_UNAVAILABLE:{errorCategory:`network`,failureReason:`optional_provider_unavailable`},PROPERTY_NOT_FOUND:{errorCategory:`config`,failureReason:`property_not_found`},PROVIDER_UNAVAILABLE:{errorCategory:`network`,failureReason:`provider_unavailable`},RATE_LIMITED:{errorCategory:`network`,failureReason:`rate_limited`}},Nr=/* @__PURE__ */ new Set([`EAI_AGAIN`,`ENODATA`,`ENOTFOUND`]),Pr=/* @__PURE__ */ new Set([`ECONNREFUSED`,`ECONNRESET`,`ECONNABORTED`,`EPIPE`,`EHOSTUNREACH`,`ENETUNREACH`,`UND_ERR_SOCKET`,`UND_ERR_CONNECT`]),Fr=/* @__PURE__ */ new Set([`ETIMEDOUT`,`UND_ERR_CONNECT_TIMEOUT`,`UND_ERR_BODY_TIMEOUT`,`UND_ERR_HEADERS_TIMEOUT`]);function Ir(e,t){try{return e[t]}catch{return}}function Lr(e){if(typeof e!=`string`&&typeof e!=`number`)return;let t=String(e);return t.length>2048?t.slice(0,2048):t}function Rr(e){let t=[e],n=/* @__PURE__ */ new Set,r=[];for(;t.length>0&&r.length<4;){let e=t.shift();typeof e==`string`?r.push({message:Lr(e)}):e&&typeof e==`object`&&!n.has(e)&&(n.add(e),r.push({code:Lr(Ir(e,`code`)),message:Lr(Ir(e,`message`)),name:Lr(Ir(e,`name`))}),t.push(Ir(e,`cause`),Ir(e,`error`)))}return r}function zr(e,t){if(e.has(`SQLITE_CONSTRAINT_PRIMARYKEY`)||e.has(`SQLITE_CONSTRAINT_UNIQUE`)||/unique constraint failed/i.test(t)){let e=/unique constraint failed:\s*crawl_pages\.run_id\s*,\s*crawl_pages\.url/i.test(t)?`crawl_pages_run_id_url`:void 0;return{errorCategory:`database`,failureReason:`database_unique_constraint`,...e?{failureContext:e}:{}}}if([...e].some(e=>e.startsWith(`SQLITE_CONSTRAINT`))||/constraint failed/i.test(t))return{errorCategory:`database`,failureReason:`database_constraint`};if([...e].some(e=>e.startsWith(`SQLITE_BUSY`)||e.startsWith(`SQLITE_LOCKED`))||/database is locked/i.test(t))return{errorCategory:`database`,failureReason:`database_locked`};if([...e].some(e=>e.startsWith(`SQLITE_CORRUPT`))||e.has(`SQLITE_NOTADB`)||/database disk image is malformed|file is not a database/i.test(t))return{errorCategory:`database`,failureReason:`database_corrupt`};if([...e].some(e=>e.startsWith(`SQLITE_READONLY`))||/attempt to write a readonly database/i.test(t))return{errorCategory:`database`,failureReason:`database_read_only`}}function Br(e){if(e.has(`EACCES`)||e.has(`EPERM`)||e.has(`EROFS`))return{errorCategory:`filesystem`,failureReason:`filesystem_permission`};if(e.has(`ENOENT`))return{errorCategory:`filesystem`,failureReason:`filesystem_not_found`};if(e.has(`ENOSPC`)||e.has(`EDQUOT`)||e.has(`SQLITE_FULL`))return{errorCategory:`filesystem`,failureReason:`filesystem_full`};if([...e].some(e=>Nr.has(e)))return{errorCategory:`network`,failureReason:`network_dns`};if([...e].some(e=>Fr.has(e)))return{errorCategory:`network`,failureReason:`network_timeout`};if([...e].some(e=>Pr.has(e)))return{errorCategory:`network`,failureReason:`network_connection`};if([...e].some(e=>/^(?:CERT_|DEPTH_ZERO_SELF_SIGNED_CERT$|ERR_SSL_|ERR_TLS_|SELF_SIGNED_CERT_IN_CHAIN$|UNABLE_TO_VERIFY_LEAF_SIGNATURE$)/.test(e)))return{errorCategory:`network`,failureReason:`network_tls`}}function Vr(e){let t=Rr(e),n=new Set(t.map(e=>e.code?.toUpperCase()).filter(e=>!!e)),r=zr(n,t.map(e=>e.message).filter(e=>!!e).join(`
13
- `));if(r)return r;let i=Br(n);if(i)return i;if(t.some(e=>e.name===`AbortError`||e.name===`TimeoutError`))return{errorCategory:`crawl_timeout`,failureReason:`crawl_timeout`};let a=t.find(e=>e.name===`ProviderError`),o=yt[a?.code],s=o&&Mr[o];if(s)return s;for(let e of n){let t=Mr[e];if(t)return t}return t.some(e=>e.name===`CLIError`)?{errorCategory:`config`,failureReason:`invalid_input`}:{errorCategory:`unknown`,failureReason:`unknown`}}function Hr(e){try{return Vr(e)}catch{return{errorCategory:`unknown`,failureReason:`unknown`}}}function Ur(e){return typeof e==`string`&&jr.includes(e)}function Wr(e){let t=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate())),n=t.getUTCDay()||7;t.setUTCDate(t.getUTCDate()+4-n);let r=new Date(Date.UTC(t.getUTCFullYear(),0,1)),i=Math.ceil(((t.getTime()-r.getTime())/864e5+1)/7);return`${t.getUTCFullYear()}-W${String(i).padStart(2,`0`)}`}function Gr(e){let t=e?.toLowerCase()??``;return t?t.includes(`claude`)?`claude-code`:t.includes(`cursor`)?`cursor`:t.includes(`codex`)||t.includes(`openai`)?`codex`:`unknown`:null}const Kr=`0.15.0`,qr=/* @__PURE__ */ `affected-urls.ai-referrals.audit-page.audit-urls.cannibalisation.compare-crawls.content-optimization.crawl-diff.site-crawl.ctr-underperformers.decaying-pages.setup-check.explain-crawl-issue.crawl-report.index-coverage.index-coverage-plan.index-monitor.index-watch.internal-links.keyword-metrics.link-evidence.link-recovery.crawl-history.crawler-rules.measure-change.page-opportunities.performance-audit.query-clusters.quick-wins.ranked-keywords.redirect-trace.second-page.segment-impact.serp-results.serp-competitors.striking-distance.top-fixes.traffic-anomaly.update-correlation.search-performance-overview.refresh-priorities.technical-watch.update-postmortem`.split(`.`),Jr=[{days:1,event:`active_d1`},{days:7,event:`active_d7`},{days:30,event:`active_d30`}],Yr=[`GITHUB_ACTIONS`,`GITLAB_CI`,`CIRCLECI`,`TRAVIS`,`BUILDKITE`,`JENKINS_URL`,`TEAMCITY_VERSION`,`TF_BUILD`,`BITBUCKET_BUILD_NUMBER`,`CODEBUILD_BUILD_ID`];function Xr(e){return e.env??process.env}function Zr(e){return e.now??/* @__PURE__ */ new Date}function Qr(e){return e.stateFile??br().telemetryStateFile}function $r(e){return e===`1`||e?.toLowerCase()===`true`}function ei(e=process.env){return e.NODE_ENV===`test`||!!e.NODE_TEST_CONTEXT||$r(e.CI)||Yr.some(t=>!!e[t])}function ti(e){if(ei(e))return`ci`;if($r(e.DO_NOT_TRACK))return`do_not_track`;if($r(e.SEOSKILL_TELEMETRY_DISABLED)||$r(e.SEO_TELEMETRY_DISABLED))return`environment`}function ni(e){return typeof e==`string`&&[`first_run`,`setup_complete`,`first_audit_complete`,`active_d1`,`active_d7`,`active_d30`].includes(e)}function ri(e){return typeof e==`string`&&qr.includes(e)}function ii(e){return typeof e==`string`&&/^\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3])$/.test(e)}function ai(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return typeof t.telemetryEnabled==`boolean`&&typeof t.firstRunAt==`string`&&Number.isFinite(Date.parse(t.firstRunAt))&&ii(t.cohort)&&Array.isArray(t.sentMilestones)&&t.sentMilestones.every(ni)}function oi(e){if(!S(e))return{kind:`missing`};let t=wr(e);return ai(t)?{kind:`valid`,state:t}:{kind:`invalid`}}function si(e,t){Tr(e,t,384)}function ci(e,t=!0){let n=Zr(e);return{telemetryEnabled:t,firstRunAt:n.toISOString(),cohort:Wr(n),sentMilestones:[]}}function li(e={}){let t=e.env??process.env;return Gr(e.clientName)||(t.CLAUDECODE||t.CLAUDE_CODE?`claude-code`:t.CURSOR_AGENT||t.CURSOR_TRACE_ID||t.CURSOR_SESSION_ID?`cursor`:t.CODEX_HOME||t.CODEX_THREAD_ID||t.CODEX_SANDBOX?`codex`:`cli`)}function ui(e=process.env){let t=(e.AI_AGENT||e.AGENT)?.trim().toLowerCase();if(t)return t.slice(0,40);let n=li({env:e});return n===`cli`?e.GEMINI_CLI?`gemini`:e.OPENCODE||e.OPENCODE_CLIENT?`opencode`:e.ANTIGRAVITY_AGENT?`antigravity`:e.AUGMENT_AGENT?`augment`:e.COPILOT_MODEL||e.COPILOT_ALLOW_ALL?`github-copilot`:e.REPL_ID?`replit`:null:n}function di(e){return e.agent??li({clientName:e.clientName,env:Xr(e)})}function fi(e){return e.replace(/^v/,``).split(`.`)[0]||`unknown`}function pi(e,t,n={},r={}){return{event:e,version:r.version??Kr,agent:di(r),os:r.processPlatform??u(),arch:r.processArch??s(),node:fi(r.nodeVersion??process.version),cohort:t.cohort,schema:2,...n.errorCategory?{errorCategory:n.errorCategory}:{},...n.failureReason?{failureReason:n.failureReason}:{},...n.failureContext?{failureContext:n.failureContext}:{},...n.operation?{operation:n.operation}:{},...n.report?{report:n.report}:{}}}function mi(e,t){let n=t.fetch??globalThis.fetch;try{n(t.endpoint??`https://seoskill.dev/api/t`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(e),signal:AbortSignal.timeout(2e3)}).catch(()=>void 0)}catch{}}function hi(e){let t=Qr(e),n=ti(Xr(e)),r=oi(t);return n?{enabled:!1,reason:n,stateFile:t,...r.kind===`valid`?{state:r.state}:{}}:r.kind===`invalid`?{enabled:!1,reason:`invalid_state`,stateFile:t}:r.kind===`valid`&&!r.state.telemetryEnabled?{enabled:!1,reason:`local_setting`,stateFile:t,state:r.state}:{enabled:!0,stateFile:t,...r.kind===`valid`?{state:r.state}:{}}}function gi(e={}){return hi(e)}function _i(e,t={}){let n=Qr(t),r=oi(n),i=r.kind===`valid`?r.state:ci(t,e);return i.telemetryEnabled=e,si(n,i),hi(t)}function vi(e){let t=hi(e);return t.enabled?t.state:void 0}function yi(e={}){let t=hi(e);if(!t.enabled)return t;let n=t.stateFile,r=t.state??ci(e),i=[];if(!r.sentMilestones.includes(`first_run`)){try{(e.notice??(e=>process.stderr.write(e)))(`SEO Skill collects anonymous usage data to improve the tool (event name,
13
+ `));if(r)return r;let i=Br(n);if(i)return i;if(t.some(e=>e.name===`AbortError`||e.name===`TimeoutError`))return{errorCategory:`crawl_timeout`,failureReason:`crawl_timeout`};let a=t.find(e=>e.name===`ProviderError`),o=yt[a?.code],s=o&&Mr[o];if(s)return s;for(let e of n){let t=Mr[e];if(t)return t}return t.some(e=>e.name===`CLIError`)?{errorCategory:`config`,failureReason:`invalid_input`}:{errorCategory:`unknown`,failureReason:`unknown`}}function Hr(e){try{return Vr(e)}catch{return{errorCategory:`unknown`,failureReason:`unknown`}}}function Ur(e){return typeof e==`string`&&jr.includes(e)}function Wr(e){let t=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate())),n=t.getUTCDay()||7;t.setUTCDate(t.getUTCDate()+4-n);let r=new Date(Date.UTC(t.getUTCFullYear(),0,1)),i=Math.ceil(((t.getTime()-r.getTime())/864e5+1)/7);return`${t.getUTCFullYear()}-W${String(i).padStart(2,`0`)}`}function Gr(e){let t=e?.toLowerCase()??``;return t?t.includes(`claude`)?`claude-code`:t.includes(`cursor`)?`cursor`:t.includes(`codex`)||t.includes(`openai`)?`codex`:`unknown`:null}const Kr=`0.15.1`,qr=/* @__PURE__ */ `affected-urls.ai-referrals.audit-page.audit-urls.cannibalisation.compare-crawls.content-optimization.crawl-diff.site-crawl.ctr-underperformers.decaying-pages.setup-check.explain-crawl-issue.crawl-report.index-coverage.index-coverage-plan.index-monitor.index-watch.internal-links.keyword-metrics.link-evidence.link-recovery.crawl-history.crawler-rules.measure-change.page-opportunities.performance-audit.query-clusters.quick-wins.ranked-keywords.redirect-trace.second-page.segment-impact.serp-results.serp-competitors.striking-distance.top-fixes.traffic-anomaly.update-correlation.search-performance-overview.refresh-priorities.technical-watch.update-postmortem`.split(`.`),Jr=[{days:1,event:`active_d1`},{days:7,event:`active_d7`},{days:30,event:`active_d30`}],Yr=[`GITHUB_ACTIONS`,`GITLAB_CI`,`CIRCLECI`,`TRAVIS`,`BUILDKITE`,`JENKINS_URL`,`TEAMCITY_VERSION`,`TF_BUILD`,`BITBUCKET_BUILD_NUMBER`,`CODEBUILD_BUILD_ID`];function Xr(e){return e.env??process.env}function Zr(e){return e.now??/* @__PURE__ */ new Date}function Qr(e){return e.stateFile??br().telemetryStateFile}function $r(e){return e===`1`||e?.toLowerCase()===`true`}function ei(e=process.env){return e.NODE_ENV===`test`||!!e.NODE_TEST_CONTEXT||$r(e.CI)||Yr.some(t=>!!e[t])}function ti(e){if(ei(e))return`ci`;if($r(e.DO_NOT_TRACK))return`do_not_track`;if($r(e.SEOSKILL_TELEMETRY_DISABLED)||$r(e.SEO_TELEMETRY_DISABLED))return`environment`}function ni(e){return typeof e==`string`&&[`first_run`,`setup_complete`,`first_audit_complete`,`active_d1`,`active_d7`,`active_d30`].includes(e)}function ri(e){return typeof e==`string`&&qr.includes(e)}function ii(e){return typeof e==`string`&&/^\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3])$/.test(e)}function ai(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return typeof t.telemetryEnabled==`boolean`&&typeof t.firstRunAt==`string`&&Number.isFinite(Date.parse(t.firstRunAt))&&ii(t.cohort)&&Array.isArray(t.sentMilestones)&&t.sentMilestones.every(ni)}function oi(e){if(!S(e))return{kind:`missing`};let t=wr(e);return ai(t)?{kind:`valid`,state:t}:{kind:`invalid`}}function si(e,t){Tr(e,t,384)}function ci(e,t=!0){let n=Zr(e);return{telemetryEnabled:t,firstRunAt:n.toISOString(),cohort:Wr(n),sentMilestones:[]}}function li(e={}){let t=e.env??process.env;return Gr(e.clientName)||(t.CLAUDECODE||t.CLAUDE_CODE?`claude-code`:t.CURSOR_AGENT||t.CURSOR_TRACE_ID||t.CURSOR_SESSION_ID?`cursor`:t.CODEX_HOME||t.CODEX_THREAD_ID||t.CODEX_SANDBOX?`codex`:`cli`)}function ui(e=process.env){let t=(e.AI_AGENT||e.AGENT)?.trim().toLowerCase();if(t)return t.slice(0,40);let n=li({env:e});return n===`cli`?e.GEMINI_CLI?`gemini`:e.OPENCODE||e.OPENCODE_CLIENT?`opencode`:e.ANTIGRAVITY_AGENT?`antigravity`:e.AUGMENT_AGENT?`augment`:e.COPILOT_MODEL||e.COPILOT_ALLOW_ALL?`github-copilot`:e.REPL_ID?`replit`:null:n}function di(e){return e.agent??li({clientName:e.clientName,env:Xr(e)})}function fi(e){return e.replace(/^v/,``).split(`.`)[0]||`unknown`}function pi(e,t,n={},r={}){return{event:e,version:r.version??Kr,agent:di(r),os:r.processPlatform??u(),arch:r.processArch??s(),node:fi(r.nodeVersion??process.version),cohort:t.cohort,schema:2,...n.errorCategory?{errorCategory:n.errorCategory}:{},...n.failureReason?{failureReason:n.failureReason}:{},...n.failureContext?{failureContext:n.failureContext}:{},...n.operation?{operation:n.operation}:{},...n.report?{report:n.report}:{}}}function mi(e,t){let n=t.fetch??globalThis.fetch;try{n(t.endpoint??`https://seoskill.dev/api/t`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(e),signal:AbortSignal.timeout(2e3)}).catch(()=>void 0)}catch{}}function hi(e){let t=Qr(e),n=ti(Xr(e)),r=oi(t);return n?{enabled:!1,reason:n,stateFile:t,...r.kind===`valid`?{state:r.state}:{}}:r.kind===`invalid`?{enabled:!1,reason:`invalid_state`,stateFile:t}:r.kind===`valid`&&!r.state.telemetryEnabled?{enabled:!1,reason:`local_setting`,stateFile:t,state:r.state}:{enabled:!0,stateFile:t,...r.kind===`valid`?{state:r.state}:{}}}function gi(e={}){return hi(e)}function _i(e,t={}){let n=Qr(t),r=oi(n),i=r.kind===`valid`?r.state:ci(t,e);return i.telemetryEnabled=e,si(n,i),hi(t)}function vi(e){let t=hi(e);return t.enabled?t.state:void 0}function yi(e={}){let t=hi(e);if(!t.enabled)return t;let n=t.stateFile,r=t.state??ci(e),i=[];if(!r.sentMilestones.includes(`first_run`)){try{(e.notice??(e=>process.stderr.write(e)))(`SEO Skill collects anonymous usage data to improve the tool (event name,
14
14
  fixed report or command name, fixed failure classification, version, agent,
15
15
  OS, architecture, Node major, and install week; never URLs, report data, error
16
16
  messages, command arguments, or identifiers of any kind).
@@ -349,7 +349,7 @@ a!==-1&&(Pf(i,a,0,t,r),n.push(...t))}),this._make(n)}function Jf(e){return nu(e?
349
349
  th:th tn:ar tr:tr tw:zh-TW ua:uk us:en uy:es ve:es vn:vi za:en`.split(/\s+/).map(e=>e.split(`:`)));function NU(e){return MU.get(e.toLowerCase())??null}function PU(e,t){return{location_name:AU(e),language_code:t??jU(e)}}function FU(e){return CU(e,`dataforseo`)}async function IU([e,t],n){let r=(await Nt(await TU(`dataforseo`,new Request(`https://api.dataforseo.com/${n}`,{headers:{authorization:`Basic ${btoa(`${e}:${t}`)}`}})),`DataForSEO`)).tasks?.[0];if(!r||r.status_code>=4e4)throw new I(`PROVIDER_ERROR`,`dataforseo: no list at ${n}`);return r.result??[]}async function LU([e,t],n,r){let i=(await Nt(await TU(`dataforseo`,new Request(`https://api.dataforseo.com/${n}`,{method:`POST`,headers:{authorization:`Basic ${btoa(`${e}:${t}`)}`,"content-type":`application/json`},body:JSON.stringify([r])})),`DataForSEO`)).tasks?.[0];if(i?.status_code===RU)return{cost:i.cost??0,result:null,partial:!1};if(!i||i.status_code>=4e4&&i.status_code!==zU)throw Object.assign(new I(`PROVIDER_ERROR`,`dataforseo: ${i?.status_message??`no task result`}`),{cost:i?.cost??0});return{cost:i.cost??0,result:i.result?.[0]??null,partial:i.status_code===zU}}const RU=40102,zU=40106;async function BU(e,t){let n={cost:0,partial:!1,missed:[]};return{rows:await VU(e,t,n),costUsd:Math.round(n.cost*1e4)/1e4,partial:n.partial,missed:n.missed}}async function VU(e,t,n){let r=NU(t.country);if(!r&&t.action!==`serp`&&t.action!==`backlinks`)throw new I(`INVALID_INPUT`,`DataForSEO has no ${t.action} data for country "${t.country}".`);let i=PU(t.country,r),a=async(t,r)=>{let a=await LU(e,t,{...i,...r}).catch(e=>{throw n.cost+=e.cost??0,e});return n.cost+=a.cost,n.partial||=a.partial,a.result?.items??[]},o=(e,t)=>t.split(`.`).reduce((e,t)=>e&&typeof e==`object`?e[t]:void 0,e),s=(e,t)=>Object.fromEntries(Object.entries(t).map(([t,n])=>[t,o(e,n)??null]));return t.action===`keyword_metrics`?(await a(`v3/dataforseo_labs/google/keyword_overview/live`,{keywords:t.keywords.slice(0,700)})).map(e=>s(e,{keyword:`keyword`,search_volume:`keyword_info.search_volume`,keyword_difficulty:`keyword_properties.keyword_difficulty`,cpc:`keyword_info.cpc`,competition:`keyword_info.competition`,main_intent:`search_intent_info.main_intent`})):t.action===`ranked_keywords`?(await a(`v3/dataforseo_labs/google/ranked_keywords/live`,{target:UU(t.target),limit:t.limit,order_by:[`ranked_serp_element.serp_item.etv,desc`]})).map(e=>s(e,{keyword:`keyword_data.keyword`,position:`ranked_serp_element.serp_item.rank_absolute`,url:`ranked_serp_element.serp_item.url`,search_volume:`keyword_data.keyword_info.search_volume`,etv:`ranked_serp_element.serp_item.etv`,keyword_difficulty:`keyword_data.keyword_properties.keyword_difficulty`,main_intent:`keyword_data.search_intent_info.main_intent`})):t.action===`serp`?OU(t.keywords.slice(0,5),n.missed,async e=>(await a(`v3/serp/google/organic/live/advanced`,{keyword:e,depth:Math.min(t.limit,100)})).filter(e=>e.type===`organic`).map(t=>{let n=s(t,{position:`rank_absolute`,url:`url`,domain:`domain`,title:`title`});if(/^https?:\/\/(www\.)?google\.[a-z.]+\/goto\b/.test(String(n.url))){let e=/^https?:\/\/([^/\s›]+)/.exec(String(t.breadcrumb??``));n.url=null,n.domain=e?.[1]??null}return{keyword:e,...n}})):t.action===`backlinks`?(await a(`v3/backlinks/referring_domains/live`,{target:UU(t.target),limit:t.limit,order_by:[`rank,desc`]})).map(e=>s(e,{domain:`domain`,rank:`rank`,backlinks:`backlinks`,referring_pages:`referring_pages`,first_seen:`first_seen`})):(await a(`v3/dataforseo_labs/google/competitors_domain/live`,{target:UU(t.target),limit:t.limit})).map(e=>s(e,{domain:`domain`,shared_keywords:`intersections`,avg_position:`avg_position`,organic_keywords:`full_domain_metrics.organic.count`,organic_etv:`full_domain_metrics.organic.etv`}))}function HU(e){return/\/./.test(UU(e))}function UU(e){return e.replace(/^https?:\/\//,``).replace(/\/$/,``)}async function WU(e,t,n){let r=async(t,n)=>{let r=new URL(`https://api.ahrefs.com/v3/${t}`);for(let[e,t]of Object.entries(n))r.searchParams.set(e,t);return Nt(await TU(`ahrefs`,new Request(r,{headers:{authorization:`Bearer ${e}`,accept:`application/json`}})),`Ahrefs`)},i=(/* @__PURE__ */ new Date()).toISOString().slice(0,10);return t.action===`keyword_metrics`?((await r(`keywords-explorer/overview`,{select:`keyword,volume,difficulty,cpc,traffic_potential,intents`,country:t.country,keywords:t.keywords.slice(0,100).join(`,`)})).keywords??[]).map(GU):t.action===`ranked_keywords`?(await r(`site-explorer/organic-keywords`,{select:`keyword,best_position,best_position_url,volume,sum_traffic,keyword_difficulty,cpc`,target:t.target,country:t.country,date:i,mode:HU(t.target)?`exact`:`subdomains`,limit:String(t.limit),order_by:`sum_traffic:desc`})).keywords??[]:t.action===`serp`?OU(t.keywords.slice(0,5),n,async e=>((await r(`serp-overview/serp-overview`,{select:`position,type,url,title,domain_rating,traffic,keywords`,keyword:e,country:t.country,top_positions:String(Math.min(t.limit,100))})).positions??[]).map(t=>({keyword:e,...t}))):t.action===`backlinks`?(await r(`site-explorer/refdomains`,{select:`domain,domain_rating,links_to_target,dofollow_links,first_seen,last_seen`,target:t.target,mode:HU(t.target)?`exact`:`subdomains`,history:`live`,limit:String(t.limit),order_by:`domain_rating:desc`})).refdomains??[]:(await r(`site-explorer/organic-competitors`,{select:`competitor_domain,keywords_common,keywords_competitor,keywords_target,share,traffic,domain_rating`,target:t.target,country:t.country,date:i,mode:`subdomains`,limit:String(t.limit),order_by:`keywords_common:desc`})).competitors??[]}function GU(e){let t=e.intents;if(!t||typeof t!=`object`)return e;let n=Object.entries(t).filter(([,e])=>e===!0).map(([e])=>e.replace(/^is_/,``));return{...e,intents:n.join(` `)||null}}function KU(e){let t=e.rows.length;if(e.detail===`full`)return`${e.title}\n\n${JSON.stringify(e.rows.slice(e.offset,e.offset+e.limit))}`;let n=e.detail===`summary`?Math.min(e.limit,25):e.limit,r=e.rows.slice(e.offset,e.offset+n),i=[...new Set(r.flatMap(e=>Object.keys(e)))],a=e.offset+r.length;return[e.title,...e.notes??[],t===0?`No rows.`:`Rows ${e.offset+1} to ${a} of ${t}:\n${Zn(r,i,e.origin??null)}`,a<t?`More: ${e.more?e.more(a):`detail "rows", offset ${a}`}.`:``].filter(Boolean).join(`
350
350
 
351
351
  `)}const qU=[`query`,`page`,`date`,`country`,`device`,`searchAppearance`],JU=25e3;async function YU(e,t){let n=e.dimensions?.length?e.dimensions:[`query`],r=e.filters??[],i=An(e,28),a=jn(i),o={dimensions:n,type:`web`,maxRows:JU,...r.length?{dimensionFilterGroups:[{groupType:`and`,filters:r}]}:{}},[s,c]=await Promise.all([t.searchAnalytics(e.site,{...o,...i}),t.searchAnalytics(e.site,{...o,...a})]),l=n.indexOf(`page`),u=e=>l<0?e:Ej(e,{pageKeyIndex:l}).rows,d=u(s.rows),f=u(c.rows),p=e=>e.join(`\0`),m=n.indexOf(`date`),h=(e,t)=>p(e.map((e,n)=>n===m?String(Math.round((Date.parse(e)-Date.parse(t))/864e5)):e)),g=new Map(f.map(e=>[h(e.keys,a.startDate),e])),_=/* @__PURE__ */ new Set,v=d.map(e=>{let t=h(e.keys,i.startDate);_.add(t);let r=g.get(t);return{...Object.fromEntries(n.map((t,n)=>[t,e.keys[n]])),clicks:e.clicks,impressions:e.impressions,ctr:e.ctr,position:e.position,...m>=0?{previous_date:r?.keys[m]??null}:{},previous_clicks:r?.clicks??0,clicks_change:e.clicks-(r?.clicks??0),previous_position:r?.position??null}});if(m<0)for(let e of f)_.has(h(e.keys,a.startDate))||v.push({...Object.fromEntries(n.map((t,n)=>[t,e.keys[n]])),clicks:0,impressions:0,ctr:0,position:null,previous_clicks:e.clicks,clicks_change:-e.clicks,previous_position:e.position});v.sort((e,t)=>m>=0?String(e.date).localeCompare(String(t.date)):Number(t.clicks??0)-Number(e.clicks??0));let y=[];if(!n.includes(`query`)&&!n.includes(`page`)){let e=e=>v.reduce((t,n)=>t+Number(n[e]??0),0);y.push(`Totals: clicks ${e(`clicks`)}, impressions ${e(`impressions`)}; previous period clicks ${e(`previous_clicks`)}.`)}y.push(`Compared with ${a.startDate} to ${a.endDate}${c.rowsFetched>=JU?` (earlier period hit the row limit)`:``}. 0 clicks with no position means the row had no impressions in the current period.`),l>=0&&y.push(`Page URLs with #fragments are merged into their page.`),s.rowsFetched>=JU&&y.push(`Search Console has more than ${JU} rows; add a filter to narrow it.`),y.push(`Search Console hides some rare queries for privacy, so query rows do not add up to the site totals.`);let b=e.detail??`summary`;return{text:`${KU({title:`Search Console ${e.site}, ${i.startDate} to ${i.endDate}, web, by ${n.join(` and `)}.`,rows:v,offset:e.offset??0,limit:50,detail:b,notes:y,origin:Mn(e.site),more:r=>t.call(`data`,{site:e.site,dimensions:n.join(`,`),detail:`rows`,offset:r})})}\n\n${En}`,data:{range:i,comparisonRange:a,total:v.length,offset:e.offset??0,rows:v.slice(e.offset??0,(e.offset??0)+50)}}}const XU=e=>Array.isArray(e)?e:[],ZU=e=>e.map(e=>e.url).filter(e=>typeof e==`string`),QU=e=>{let t=XU(e.items).find(e=>e.type===`ai_overview`);if(!t)return null;let n=XU(t.items);return{markdown:typeof t.markdown==`string`?t.markdown:n.map(e=>String(e.markdown??``)).join(`
352
- `),sources:ZU([...XU(t.references),...n.flatMap(e=>XU(e.references))]),model:null}},$U=[{name:`chatgpt`,means:`a ChatGPT search, read by DataForSEO`,path:`v3/ai_optimization/chat_gpt/llm_scraper/live/advanced`,languages:`v3/ai_optimization/chat_gpt/llm_scraper/languages`,task:(e,t)=>({keyword:e,...t}),shown:e=>{let t=typeof e.markdown==`string`?e.markdown:``,n=ZU(XU(e.sources));return!t&&!n.length?null:{markdown:t,sources:n,model:typeof e.model==`string`?e.model:null}}},{name:`google_ai_mode`,means:`Google AI Mode`,path:`v3/serp/google/ai_mode/live/advanced`,languages:`v3/serp/google/ai_mode/languages`,task:(e,t)=>({keyword:e,...t}),shown:QU},{name:`google_ai_overview`,means:`the AI Overview on a Google search`,path:`v3/serp/google/organic/live/advanced`,languages:`v3/serp/google/languages`,task:(e,t)=>({keyword:e,depth:10,load_async_ai_overview:!0,...t}),shown:QU}];function eW(e){return e.replace(/\[([^\]]*)\]\(https?:\/\/[^)]*\)/g,(e,t)=>/^\s*(\d+|[\w-]+(\.[\w-]+)+)\s*$/.test(t)?` `:t).replace(/https?:\/\/\S+/g,` `)}function tW(e,t){try{let n=new URL(e).hostname.toLowerCase();return n===t||n.endsWith(`.${t}`)}catch{return!1}}async function nW(e,t,n,r,i){if(typeof r==`string`)return{result:`unavailable`,model:null,cited_url:null,answered_at:null,cost_usd:0,error:r};try{let a=await LU(e,t.path,t.task(n,r)),o={answered_at:String(a.result?.datetime??``).slice(0,10)||null,cost_usd:a.cost},s=a.result&&t.shown(a.result);if(!s)return{result:`no_answer`,model:null,cited_url:null,...o};let c={model:s.model,...o},l=s.markdown.match(/https?:\/\/[^\s)\]]+/g)??[],u=[...s.sources,...l].find(e=>tW(e,i.domain));if(u)return{result:`cited`,cited_url:u,...c};let d=eW(s.markdown);return{result:d.toLowerCase().includes(i.domain)||hC(d,i.terms)?`mentioned`:`neither`,cited_url:null,...c}}catch(e){if(e instanceof I&&e.code===`NO_KEY`)throw e;return{result:`unavailable`,model:null,cited_url:null,answered_at:null,cost_usd:e.cost??0,error:e instanceof Error?e.message.slice(0,200):`failed`}}}async function rW(e,t){let n=t.toUpperCase(),r=NU(t)??PU(t,null).language_code,[i,...a]=await Promise.all([IU(e,`v3/ai_optimization/chat_gpt/llm_scraper/locations`),...$U.map(t=>IU(e,t.languages))]),o=i.find(e=>e.location_type===`Country`&&e.country_iso_code===n),s=o?{location_code:Number(o.location_code)}:{location_name:PU(t,null).location_name};return new Map($U.map((e,n)=>{if(e.name===`chatgpt`&&!o)return[e.name,`DataForSEO's ChatGPT search has no data for country "${t}".`];let i=(a[n]??[]).some(e=>e.language_code===r);return[e.name,i?{...s,language_code:r}:`DataForSEO's ${e.name} has no "${r}" language, the language used for country "${t}".`]}))}const iW=e=>Math.round(e*1e4)/1e4;async function aW(e,t,n,r){let i=FU(t),a=oC(e.site)?.domain;if(!a)throw new I(`INVALID_INPUT`,`ai_answers needs a site with a domain, not "${e.site}".`);let o=An({},90),s=((await SU({site:e.site,...o},n.searchAnalytics)).items??[]).filter(e=>e.askable).slice(0,10),c={domain:a,terms:fC({siteUrl:e.site})},l=s.length?await rW(i,e.country):/* @__PURE__ */ new Map,u=await Promise.all(s.flatMap(e=>$U.map(async t=>({question:e.query,impressions:e.impressions,engine:t.name,...await nW(i,t,e.query,l.get(t.name)??`no market`,c)})))),d=iW(u.reduce((e,t)=>e+t.cost_usd,0)),f=e=>u.filter(t=>t.result===e).length,p=[s.length?`The top ${s.length} askable questions in Search Console’s AI-shaped queries for ${o.startDate} to ${o.endDate}, by impressions. No calls were made for other questions.`:`No askable questions in Search Console’s AI-shaped queries for ${o.startDate} to ${o.endDate}, so no answers were checked and nothing was spent.`,...s.length?[`Cited ${f(`cited`)}, mentioned ${f(`mentioned`)}, neither ${f(`neither`)}, no answer shown ${f(`no_answer`)}, unavailable ${f(`unavailable`)}. DataForSEO cost: $${d}.`,`Each row is one sample of an answer that changes from run to run, not a ranking.`,`cited: the answer lists or links a page on ${a} or a subdomain. mentioned: no such source, but the answer text names ${[a,...c.terms].join(` or `)}, a text match. no_answer: the engine showed no AI answer for the question. model: the model DataForSEO reports for the answer, when it reports one.`,`Engines: ${$U.map(e=>`${e.name} is ${e.means}`).join(`; `)}.`]:[]];return{text:KU({title:`AI answers for ${e.site}, ${e.country}.`,rows:u,offset:e.offset,limit:50,detail:e.detail,notes:p,more:r}),data:{provider:`dataforseo`,action:`ai_answers`,site:e.site,country:e.country,questionsFrom:{section:`ai-queries`,...o},questions:s.map(e=>e.query),costUsd:d,total:u.length,offset:e.offset,rows:u.slice(e.offset,e.offset+50)}}}async function oW(e,t,n){let r=e.length===1?e[0]:void 0;if(r)return r.google;let i=(await Promise.all(e.map(async e=>(await Wt(e.google,{signIn:n}).catch(()=>[])).some(e=>e.propertySummaries.some(e=>e.property===`properties/${t}`))?e:null))).find(Boolean);if(i)return i.google;throw new I(`NO_ACCESS`,`No connected Google account can read Google Analytics property ${t}. Call seo_sites to list the properties.`)}async function sW(e,t,n){let r=An(e,28),i=e.offset??0,a=Math.min(1e3,i+50),o=await Kt(t,e.property,{dateRanges:[r],dimensions:[{name:`landingPage`}],metrics:[{name:`sessions`},{name:`totalUsers`},{name:`engagedSessions`},{name:`keyEvents`}],dimensionFilter:{filter:{fieldName:`sessionDefaultChannelGroup`,stringFilter:{value:`Organic Search`}}},orderBys:[{metric:{metricName:`sessions`},desc:!0}],limit:a},n),s=[`Sessions in the Organic Search default channel group.`];o.metadata?.subjectToThresholding&&s.push(`Google Analytics applied thresholding; small rows can be hidden.`),o.metadata?.dataLossFromOtherRow&&s.push(`Some rows were grouped into (other) by Google Analytics.`),(o.rowCount??0)>a&&s.push(`${o.rowCount} landing pages in total; ${a} read.`);let c=(o.rows??[]).map(e=>({page:e.dimensionValues[0]?.value??``,sessions:Number(e.metricValues[0]?.value??0),users:Number(e.metricValues[1]?.value??0),engaged_sessions:Number(e.metricValues[2]?.value??0),key_events:Number(e.metricValues[3]?.value??0)}));return{text:KU({title:`Google Analytics property ${e.property}: search landing pages, ${r.startDate} to ${r.endDate}.`,rows:c,offset:i,limit:50,detail:e.detail??`summary`,notes:s,more:n.call}),data:{range:r,total:c.length,offset:i,rows:c.slice(i,i+50)}}}function cW(e){try{let t=new URL(e);return t.protocol!==`http:`&&t.protocol!==`https:`?void 0:t.hostname.toLowerCase().replace(/\.$/,``)||void 0}catch{return}}function lW(e){if(!e.startsWith(`sc-domain:`))return;let t=e.slice(10).trim().toLowerCase();if(t&&!/[/:]/.test(t))return t.replace(/\.$/,``)||void 0}function uW(e,t){let n=lW(e);return n?t===n||t.endsWith(`.${n}`)?`domain`:void 0:cW(e)===t?`hostname`:void 0}function dW(e,t){return t.flatMap(t=>{let n=t.stream.webStreamData?.defaultUri;if(!n)return[];let r=cW(n);if(!r)return[];let i=uW(e,r);return i?[{...t,defaultUri:n,hostname:r,match:i}]:[]})}const fW=e=>{let t=e.replace(/^sc-domain:/,``);try{return new URL(t.includes(`://`)?t:`https://${t}`).hostname}catch{return t}},pW=e=>e.toLowerCase().replace(/[^a-z0-9]/g,``);function mW(e,t){let n=fW(e).replace(/^www\./,``),r=[pW(n),pW(n.split(`.`)[0]??``)].filter(e=>e.length>=3),i=e=>+!r.some(t=>pW(e.propertyName).includes(t));return[...t].sort((e,t)=>i(e)-i(t)||e.property.localeCompare(t.property,`en`,{numeric:!0}))}function hW(e,t){let n=fW(e).replace(/^www\./,``),r=e=>e.hostname===n||e.hostname===`www.${n}`?0:1;return[...t].sort((e,t)=>r(e)-r(t)||e.hostname.length-t.hostname.length||e.property.localeCompare(t.property,`en`,{numeric:!0}))}const gW=/* @__PURE__ */ new WeakMap;function _W(e,t){let n=gW.get(e)??/* @__PURE__ */ new Map;gW.set(e,n);let r=n.get(t);if(r&&Date.now()-r.at<3e5)return r.found;let i=vW(e,t);return n.set(t,{at:Date.now(),found:i}),i.catch(()=>n.delete(t)),i}async function vW(e,t){let n=await e.accounts(),r=await Promise.all(n.map(t=>Wt(t.google,{signIn:e.signIn}).then(e=>({account:t,summaries:e}),e=>e instanceof Error?e:Error(String(e))))),i=/* @__PURE__ */ new Set,a=[];for(let e of r)if(!(e instanceof Error))for(let t of e.summaries)for(let n of t.propertySummaries){let r=n.property.replace(/^properties\//,``);!i.has(r)&&/^\d+$/.test(r)&&(i.add(r),a.push({account:t.account,property:r,propertyName:n.displayName??r,google:e.account.google}))}if(!a.length){let e=r.find(e=>e instanceof Error);return e?`could not be read. ${e.message}`:`the connected Google accounts cannot read any property.`}let o=mW(t,a).slice(0,30),s=0;for(let n=0;n<o.length;n+=5){let r=o.slice(n,n+5),i=await Promise.all(r.map(t=>Gt(t.google,t.property,{signIn:e.signIn}).catch(()=>(s+=1,[])))),a=hW(t,r.flatMap((e,n)=>dW(t,(i[n]??[]).map(t=>({...e,stream:t}))))),c=/* @__PURE__ */ new Map;for(let e of a){let t=r.find(t=>t.property===e.property);t&&!c.has(e.property)&&c.set(e.property,{property:e.property,name:e.propertyName,stream:e.defaultUri,google:t.google})}if(c.size)return[...c.values()].slice(0,3)}return`no web stream matches ${t} in ${o.length<a.length?`the first ${o.length} of ${a.length} properties, named like the site first`:`${a.length} ${a.length===1?`property`:`properties`}`}${s?` (${s} could not be read)`:``}.`}const yW=[`chatgpt.com`,`chatgpt`,`chat.openai.com`,`perplexity.ai`,`perplexity`,`claude.ai`,`gemini.google.com`,`copilot.microsoft.com`,`copilot.com`,`chat.deepseek.com`,`meta.ai`,`grok.com`,`chat.mistral.ai`,`you.com`,`phind.com`,`poe.com`],bW=`(www\\.)?(${yW.map(e=>e.replaceAll(`.`,`\\.`)).join(`|`)})`,xW=(e,t,n=`EXACT`)=>({filter:{fieldName:e,stringFilter:{matchType:n,value:t}}});async function SW(e,t,n,r,i){let a=(i,a)=>Kt(e.google,e.property,{dateRanges:[{...n,name:`now`},{...r,name:`before`}],dimensions:i.map(e=>({name:e})),metrics:[{name:`sessions`},{name:`keyEvents`}],...a.length?{dimensionFilter:{andGroup:{expressions:a}}}:{},limit:100},{signIn:t.signIn}),o=i?[xW(`landingPage`,i,`CONTAINS`)]:[],s=[...o,xW(`sessionSource`,bW,`FULL_REGEXP`)],[c,l,u,d,f]=await Promise.all([a([`sessionDefaultChannelGroup`],o),a([],[...o,xW(`sessionDefaultChannelGroup`,`Organic Search`),xW(`sessionSource`,`google`)]),a([`sessionSource`],s),a([`landingPage`],s),a([`sessionSource`],[...o,xW(`sessionDefaultChannelGroup`,`Organic Search`)])]),p=()=>({google:{sessions:0,keyEvents:0},organic:{sessions:0,keyEvents:0},all:{sessions:0,keyEvents:0}}),m={now:p(),before:p()},h=(e,t)=>{let n=(e.dimensionHeaders?.map(e=>e.name)??[]).indexOf(`dateRange`);for(let r of e.rows??[]){let e=r.dimensionValues??[];t((n>=0?e[n]:e.at(-1))?.value===`before`?m.before:m.now,e[0]?.value??``,{sessions:Number(r.metricValues?.[0]?.value??0),keyEvents:Number(r.metricValues?.[1]?.value??0)})}},g=(e,t)=>{e.sessions+=t.sessions,e.keyEvents+=t.keyEvents};h(c,(e,t,n)=>{g(e.all,n),t===`Organic Search`&&g(e.organic,n)}),h(l,(e,t,n)=>g(e.google,n));let _=(e,t)=>{let n=/* @__PURE__ */ new Map,r=(e.dimensionHeaders?.map(e=>e.name)??[]).indexOf(`dateRange`);for(let t of e.rows??[]){let e=t.dimensionValues??[],i=e[0]?.value??``,a=(r>=0?e[r]:e.at(-1))?.value===`before`,o=n.get(i)??{now:0,before:0,keyEvents:0},s=Number(t.metricValues?.[0]?.value??0);a?o.before+=s:(o.now+=s,o.keyEvents+=Number(t.metricValues?.[1]?.value??0)),n.set(i,o)}return[...n].map(([e,n])=>({[t]:e,sessions:n.now,previousSessions:n.before,keyEvents:n.keyEvents})).sort((e,n)=>n.sessions-e.sessions||n.previousSessions-e.previousSessions||String(e[t]).localeCompare(String(n[t])))},v=_(u,`source`),y=_(f,`source`),b=v.reduce((e,t)=>e+t.sessions,0),x=v.reduce((e,t)=>e+t.previousSessions,0),S={metadata:{timeZone:c.metadata?.timeZone,subjectToThresholding:[c,l,u,d,f].some(e=>e.metadata?.subjectToThresholding),dataLossFromOtherRow:[c,l,u,d,f].some(e=>e.metadata?.dataLossFromOtherRow)}},C=[];return S.metadata?.timeZone&&S.metadata.timeZone!==`America/Los_Angeles`&&C.push(`Google Analytics days use the property's time zone, ${S.metadata.timeZone}; Search Console days use Pacific time.`),S.metadata?.subjectToThresholding&&C.push(`Google Analytics applied thresholding; small rows can be hidden.`),S.metadata?.dataLossFromOtherRow&&C.push(`Google Analytics grouped some rows into (other).`),{property:e.property,propertyName:e.name,webStream:e.stream,...i?{landingPageContains:i}:{},organicSearchFromGoogle:{sessions:m.now.google.sessions,previousSessions:m.before.google.sessions,sessionsChangePct:Nn(m.now.google.sessions,m.before.google.sessions)},organicSearch:{sessions:m.now.organic.sessions,previousSessions:m.before.organic.sessions,sessionsChangePct:Nn(m.now.organic.sessions,m.before.organic.sessions),keyEvents:m.now.organic.keyEvents,previousKeyEvents:m.before.organic.keyEvents},allChannels:{sessions:m.now.all.sessions,previousSessions:m.before.all.sessions,sessionsChangePct:Nn(m.now.all.sessions,m.before.all.sessions)},organicSearchByEngine:y.slice(0,10),aiAssistants:{sessions:b,previousSessions:x,sessionsChangePct:Nn(b,x),bySource:v,topLandingPages:_(d,`page`).slice(0,20),sources:yW},notes:C}}const CW=e=>e===null?`n/a`:`${e>0?`+`:``}${e}%`;function wW(e){let t=e.filter(e=>e.sessions||e.previousSessions);return t.length<2?``:`Organic Search sessions by search engine: ${t.slice(0,5).map(e=>`${e.source} ${e.sessions} (was ${e.previousSessions})`).join(`, `)}.`}function TW(e){let t=(e,t)=>e.slice(0,5).map(e=>`${e[t]} ${e.sessions} (was ${e.previousSessions})`).join(`, `);return!e.sessions&&!e.previousSessions?`Visits from AI assistants (ChatGPT, Perplexity, Claude, Gemini, Copilot and others): none in either period.`:[`Visits from AI assistants (ChatGPT, Perplexity, Claude, Gemini, Copilot and others): ${e.sessions} sessions (was ${e.previousSessions}, ${CW(e.sessionsChangePct)}). By source: ${t(e.bySource,`source`)}.`,e.topLandingPages.length?`Top landing pages from AI assistants: ${t(e.topLandingPages,`page`)}.`:``,`Only visits that pass a referrer or utm_source count; AI apps that send none show as Direct.`].filter(Boolean).join(` `)}async function EW(e,t,n){if(!e)return null;try{let r=await _W(e,t);if(typeof r==`string`)return`Google Analytics: ${r}`;let i=[...await Promise.all(r.map(async t=>{let r=await Kt(t.google,t.property,{dateRanges:[n],dimensions:[{name:`landingPage`}],metrics:[{name:`sessions`},{name:`keyEvents`}],dimensionFilter:xW(`sessionDefaultChannelGroup`,`Organic Search`),orderBys:[{metric:{metricName:`sessions`},desc:!0}],limit:1e4},{signIn:e.signIn}),i=/* @__PURE__ */ new Map,a=0;for(let e of r.rows??[]){let t=e.dimensionValues?.[0]?.value??``,n=Number(e.metricValues?.[0]?.value??0),r=Number(e.metricValues?.[1]?.value??0);a+=n;let o=i.get(t)??{sessions:0,keyEvents:0};o.sessions+=n,o.keyEvents+=r,i.set(t,o)}return{item:t,pages:i,total:a,capped:(r.rowCount??0)>(r.rows?.length??0)||!!r.metadata?.subjectToThresholding}}))].sort((e,t)=>t.total-e.total)[0];return i?{property:i.item.property,propertyName:i.item.name,host:fW(i.item.stream).replace(/^www\./,``),pages:i.pages,capped:i.capped}:`Google Analytics: no matching property.`}catch(e){return`Google Analytics: could not be read. ${e instanceof Error?e.message:String(e)}`}}function DW(e,t){if(typeof t==`string`)return`Not ranked by key events. ${t}`;let n=e.items;if(!Array.isArray(n)||!n.length)return``;let r=e=>{if(typeof e!=`string`)return;let n;try{n=new URL(e)}catch{return}if(n.hostname.replace(/^www\./,``)!==t.host)return;let r=n.pathname,i=r.endsWith(`/`)?r.slice(0,-1)||`/`:`${r}/`;return t.pages.get(r)??t.pages.get(i)??null},i=n.map((e,n)=>{let i=r(e.url??e.page),a=i===void 0||i===null&&t.capped?void 0:i??{sessions:0,keyEvents:0},o=a?.keyEvents??null;return{index:n,item:{...e,keyEvents:o,organicSessions:a?.sessions??null}}});i.sort((e,t)=>(t.item.keyEvents??0)-(e.item.keyEvents??0)||e.index-t.index),e.items=i.map(e=>e.item);let a=new Set(i.filter(e=>e.item.keyEvents).map(e=>e.item.url??e.item.page)).size;return[a?`Pages that converted come first: ${a} ${a===1?`page`:`pages`} here had Google Analytics key events from Organic Search in the same dates (property ${t.propertyName}, ${t.property}); the rest keep this section's order.`:`${t.capped?`None of these pages that Google Analytics listed`:`None of these pages`} had Google Analytics key events from Organic Search in the same dates (property ${t.propertyName}, ${t.property}), so the section keeps its own order.`,`keyEvents and organicSessions are Google Analytics counts for the whole landing page, not for one query, and sessions are not Search Console clicks.`,t.capped?`Google Analytics did not list every landing page (row limit or thresholding), so a blank keyEvents is unknown, not zero.`:``].filter(Boolean).join(` `)}async function OW(e,t,n,r,i){if(!e)return null;try{let a=await _W(e,t);if(typeof a==`string`)return{text:`Google Analytics: ${a}`,data:{error:a}};let o=await Promise.all(a.map(t=>SW(t,e,n,r,i).catch(e=>e instanceof Error?e:Error(String(e))))),s=[...o.filter(e=>!(e instanceof Error))].sort((e,t)=>t.allChannels.sessions-e.allChannels.sessions)[0];if(!s)throw o[0];let c=a.filter(e=>e.property!==s.property).map(e=>`${e.name} (${e.property})`),l=s.organicSearchFromGoogle,u=s.organicSearch,d=s.allChannels,f=u.keyEvents||u.previousKeyEvents?`, Organic Search key events ${u.keyEvents} (was ${u.previousKeyEvents})`:``;return{text:[`Google Analytics, property ${s.propertyName} (${s.property}), matched by its web stream ${s.webStream}${i?`, landing pages whose path contains "${i}"`:``}: Organic Search sessions from Google ${l.sessions} (was ${l.previousSessions}, ${CW(l.sessionsChangePct)}), from every search engine ${u.sessions} (was ${u.previousSessions}, ${CW(u.sessionsChangePct)})${f}, sessions from all channels ${d.sessions} (was ${d.previousSessions}, ${CW(d.sessionsChangePct)}).`,wW(s.organicSearchByEngine),`Sessions are Google Analytics’ own count, not Search Console clicks.`,...s.notes,...c.length?[`Other properties with a matching web stream, with fewer sessions: ${c.join(`, `)}.`]:[]].filter(Boolean).join(` `).concat(`
352
+ `),sources:ZU([...XU(t.references),...n.flatMap(e=>XU(e.references))]),model:null}},$U=[{name:`chatgpt`,means:`a ChatGPT search, read by DataForSEO`,path:`v3/ai_optimization/chat_gpt/llm_scraper/live/advanced`,languages:`v3/ai_optimization/chat_gpt/llm_scraper/languages`,task:(e,t)=>({keyword:e,...t}),shown:e=>{let t=typeof e.markdown==`string`?e.markdown:``,n=ZU(XU(e.sources));return!t&&!n.length?null:{markdown:t,sources:n,model:typeof e.model==`string`?e.model:null}}},{name:`google_ai_mode`,means:`Google AI Mode`,path:`v3/serp/google/ai_mode/live/advanced`,languages:`v3/serp/google/ai_mode/languages`,task:(e,t)=>({keyword:e,...t}),shown:QU},{name:`google_ai_overview`,means:`the AI Overview on a Google search`,path:`v3/serp/google/organic/live/advanced`,languages:`v3/serp/google/languages`,task:(e,t)=>({keyword:e,depth:10,load_async_ai_overview:!0,...t}),shown:QU}];function eW(e){return e.replace(/\[([^\]]*)\]\(https?:\/\/[^)]*\)/g,(e,t)=>/^\s*(\d+|[\w-]+(\.[\w-]+)+)\s*$/.test(t)?` `:t).replace(/https?:\/\/\S+/g,` `)}function tW(e,t){try{let n=new URL(e).hostname.toLowerCase();return n===t||n.endsWith(`.${t}`)}catch{return!1}}async function nW(e,t,n,r,i){if(typeof r==`string`)return{result:`unavailable`,model:null,cited_url:null,answered_at:null,cost_usd:0,error:r};try{let a=await LU(e,t.path,t.task(n,r)),o={answered_at:String(a.result?.datetime??``).slice(0,10)||null,cost_usd:a.cost},s=a.result&&t.shown(a.result);if(!s)return{result:`no_answer`,model:null,cited_url:null,...o};let c={model:s.model,...o},l=s.markdown.match(/https?:\/\/[^\s)\]]+/g)??[],u=[...s.sources,...l].find(e=>tW(e,i.domain));if(u)return{result:`cited`,cited_url:u,...c};let d=eW(s.markdown);return{result:d.toLowerCase().includes(i.domain)||hC(d,i.terms)?`mentioned`:`neither`,cited_url:null,...c}}catch(e){if(e instanceof I&&e.code===`NO_KEY`)throw e;return{result:`unavailable`,model:null,cited_url:null,answered_at:null,cost_usd:e.cost??0,error:e instanceof Error?e.message.slice(0,200):`failed`}}}async function rW(e,t){let n=t.toUpperCase(),r=NU(t)??PU(t,null).language_code,[i,...a]=await Promise.all([IU(e,`v3/ai_optimization/chat_gpt/llm_scraper/locations`),...$U.map(t=>IU(e,t.languages))]),o=i.find(e=>e.location_type===`Country`&&e.country_iso_code===n),s=o?{location_code:Number(o.location_code)}:{location_name:PU(t,null).location_name};return new Map($U.map((e,n)=>{if(e.name===`chatgpt`&&!o)return[e.name,`DataForSEO's ChatGPT search has no data for country "${t}".`];let i=(a[n]??[]).some(e=>e.language_code===r);return[e.name,i?{...s,language_code:r}:`DataForSEO's ${e.name} has no "${r}" language, the language used for country "${t}".`]}))}const iW=e=>Math.round(e*1e4)/1e4;async function aW(e,t,n,r){let i=FU(t),a=oC(e.site)?.domain;if(!a)throw new I(`INVALID_INPUT`,`ai_answers needs a site with a domain, not "${e.site}".`);let o=[...new Set((e.questions??[]).map(e=>e.trim()).filter(Boolean))];if(o.length>10)throw new I(`INVALID_INPUT`,`ai_answers takes up to 10 questions, not ${o.length}.`);let s=An({},90),c;c=o.length?o.map(e=>({query:e,impressions:null})):((await SU({site:e.site,...s},n.searchAnalytics)).items??[]).filter(e=>e.askable).slice(0,10);let l={domain:a,terms:fC({siteUrl:e.site})},u=c.length?await rW(i,e.country):/* @__PURE__ */ new Map,d=await Promise.all(c.flatMap(e=>$U.map(async t=>({question:e.query,impressions:e.impressions,engine:t.name,...await nW(i,t,e.query,u.get(t.name)??`no market`,l)})))),f=iW(d.reduce((e,t)=>e+t.cost_usd,0)),p=e=>d.filter(t=>t.result===e).length,m=[o.length?`The ${o.length} questions you gave, asked as written. Impressions are blank because they don't come from Search Console.`:c.length?`The top ${c.length} askable questions in Search Console’s AI-shaped queries for ${s.startDate} to ${s.endDate}, by impressions. No calls were made for other questions.`:`No askable questions in Search Console’s AI-shaped queries for ${s.startDate} to ${s.endDate}, so no answers were checked and nothing was spent.`,...c.length?[`Cited ${p(`cited`)}, mentioned ${p(`mentioned`)}, neither ${p(`neither`)}, no answer shown ${p(`no_answer`)}, unavailable ${p(`unavailable`)}. DataForSEO cost: $${f}.`,`Each row is one sample of an answer that changes from run to run, not a ranking.`,`cited: the answer lists or links a page on ${a} or a subdomain. mentioned: no such source, but the answer text names ${[a,...l.terms].join(` or `)}, a text match. no_answer: the engine showed no AI answer for the question. model: the model DataForSEO reports for the answer, when it reports one.`,`Engines: ${$U.map(e=>`${e.name} is ${e.means}`).join(`; `)}.`]:[]];return{text:KU({title:`AI answers for ${e.site}, ${e.country}.`,rows:d,offset:e.offset,limit:50,detail:e.detail,notes:m,more:r}),data:{provider:`dataforseo`,action:`ai_answers`,site:e.site,country:e.country,questionsFrom:o.length?{section:`given`}:{section:`ai-queries`,...s},questions:c.map(e=>e.query),costUsd:f,total:d.length,offset:e.offset,rows:d.slice(e.offset,e.offset+50)}}}async function oW(e,t,n){let r=e.length===1?e[0]:void 0;if(r)return r.google;let i=(await Promise.all(e.map(async e=>(await Wt(e.google,{signIn:n}).catch(()=>[])).some(e=>e.propertySummaries.some(e=>e.property===`properties/${t}`))?e:null))).find(Boolean);if(i)return i.google;throw new I(`NO_ACCESS`,`No connected Google account can read Google Analytics property ${t}. Call seo_sites to list the properties.`)}async function sW(e,t,n){let r=An(e,28),i=e.offset??0,a=Math.min(1e3,i+50),o=await Kt(t,e.property,{dateRanges:[r],dimensions:[{name:`landingPage`}],metrics:[{name:`sessions`},{name:`totalUsers`},{name:`engagedSessions`},{name:`keyEvents`}],dimensionFilter:{filter:{fieldName:`sessionDefaultChannelGroup`,stringFilter:{value:`Organic Search`}}},orderBys:[{metric:{metricName:`sessions`},desc:!0}],limit:a},n),s=[`Sessions in the Organic Search default channel group.`];o.metadata?.subjectToThresholding&&s.push(`Google Analytics applied thresholding; small rows can be hidden.`),o.metadata?.dataLossFromOtherRow&&s.push(`Some rows were grouped into (other) by Google Analytics.`),(o.rowCount??0)>a&&s.push(`${o.rowCount} landing pages in total; ${a} read.`);let c=(o.rows??[]).map(e=>({page:e.dimensionValues[0]?.value??``,sessions:Number(e.metricValues[0]?.value??0),users:Number(e.metricValues[1]?.value??0),engaged_sessions:Number(e.metricValues[2]?.value??0),key_events:Number(e.metricValues[3]?.value??0)}));return{text:KU({title:`Google Analytics property ${e.property}: search landing pages, ${r.startDate} to ${r.endDate}.`,rows:c,offset:i,limit:50,detail:e.detail??`summary`,notes:s,more:n.call}),data:{range:r,total:c.length,offset:i,rows:c.slice(i,i+50)}}}function cW(e){try{let t=new URL(e);return t.protocol!==`http:`&&t.protocol!==`https:`?void 0:t.hostname.toLowerCase().replace(/\.$/,``)||void 0}catch{return}}function lW(e){if(!e.startsWith(`sc-domain:`))return;let t=e.slice(10).trim().toLowerCase();if(t&&!/[/:]/.test(t))return t.replace(/\.$/,``)||void 0}function uW(e,t){let n=lW(e);return n?t===n||t.endsWith(`.${n}`)?`domain`:void 0:cW(e)===t?`hostname`:void 0}function dW(e,t){return t.flatMap(t=>{let n=t.stream.webStreamData?.defaultUri;if(!n)return[];let r=cW(n);if(!r)return[];let i=uW(e,r);return i?[{...t,defaultUri:n,hostname:r,match:i}]:[]})}const fW=e=>{let t=e.replace(/^sc-domain:/,``);try{return new URL(t.includes(`://`)?t:`https://${t}`).hostname}catch{return t}},pW=e=>e.toLowerCase().replace(/[^a-z0-9]/g,``);function mW(e,t){let n=fW(e).replace(/^www\./,``),r=[pW(n),pW(n.split(`.`)[0]??``)].filter(e=>e.length>=3),i=e=>+!r.some(t=>pW(e.propertyName).includes(t));return[...t].sort((e,t)=>i(e)-i(t)||e.property.localeCompare(t.property,`en`,{numeric:!0}))}function hW(e,t){let n=fW(e).replace(/^www\./,``),r=e=>e.hostname===n||e.hostname===`www.${n}`?0:1;return[...t].sort((e,t)=>r(e)-r(t)||e.hostname.length-t.hostname.length||e.property.localeCompare(t.property,`en`,{numeric:!0}))}const gW=/* @__PURE__ */ new WeakMap;function _W(e,t){let n=gW.get(e)??/* @__PURE__ */ new Map;gW.set(e,n);let r=n.get(t);if(r&&Date.now()-r.at<3e5)return r.found;let i=vW(e,t);return n.set(t,{at:Date.now(),found:i}),i.catch(()=>n.delete(t)),i}async function vW(e,t){let n=await e.accounts(),r=await Promise.all(n.map(t=>Wt(t.google,{signIn:e.signIn}).then(e=>({account:t,summaries:e}),e=>e instanceof Error?e:Error(String(e))))),i=/* @__PURE__ */ new Set,a=[];for(let e of r)if(!(e instanceof Error))for(let t of e.summaries)for(let n of t.propertySummaries){let r=n.property.replace(/^properties\//,``);!i.has(r)&&/^\d+$/.test(r)&&(i.add(r),a.push({account:t.account,property:r,propertyName:n.displayName??r,google:e.account.google}))}if(!a.length){let e=r.find(e=>e instanceof Error);return e?`could not be read. ${e.message}`:`the connected Google accounts cannot read any property.`}let o=mW(t,a).slice(0,30),s=0;for(let n=0;n<o.length;n+=5){let r=o.slice(n,n+5),i=await Promise.all(r.map(t=>Gt(t.google,t.property,{signIn:e.signIn}).catch(()=>(s+=1,[])))),a=hW(t,r.flatMap((e,n)=>dW(t,(i[n]??[]).map(t=>({...e,stream:t}))))),c=/* @__PURE__ */ new Map;for(let e of a){let t=r.find(t=>t.property===e.property);t&&!c.has(e.property)&&c.set(e.property,{property:e.property,name:e.propertyName,stream:e.defaultUri,google:t.google})}if(c.size)return[...c.values()].slice(0,3)}return`no web stream matches ${t} in ${o.length<a.length?`the first ${o.length} of ${a.length} properties, named like the site first`:`${a.length} ${a.length===1?`property`:`properties`}`}${s?` (${s} could not be read)`:``}.`}const yW=[`chatgpt.com`,`chatgpt`,`chat.openai.com`,`perplexity.ai`,`perplexity`,`claude.ai`,`gemini.google.com`,`copilot.microsoft.com`,`copilot.com`,`chat.deepseek.com`,`meta.ai`,`grok.com`,`chat.mistral.ai`,`you.com`,`phind.com`,`poe.com`],bW=`(www\\.)?(${yW.map(e=>e.replaceAll(`.`,`\\.`)).join(`|`)})`,xW=(e,t,n=`EXACT`)=>({filter:{fieldName:e,stringFilter:{matchType:n,value:t}}});async function SW(e,t,n,r,i){let a=(i,a)=>Kt(e.google,e.property,{dateRanges:[{...n,name:`now`},{...r,name:`before`}],dimensions:i.map(e=>({name:e})),metrics:[{name:`sessions`},{name:`keyEvents`}],...a.length?{dimensionFilter:{andGroup:{expressions:a}}}:{},limit:100},{signIn:t.signIn}),o=i?[xW(`landingPage`,i,`CONTAINS`)]:[],s=[...o,xW(`sessionSource`,bW,`FULL_REGEXP`)],[c,l,u,d,f]=await Promise.all([a([`sessionDefaultChannelGroup`],o),a([],[...o,xW(`sessionDefaultChannelGroup`,`Organic Search`),xW(`sessionSource`,`google`)]),a([`sessionSource`],s),a([`landingPage`],s),a([`sessionSource`],[...o,xW(`sessionDefaultChannelGroup`,`Organic Search`)])]),p=()=>({google:{sessions:0,keyEvents:0},organic:{sessions:0,keyEvents:0},all:{sessions:0,keyEvents:0}}),m={now:p(),before:p()},h=(e,t)=>{let n=(e.dimensionHeaders?.map(e=>e.name)??[]).indexOf(`dateRange`);for(let r of e.rows??[]){let e=r.dimensionValues??[];t((n>=0?e[n]:e.at(-1))?.value===`before`?m.before:m.now,e[0]?.value??``,{sessions:Number(r.metricValues?.[0]?.value??0),keyEvents:Number(r.metricValues?.[1]?.value??0)})}},g=(e,t)=>{e.sessions+=t.sessions,e.keyEvents+=t.keyEvents};h(c,(e,t,n)=>{g(e.all,n),t===`Organic Search`&&g(e.organic,n)}),h(l,(e,t,n)=>g(e.google,n));let _=(e,t)=>{let n=/* @__PURE__ */ new Map,r=(e.dimensionHeaders?.map(e=>e.name)??[]).indexOf(`dateRange`);for(let t of e.rows??[]){let e=t.dimensionValues??[],i=e[0]?.value??``,a=(r>=0?e[r]:e.at(-1))?.value===`before`,o=n.get(i)??{now:0,before:0,keyEvents:0},s=Number(t.metricValues?.[0]?.value??0);a?o.before+=s:(o.now+=s,o.keyEvents+=Number(t.metricValues?.[1]?.value??0)),n.set(i,o)}return[...n].map(([e,n])=>({[t]:e,sessions:n.now,previousSessions:n.before,keyEvents:n.keyEvents})).sort((e,n)=>n.sessions-e.sessions||n.previousSessions-e.previousSessions||String(e[t]).localeCompare(String(n[t])))},v=_(u,`source`),y=_(f,`source`),b=v.reduce((e,t)=>e+t.sessions,0),x=v.reduce((e,t)=>e+t.previousSessions,0),S={metadata:{timeZone:c.metadata?.timeZone,subjectToThresholding:[c,l,u,d,f].some(e=>e.metadata?.subjectToThresholding),dataLossFromOtherRow:[c,l,u,d,f].some(e=>e.metadata?.dataLossFromOtherRow)}},C=[];return S.metadata?.timeZone&&S.metadata.timeZone!==`America/Los_Angeles`&&C.push(`Google Analytics days use the property's time zone, ${S.metadata.timeZone}; Search Console days use Pacific time.`),S.metadata?.subjectToThresholding&&C.push(`Google Analytics applied thresholding; small rows can be hidden.`),S.metadata?.dataLossFromOtherRow&&C.push(`Google Analytics grouped some rows into (other).`),{property:e.property,propertyName:e.name,webStream:e.stream,...i?{landingPageContains:i}:{},organicSearchFromGoogle:{sessions:m.now.google.sessions,previousSessions:m.before.google.sessions,sessionsChangePct:Nn(m.now.google.sessions,m.before.google.sessions)},organicSearch:{sessions:m.now.organic.sessions,previousSessions:m.before.organic.sessions,sessionsChangePct:Nn(m.now.organic.sessions,m.before.organic.sessions),keyEvents:m.now.organic.keyEvents,previousKeyEvents:m.before.organic.keyEvents},allChannels:{sessions:m.now.all.sessions,previousSessions:m.before.all.sessions,sessionsChangePct:Nn(m.now.all.sessions,m.before.all.sessions)},organicSearchByEngine:y.slice(0,10),aiAssistants:{sessions:b,previousSessions:x,sessionsChangePct:Nn(b,x),bySource:v,topLandingPages:_(d,`page`).slice(0,20),sources:yW},notes:C}}const CW=e=>e===null?`n/a`:`${e>0?`+`:``}${e}%`;function wW(e){let t=e.filter(e=>e.sessions||e.previousSessions);return t.length<2?``:`Organic Search sessions by search engine: ${t.slice(0,5).map(e=>`${e.source} ${e.sessions} (was ${e.previousSessions})`).join(`, `)}.`}function TW(e){let t=(e,t)=>e.slice(0,5).map(e=>`${e[t]} ${e.sessions} (was ${e.previousSessions})`).join(`, `);return!e.sessions&&!e.previousSessions?`Visits from AI assistants (ChatGPT, Perplexity, Claude, Gemini, Copilot and others): none in either period.`:[`Visits from AI assistants (ChatGPT, Perplexity, Claude, Gemini, Copilot and others): ${e.sessions} sessions (was ${e.previousSessions}, ${CW(e.sessionsChangePct)}). By source: ${t(e.bySource,`source`)}.`,e.topLandingPages.length?`Top landing pages from AI assistants: ${t(e.topLandingPages,`page`)}.`:``,`Only visits that pass a referrer or utm_source count; AI apps that send none show as Direct.`].filter(Boolean).join(` `)}async function EW(e,t,n){if(!e)return null;try{let r=await _W(e,t);if(typeof r==`string`)return`Google Analytics: ${r}`;let i=[...await Promise.all(r.map(async t=>{let r=await Kt(t.google,t.property,{dateRanges:[n],dimensions:[{name:`landingPage`}],metrics:[{name:`sessions`},{name:`keyEvents`}],dimensionFilter:xW(`sessionDefaultChannelGroup`,`Organic Search`),orderBys:[{metric:{metricName:`sessions`},desc:!0}],limit:1e4},{signIn:e.signIn}),i=/* @__PURE__ */ new Map,a=0;for(let e of r.rows??[]){let t=e.dimensionValues?.[0]?.value??``,n=Number(e.metricValues?.[0]?.value??0),r=Number(e.metricValues?.[1]?.value??0);a+=n;let o=i.get(t)??{sessions:0,keyEvents:0};o.sessions+=n,o.keyEvents+=r,i.set(t,o)}return{item:t,pages:i,total:a,capped:(r.rowCount??0)>(r.rows?.length??0)||!!r.metadata?.subjectToThresholding}}))].sort((e,t)=>t.total-e.total)[0];return i?{property:i.item.property,propertyName:i.item.name,host:fW(i.item.stream).replace(/^www\./,``),pages:i.pages,capped:i.capped}:`Google Analytics: no matching property.`}catch(e){return`Google Analytics: could not be read. ${e instanceof Error?e.message:String(e)}`}}function DW(e,t){if(typeof t==`string`)return`Not ranked by key events. ${t}`;let n=e.items;if(!Array.isArray(n)||!n.length)return``;let r=e=>{if(typeof e!=`string`)return;let n;try{n=new URL(e)}catch{return}if(n.hostname.replace(/^www\./,``)!==t.host)return;let r=n.pathname,i=r.endsWith(`/`)?r.slice(0,-1)||`/`:`${r}/`;return t.pages.get(r)??t.pages.get(i)??null},i=n.map((e,n)=>{let i=r(e.url??e.page),a=i===void 0||i===null&&t.capped?void 0:i??{sessions:0,keyEvents:0},o=a?.keyEvents??null;return{index:n,item:{...e,keyEvents:o,organicSessions:a?.sessions??null}}});i.sort((e,t)=>(t.item.keyEvents??0)-(e.item.keyEvents??0)||e.index-t.index),e.items=i.map(e=>e.item);let a=new Set(i.filter(e=>e.item.keyEvents).map(e=>e.item.url??e.item.page)).size;return[a?`Pages that converted come first: ${a} ${a===1?`page`:`pages`} here had Google Analytics key events from Organic Search in the same dates (property ${t.propertyName}, ${t.property}); the rest keep this section's order.`:`${t.capped?`None of these pages that Google Analytics listed`:`None of these pages`} had Google Analytics key events from Organic Search in the same dates (property ${t.propertyName}, ${t.property}), so the section keeps its own order.`,`keyEvents and organicSessions are Google Analytics counts for the whole landing page, not for one query, and sessions are not Search Console clicks.`,t.capped?`Google Analytics did not list every landing page (row limit or thresholding), so a blank keyEvents is unknown, not zero.`:``].filter(Boolean).join(` `)}async function OW(e,t,n,r,i){if(!e)return null;try{let a=await _W(e,t);if(typeof a==`string`)return{text:`Google Analytics: ${a}`,data:{error:a}};let o=await Promise.all(a.map(t=>SW(t,e,n,r,i).catch(e=>e instanceof Error?e:Error(String(e))))),s=[...o.filter(e=>!(e instanceof Error))].sort((e,t)=>t.allChannels.sessions-e.allChannels.sessions)[0];if(!s)throw o[0];let c=a.filter(e=>e.property!==s.property).map(e=>`${e.name} (${e.property})`),l=s.organicSearchFromGoogle,u=s.organicSearch,d=s.allChannels,f=u.keyEvents||u.previousKeyEvents?`, Organic Search key events ${u.keyEvents} (was ${u.previousKeyEvents})`:``;return{text:[`Google Analytics, property ${s.propertyName} (${s.property}), matched by its web stream ${s.webStream}${i?`, landing pages whose path contains "${i}"`:``}: Organic Search sessions from Google ${l.sessions} (was ${l.previousSessions}, ${CW(l.sessionsChangePct)}), from every search engine ${u.sessions} (was ${u.previousSessions}, ${CW(u.sessionsChangePct)})${f}, sessions from all channels ${d.sessions} (was ${d.previousSessions}, ${CW(d.sessionsChangePct)}).`,wW(s.organicSearchByEngine),`Sessions are Google Analytics’ own count, not Search Console clicks.`,...s.notes,...c.length?[`Other properties with a matching web stream, with fewer sessions: ${c.join(`, `)}.`]:[]].filter(Boolean).join(` `).concat(`
353
353
 
354
354
  `,TW(s.aiAssistants)),data:{...s,...c.length?{otherMatches:c}:{}}}}catch(e){let t=e instanceof Error?e.message:String(e);return{text:`Google Analytics: could not be read. ${t}`,data:{error:t}}}}const kW=`weekly-audit`,AW={link_to_error:`Internal links to a page that returns an error`,link_to_redirect:`Internal links to a redirect`,link_failed:`Internal links to a page that could not be fetched`,title_missing:`Pages with no title`,title_duplicate:`Titles used by more than one page`,description_missing:`Pages with no meta description`,h1_missing:`Pages with no H1`,noindex:`Pages that ask not to be indexed`,canonical_elsewhere:`Pages whose canonical is another URL`,sitemap_url_not_ok:`Sitemap URLs that do not return 200`,sitemap_page_unlinked:`Sitemap pages no crawled page links to (heuristic: the crawl may not reach every page)`,thin_page:`Pages under 100 words (heuristic)`,js_changed_tags:`Pages whose raw HTML has another title, canonical or robots tag than the page after JavaScript runs`},jW=e=>new Date(e).toISOString().slice(0,10);function MW(e,t){return t&&e.startsWith(t)?e.slice(t.length)||`/`:e}function NW(e,t){let n=JSON.parse(e.details),r=[];if(typeof n.status==`number`&&r.push(`${n.status}`),typeof n.location==`string`&&r.push(`to ${MW(n.location,t)}`),typeof n.canonical==`string`&&r.push(`canonical ${MW(n.canonical,t)}`),typeof n.words==`number`&&r.push(`${n.words} words`),n.raw&&typeof n.raw==`object`){let e=n.raw;for(let n of[`title`,`canonical`,`robots`])if(n in e){let i=e[n];r.push(`raw ${n} ${typeof i==`string`?n===`canonical`?MW(i,t):i:`none`}`)}}let i=typeof n.linking_pages==`number`,a=i?n.linking_pages:n.pages;if(typeof a==`number`){let e=(Array.isArray(n.examples)?n.examples.map(e=>MW(e,t)):[]).slice(0,3),o=`${i?`linking page`:`page`}${a===1?``:`s`}`;r.push(`${a} ${o}${e.length?` (${e.join(`, `)}${a>e.length?`, and more`:``})`:``}`)}return r.join(`, `)}function PW(e,t){return e.state===`resolved`?`resolved`:e.reopened_at&&e.reopened_at>=t?`back`:e.first_seen_at>=t?`new`:`open since ${jW(e.first_seen_at)}`}function FW(e,t,n){return[`state | finding | subject | evidence`,...e.map(e=>[PW(e,n),e.rule,MW(e.subject,t),NW(e,t)].join(` | `))].join(`
355
355
  `)}function IW(e,t){let n=e=>{let n=PW(e,t);return n===`new`||n===`back`?0:n===`resolved`?1:2};return[...e].sort((e,t)=>n(e)-n(t)||e.rule.localeCompare(t.rule)||(e.subject<t.subject?-1:+(e.subject>t.subject)))}function LW(e){if(!e)return null;let t=e=>e===1?`1 page`:`${e} pages`;return e.mode===`on`?`JavaScript changed what some pages show, so the crawl rendered the parts of the site that need it (${t(e.rendered)} rendered${e.failed?`; ${t(e.failed)} could not be rendered, so ${e.failed===1?`it is`:`they are`} left out of the tag and text checks`:``}).`:e.mode===`off`?`JavaScript did not change the pages checked, so the crawl read raw HTML.`:e.mode===`unavailable`?`JavaScript rendering failed on the pages checked, so the crawl read raw HTML. If the site builds its pages with JavaScript, titles, canonicals and links may be missing here.`:e.failed&&!e.rendered?`JavaScript rendering failed, so the crawl read raw HTML.`:null}function RW(e,t,n){let r=e.site,i=e.detail??`summary`;if(!t)return{text:`${r} is not in the weekly site audit. Add it under Beta on the account page; the first crawl starts at once.`,data:{site:r,audited:!1}};let{summary:a,origin:o}=t,s=t.run?.status===`running`?`A crawl is running now: ${t.run.fetched} pages so far.`:null;if(!a||t.finished_at===null)return{text:[`The first weekly crawl of ${r} has not finished.`,s,t.run?.error?`Last error: ${t.run.error}`:null,`Run ${n(`report`,{site:r,section:kW})} again later.`].filter(Boolean).join(` `),data:{site:r,audited:!0,finished:!1}};let c=t.history[0]?.started_at??t.finished_at,l=t.history[1],u=IW(t.findings,c);if(i===`full`)return{text:JSON.stringify({site:r,...t,findings:u}),data:{site:r,...t,findings:u}};let d=a.pages,f=a.complete?`${d.fetched} pages, the whole site the crawl could reach`:a.status===`failed`?`${d.fetched} pages before the crawl stopped on errors, so it is partial`:`${d.fetched} pages; it stopped at the 5,000-page limit${d.links_past_cap?` with ${d.links_past_cap} more links past it`:``}, so it is partial`,p=[`Weekly site audit of ${r}, crawled ${jW(t.finished_at)}: ${f}.${d.blocked?` ${d.blocked===1?`1 URL`:`${d.blocked} URLs`} blocked by robots.txt ${d.blocked===1?`was`:`were`} not fetched.`:``}`,l?`Since the crawl of ${jW(l.finished_at)}: ${a.findings.new} new, ${a.findings.reopened} back, ${a.findings.resolved} resolved. ${a.findings.open} open now.`:`This is the first crawl, so every finding is new: ${a.findings.open} open.`,LW(a.rendering),a.findings.resolution===`crawled-pages-only`?a.complete?`Because some pages could not be rendered, a finding counts as resolved only when this crawl rendered its page; a finding not seen here is not an all-clear.`:`Because the crawl is partial, a finding counts as resolved only when this crawl fetched its page again; a finding not seen here is not an all-clear.`:null,a.findings.capped_rules.length?`Capped at 1,000 findings each, so these counts are lower bounds: ${a.findings.capped_rules.join(`, `)}.`:null,s].filter(Boolean).join(`
@@ -406,7 +406,7 @@ a!==-1&&(Pf(i,a,0,t,r),n.push(...t))}),this._make(n)}function Jf(e){return nu(e?
406
406
 
407
407
  `),data:{range:n,comparisonRange:r,rule:zq,lostPages:l.length,index:_.data,pages:v,indexing:T}}}const Hq={overview:`search-performance-overview`,"quick-wins":`quick-wins`,"low-ctr":`ctr-underperformers`,"striking-distance":`striking-distance`,"second-page":`second-page`,"content-decay":`decaying-pages`,cannibalization:`cannibalisation`,"traffic-drop":`traffic-anomaly`},Uq={keyword_metrics:`keyword-metrics`,ranked_keywords:`ranked-keywords`,serp:`serp-results`,competitors:`serp-competitors`,backlinks:`link-evidence`};function Wq(e,t={}){return e===`report`?Hq[t.section||`overview`]??null:e===`research`?Uq[t.action??``]??null:{page:`page-opportunities`,compare:`measure-change`,index:`link-recovery`,crawl:`site-crawl`,"internal-links":`internal-links`}[e]??null}const Gq=864e5;function Kq(e,t){let n=e=>{let t=/* @__PURE__ */ new Date(`${e}T00:00:00Z`);return{day:t.getUTCDate(),month:t.toLocaleString(`en-GB`,{month:`short`,timeZone:`UTC`})}},r=n(e),i=n(t);return r.month===i.month?`${r.day} to ${i.day} ${i.month}`:`${r.day} ${r.month} to ${i.day} ${i.month}`}const qq=e=>(/* @__PURE__ */ new Date(`${e}T00:00:00Z`)).toLocaleString(`en-GB`,{day:`numeric`,month:`short`,timeZone:`UTC`}),Jq=e=>`Your SEO Skill week, ${Kq(e.start,e.end)}`,Yq=e=>e.replace(/^sc-domain:|^https?:\/\//,``).replace(/\/$/,``),Xq=e=>e.toLocaleString(`en-US`),Zq=(e,t,n=`${t}s`)=>`${Xq(e)} ${e===1?t:n}`,Qq=e=>e.length<=1?e[0]??``:`${e.slice(0,-1).join(`, `)} and ${e.at(-1)}`,$q=e=>/[.!?]$/.test(e)?e:`${e}.`;function eJ(e){try{let t=new URL(e);return`${t.pathname}${t.search}`}catch{return e}}function tJ(e){let t=e.clicks;if(!t)return null;let n=t.clicks-t.previous;return Math.abs(n)<50?null:n<0&&t.previous>=100&&-n>=.3*t.previous?`fell`:n>0&&t.previous>=50&&n>=.3*t.previous?`rose`:null}const nJ=e=>{let t=e.clicks;return Math.round(Math.abs(t.clicks-t.previous)/t.previous*100)};function rJ(e,t,n){let r=[];e.index?.dropped&&r.push({rank:0,heading:`${Zq(e.index.dropped,`page`)} dropped`});let i=e.crawl,a=i!==null&&i.finished_at>=t&&i.finished_at<n;return a&&i.status===`failed`?r.push({rank:1,heading:`crawl stopped early`}):!a&&e.added_at<t&&r.push({rank:1,heading:`no crawl this week`}),tJ(e)===`fell`&&r.push({rank:2,heading:`clicks down ${nJ(e)}%`}),a&&i.new_issues>0&&r.push({rank:3,heading:Zq(i.new_issues,`new issue`)}),r}function iJ(e,t,n){let r=[],i=e.index;if(!i)r.push(`Google index: no checks ran this week yet.`);else if(i.dropped){let e=i.dropped_pages.map(e=>eJ(e.url)),t=i.dropped-e.length,n=t>0?`${e.join(`, `)} and ${Zq(t,`more`)}`:Qq(e),a=new Set(i.dropped_pages.map(e=>e.coverage)),o=a.size===1?[...a][0]:null;r.push(`Google index: ${Zq(i.dropped,`page`)} dropped out (${n}${o?`: ${o}`:``}).${i.returned?` ${Zq(i.returned,`page`)} now indexed.`:``} ${Zq(i.checked,`page`)} checked.`)}else r.push(`Google index: ${Zq(i.checked,`page`)} checked, none dropped${i.returned?`, ${Zq(i.returned,`page`)} now indexed`:``}.`);let a=e.crawl,o=a!==null&&a.finished_at>=t&&a.finished_at<n;if(o&&a.status===`failed`)r.push(`Weekly crawl: stopped after ${Zq(a.pages,`page`)}${a.error?`: ${$q(a.error)}`:`.`} It tries again next week.`);else if(o){let e=a.top_rule?AW[a.top_rule]:void 0;r.push(a.new_issues?`Weekly crawl: ${Zq(a.pages,`page`)}, ${Zq(a.new_issues,`new issue`)}${e?`, most of them ${e.charAt(0).toLowerCase()}${e.slice(1)}`:``}.`:`Weekly crawl: ${Zq(a.pages,`page`)}, no new issues.`)}else e.added_at<t?r.push(`Weekly crawl: none finished this week${e.start_error?`: ${$q(e.start_error)}`:`.`} It tries again each hour.`):r.push(`Weekly crawl: the first crawl is still running.`);let s=e.clicks;if(s){let e=s.previous>0?Math.round((s.clicks-s.previous)/s.previous*100):null,t=e===null?`from ${Xq(s.previous)}`:e===0?`level with`:`${e>0?`up`:`down`} ${Math.abs(e)}% on`;r.push(`Search clicks: ${Xq(s.clicks)} (${Kq(s.start,s.end)}), ${t}${e===null?``:` ${Xq(s.previous)}`} the 7 days before.`)}else r.push(`Search clicks: not available this week.`);return r}function aJ(e){let t=e.opportunities;if(!t)return[];let n=t.competitors.length?` (${Zq(t.competitors.length,`competitor`)} checked)`:``;if(t.blocked&&!t.items.length)return[`- Competitor opportunities: ${$q(t.blocked)}`];let r=t.partial?` Not every candidate keyword was judged yet, so more may follow next week.`:``;if(!t.items.length)return[`- Competitor opportunities: none cleared every check this week${n}.${r}`];let i=e=>`$${e.toFixed(2)}`;return[`- Competitor opportunities: ${Zq(t.items.length,`keyword`)} cleared every check${n}.${r}`,...t.items.map(e=>{let t=Qq(e.competitors.map(e=>`${e.domain} (#${e.position})`)),n=e.kind===`tool`;return` - "${e.keyword}"${n?`, a free tool`:``}: ${Xq(e.search_volume)} searches a month, ${i(e.cpc)} CPC, ${e.intent} intent. Top 10 for ${t}.${n?` The best tool page ranks for ${Zq(e.cluster,`paying keyword`)}.`:``} ${e.low_dr} of the top ${e.top} have Domain Rating at or below ${e.site_dr+10} (yours is ${e.site_dr}).`}),` - Searches, CPC and intent are DataForSEO estimates, Domain Rating is by Ahrefs, fit with the site is a model classification and "free tool" is a guess from the words.`]}function oJ(e){let t=e.measured;if(!t?.total)return[];let n=t.total-t.items.length;return[`- After fixes: ${Zq(t.total,`result`)} from Search Console, the days from the day an issue was marked fixed against the same days before (timing, not proof the fix caused it).`,...t.items.map(e=>{let t=e.subject.replace(/https?:\/\/[^\s/]+/g,``)||`/`,n=e.after.clicks-e.before.clicks;return` - ${t}, ${e.days} days from ${qq(e.fixed_on)}: ${Zq(e.after.clicks,`click`)} (${n>0?`up`:n<0?`down`:`level`}${n?` ${Xq(Math.abs(n))}`:``} on ${Xq(e.before.clicks)}), ${Zq(e.after.impressions,`impression`)} (from ${Xq(e.before.impressions)}).${e.updates?.length?` Google updates in these dates: ${Qq(e.updates)}.`:``}`}),...n>0?[` - And ${Zq(n,`more`)} in the issues list.`]:[]]}function sJ(e){let t=Date.parse(`${e.start}T00:00:00Z`),n=Date.parse(`${e.end}T00:00:00Z`)+Gq,r=`# ${Jq(e)}`;if(!e.sites.length)return`${r}\n\nNo site is in your weekly audit yet. Add one on the Sites tab: ${e.account}#sites`;let i=(e,t)=>Yq(e.property).localeCompare(Yq(t.property))||e.property.localeCompare(t.property),a=e.sites.map(e=>({site:e,found:rJ(e,t,n),waiting:!e.crawl||!e.index})),o=a.filter(e=>e.found.length),s=a.filter(e=>!e.found.length&&!e.waiting);a.sort((e,t)=>(e.found[0]?.rank??1/0)-(t.found[0]?.rank??1/0)||i(e.site,t.site));let c=a.length,l=[r,``,o.length?c===1?`Your site had changes worth a look this week.`:`${o.length} of your ${c} sites had changes worth a look this week.`:s.length?c===1?`Your site had a quiet week.`:s.length===c?`All ${c} of your sites had a quiet week.`:`${s.length} of your ${c} sites had a quiet week.`:`Your sites are still on their first checks.`];for(let{site:e,found:r}of a)l.push(``,`## ${Yq(e.property)}${r.length?`: ${Qq(r.map(e=>e.heading))}`:``}`,``,...iJ(e,t,n).map(e=>`- ${e}`),...aJ(e),...oJ(e),...e.link?[`- [View site](${e.link})`]:[]);return l.join(`
408
408
  `)}async function cJ(e,t){let n=e.week?await e.week():null;if(!n&&t){let n=await t();return{text:`No site given. Pick one below and send it: ${e.call(`report`,{site:`<site>`})}. The summary of every site is for accounts in the weekly site audit beta.\n\n${n.text}`,data:n.data}}if(!n)throw new I(`INVALID_INPUT`,`No site given. Name one: ${e.call(`report`,{site:`<site>`})}. The summary of every site is for accounts in the weekly site audit beta.`);let r=Date.parse(`${n.start}T00:00:00Z`),i=Date.parse(`${n.end}T00:00:00Z`)+Gq,a=e=>rJ(e,r,i)[0]?.rank??1/0,o=[...n.sites].sort((e,t)=>a(e)-a(t)||Yq(e.property).localeCompare(Yq(t.property)))[0],s=o?Yq(o.property):`<site>`;return{text:`${sJ(n)}\n\nFull report on one site: ${e.call(`report`,{site:s})}`,data:n}}const lJ=o.string().min(1).describe(`Search Console property or site, such as sc-domain:example.com, https://example.com/ or example.com.`),uJ={start_date:o.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(`YYYY-MM-DD. Optional; without end_date it runs to the latest final day.`),end_date:o.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(`YYYY-MM-DD. Optional.`)},dJ={detail:o.enum([`summary`,`rows`,`full`]).default(`summary`).describe(`summary first. rows: the next page of rows. full: every field as JSON.`),offset:o.number().int().min(0).default(0).describe(`rows only: the first row to return.`)},fJ={mark:o.enum(eK).optional().describe(`Optional, with site and issues: fixed, ignored, or open to undo. Only when the person says so.`),issues:o.string().min(1).optional().describe(`With mark: issue ids from the issues section, comma-separated.`),note:o.string().max(300).optional().describe(`Optional, with mark: why, in the person’s words.`),saved:o.string().max(6e5).optional().describe(`Optional, with section issues or mark, only where this server is not storing the list: the JSON the issues section returned last time, as the agent saved it.`)},pJ=[`search_console`,`google_analytics`,`bing`,`clicky`];function mJ(e){return e}const hJ=async e=>cr({accounts:await e.accounts(),keys:await e.keys(),clicky:pG(await e.keys()).map(mG),signIn:e.signIn,bing:e.deps.bing?await e.deps.bing():void 0,connect:e.connect}),gJ={text:o.string().max(XH).optional().describe(`The copy to check: a page, post, title or description, up to ${XH.toLocaleString(`en-US`)} characters. Markdown, MDX or HTML as written, so line numbers match.`),format:o.enum(YH).optional().describe(`Optional. md (default for text), mdx, html, astro or text. Code, tags and URLs are skipped; captions and titles are checked.`),preset:o.enum(JH).optional().describe(`Optional. blog (default for Markdown), landing (default for HTML: stricter on buzzwords, weak links and claims with no number), docs, or meta (one title or description per line).`),ignore_rules:o.array(o.string().min(1).max(60)).max(50).optional().describe(`Optional rule ids or review lenses to skip.`),ignore_phrases:o.array(o.string().min(1).max(200)).max(50).optional().describe(`Optional phrases to allow, such as a product name a rule keeps matching.`)},_J=[mJ({name:`seo_sites`,command:`data`,title:`List sites`,description:`Lists the Search Console and Google Analytics properties the connected Google accounts can read, the Bing Webmaster site for each, and which research keys are set. Call it first. With add: starts the weekly site audit of that site on the hosted server (a weekly crawl, daily Google index checks, a Monday email and webhook with what changed, and alerts when a page drops out of Google's index). A site that needs paying for returns a Stripe Checkout link: give it to the person to open, and the audit starts once they pay.`,input:o.object({add:lJ.optional().describe(`Optional. A site to add to the weekly site audit. Only when the person asks for it: a site can cost money.`)}),run:async(e,t)=>{if(!t.add)return hJ(e);let n=e.deps.startAudit;if(!n)throw new I(`INVALID_INPUT`,`The weekly site audit runs on the hosted server only. Connect https://seoskill.dev/mcp and call seo_sites with add there.`);let r=await e.resolveSite(t.add),i=await n(r);return{text:{added:`Added ${r} to the weekly site audit. Its first crawl and index checks have started; ask ${e.deps.call(`report`,{site:r,section:kW})} for the findings once the crawl is done.`,exists:`${r} is already in the weekly site audit.`,pending:`The payment went through and the plan is starting. Call seo_sites with add again in a minute.`,checkout:i.status===`checkout`?`${r} needs a plan: $${i.price} a month per site, cancel at any time. Give the person this Stripe Checkout link to open and pay: ${i.url}\nThe audit of ${r} starts once they pay. Call seo_sites with add again afterwards to confirm.`:``,full:i.status===`full`?`This account already audits ${i.limit} sites, the most it can. Remove one on the account page first.`:``,payment_failed:`Stripe could not take the payment for another site, so ${r} was not added. The person can check their card under Manage billing on the account page.`}[i.status],data:{property:r,...i}}}}),mJ({name:`seo_report`,command:`report`,title:`Search Console report`,description:`Start here. Without site: a short summary of every site in the person's weekly site audit (beta) for last week, with each site's Google index, weekly crawl and search clicks, sites with changes first, ending with the site to ask about next; an account outside that beta gets its site list to pick from. With site: the main report for that Search Console property, the overview plus each section's row count and top rows. With site and section: that section's rows; page sections also check their top 20 URLs live. Sections:\n${Object.entries($K).map(([e,t])=>`- ${e}: ${t}`).join(`
409
- `)}\n- ${$G}: A list kept for the site between sessions: issues the striking-distance, low-ctr and quick-wins checks found (and the weekly crawl and Google index checks for an audited site), each with its id, the latest check and what the person marked it (open, fixed or ignored). A fixed page, or query on a page, gets its Search Console clicks and impressions for the 28 and 56 days from the day it was marked fixed against the same days before, once final data covers them. Asking for it runs those checks again. To mark issues, add mark and issues instead of section. The hosted server keeps the list only for sites in the weekly site audit; for any other site it returns the list as JSON for the agent to save and pass back as saved.\nWith site, mark and issues: records what the person decided about those issue ids (fixed, ignored, or open to undo) and runs no checks.`,input:o.object({site:lJ.optional().describe(`Leave it out for the summary of every audited site (weekly site audit beta). Otherwise a Search Console property or site, such as sc-domain:example.com, https://example.com/ or example.com.`),section:o.enum([...eq,$G]).optional().describe(`Optional, needs site. Leave it out first; then ask for one section.`),...uJ,...dJ,...fJ}),run:async(e,t)=>{if(!t.site){if(t.section)throw new I(`INVALID_INPUT`,`section needs a site: ${e.deps.call(`report`,{site:`<site>`,section:t.section})}.`);if(t.mark||t.issues)throw new I(`INVALID_INPUT`,`mark needs a site: ${e.deps.call(`report`,{site:`<site>`,mark:t.mark??`fixed`,issues:t.issues??`<id>`})}.`);return cJ(e.deps,()=>hJ(e))}let n=t.site;if(t.mark||t.issues)return gq({site:await e.resolveSite(n),section:t.section,mark:t.mark,issues:t.issues?.split(`,`),note:t.note,saved:t.saved},e.deps);let r=await e.resolveSite(n).catch(e=>e instanceof Error?e:Error(String(e)));if(r instanceof Error&&!t.section&&e.deps.bing)return _q({site:n,startDate:t.start_date,endDate:t.end_date},e.deps).catch(()=>{throw r});if(r instanceof Error&&t.section!==`bing`)throw r;return gq({site:r instanceof Error?n:r,section:t.section,startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset,saved:t.saved},e.deps)}}),mJ({name:`seo_page`,command:`page`,title:`One page`,description:`Everything for one URL: live page facts (status, redirect, title, description, H1, canonical, robots), Google’s index status from URL Inspection, and the page’s Search Console queries against the previous period.`,input:o.object({url:o.url(),...uJ,...dJ}),run:async(e,t)=>Nq({site:await e.resolveSite(t.url),url:t.url,startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset},e.deps)}),mJ({name:`seo_compare`,command:`compare`,title:`Before and after a date`,description:`Compares equal periods before and after a date, such as a site change or a Google update: totals, devices, and the pages and queries that moved, Google Analytics sessions from the property whose web stream matches the site, and Google updates in the window. Optional pages narrows it to URLs that contain that text, such as /blog/.`,input:o.object({site:lJ,date:o.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(`YYYY-MM-DD: the first day after the change.`),pages:o.string().min(1).optional().describe(`Optional URL text, such as /blog/.`)}),run:async(e,t)=>Fq({site:await e.resolveSite(t.site),date:t.date,pages:t.pages},e.deps)}),mJ({name:`seo_index`,command:`index`,title:`Google indexing reasons and pages that lost their clicks`,description:`${Bq} Use it for a first baseline and then weekly: keep the full result and compare by URL and reason to find new reasons. Also: ${zq}`,input:o.object({site:lJ,...uJ,...dJ}),localInput:o.object({site:lJ,...uJ,max_urls:o.number().int().min(1).max(jG).optional().describe(`URLs to inspect with Google, 100 by default, up to the URL Inspection quota left today.`),...dJ}),run:async(e,t)=>Vq({site:await e.resolveSite(t.site),startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset,maxUrls:t.max_urls},e.deps)}),mJ({name:`seo_research`,command:`research`,title:`Keyword, competitor, backlink and AI answer data`,description:`Data from the person’s own DataForSEO or Ahrefs account, which costs them credits. It uses the key the person saved on their seoskill.dev account (or on this machine for the CLI). Actions: keyword_metrics (keywords: volume, difficulty, CPC), ranked_keywords (target: keywords a domain or URL ranks for), serp (keywords, up to 5: the ranking pages), competitors (target: domains that share keywords), backlinks (target: referring domains), ai_answers (DataForSEO only; target: the site. It takes the site’s top 10 chat-style and long questions from Search Console’s AI-shaped queries itself, then asks ChatGPT search, Google AI Mode and Google AI Overviews each one and says whether the answer cites or mentions the site. DataForSEO’s live prices are $0.012 a question, so up to about $0.12 a run: tell the person before running it). Rows with a domain, and the target, get a free Ahrefs domain_rating column. Numbers are provider estimates, not Search Console data.`,input:o.object({provider:o.enum(yq),action:o.enum(vq),keywords:o.array(o.string().min(1)).max(100).default([]),target:o.string().default(``).describe(`Domain or URL.`),country:o.string().regex(/^[a-z]{2}$/).default(`us`).describe(`Two-letter country code.`),...dJ}),run:async(e,t)=>{let n=e=>`seo_research with the same input, detail "rows", offset ${e}`;if(t.action===`ai_answers`){if(t.provider!==`dataforseo`)throw new I(`INVALID_INPUT`,`ai_answers uses dataforseo.`);return aW({site:await e.resolveSite(t.target),country:t.country,detail:t.detail,offset:t.offset},await e.keys(),e.deps,n)}return Cq(t,await e.keys(),{drKey:e.drKey,call:n})}}),mJ({name:`seo_data`,command:`data`,title:`Raw rows`,description:`Raw rows for questions the report does not answer. search_console (default): rows for any dimensions, each with the previous period and the change; page and query narrow to rows whose URL or query contains that text. google_analytics: landing pages for visits from search, with Google Analytics’ own metric names. bing: Bing Webmaster query or page rows (first dimension) for the same site, by week. clicky: landing-page visits, traffic sources and search engines from a Clicky site the person saved (site is its domain or Clicky site ID; at most 31 days).`,input:o.object({source:o.enum(pJ).default(`search_console`),site:o.string().min(1).describe(`search_console and bing: the property or site. google_analytics: numeric property id from seo_sites. clicky: the site or its Clicky site ID.`),dimensions:o.array(o.enum(qU)).max(4).default([`query`]).describe(`search_console; bing uses the first, query or page.`),page:o.string().min(1).optional().describe(`search_console only: URL text, such as /blog/.`),query:o.string().min(1).optional().describe(`search_console only: query text.`),...uJ,...dJ}),run:async(e,t)=>t.source===`clicky`?wG({site:t.site,startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset},await e.keys(),{connect:e.connect,call:e=>`seo_data with source "clicky", site "${t.site}", detail "rows", offset ${e}`,refresh:e.refreshClicky}):t.source===`bing`?cG({site:t.site,dimension:t.dimensions[0]===`page`?`page`:`query`,startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset},e.deps):t.source===`google_analytics`?sW({property:t.site,startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset},await oW(await e.accounts(),t.site,e.signIn),{signIn:e.signIn,call:e=>`seo_data with source "google_analytics", site "${t.site}", detail "rows", offset ${e}`}):YU({site:await e.resolveSite(t.site),dimensions:t.dimensions,filters:[...t.page?[{dimension:`page`,operator:`contains`,expression:t.page}]:[],...t.query?[{dimension:`query`,operator:`contains`,expression:t.query}]:[]],startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset},e.deps)}),mJ({name:`seo_lint`,command:`lint`,title:`Check copy for AI writing patterns`,description:`Checks landing page copy, blog posts, titles and meta descriptions for patterns common in AI-written text (such as "isn't X, it's Y" reframes, ", not X" contrasts, two-beat fragments, stock idioms and kicker lines), weak copy (dash punctuation, buzzwords, vague link text) and SEO rules (title 30 to 60 characters, description 70 to 160, one H1). Each error and warning has a line, the words, and a concrete fix. Review items are judgement calls for you: read each quote and change it only when it fails the question. It runs offline and reads no accounts.`,input:o.object(gJ),localInput:o.object({...gJ,paths:o.array(o.string().min(1)).max(20).optional().describe(`Optional instead of text: files, folders or globs on this machine (.md, .mdx, .html, .astro, .txt). The format comes from each file name.`)}),run:async(e,t)=>WK({text:t.text,paths:t.paths,format:t.format,preset:t.preset,ignoreRules:t.ignore_rules,ignorePhrases:t.ignore_phrases},e.readFiles)})];function vJ(e){let t=`detail`in e.input.shape;return{...e,description:`${e.description} For a site in the weekly site audit beta it answers at once from the URL Inspection results this server keeps for the site, checked in the background each day, and says how many URLs are covered, how many are queued and the time left.`,run:async(n,r)=>{let i=r,a=n.deps.audit;if(!a)return e.run(n,r);let o=await n.resolveSite(i.site),s=i.detail??`summary`,c=s===`rows`?i.offset??0:0,l=s===`rows`?50:20,u=await a.checks(o,c,l);if(!u){if(await a.add(o)===`full`){let t=await e.run(n,r);return{text:`${t.text}\n\n${o} is not in the weekly site audit, and this account already audits the most sites the beta allows, so this ran the usual way. Remove a site under Beta on the account page to keep its checks here.`,data:t.data}}if(u=await a.checks(o,c,l),!u)throw Error(`The audit of ${o} did not start.`)}return qW({site:o,detail:s,offset:c,paged:t},u,n.deps.call)}}}_J.map(e=>e.name===`seo_index`?vJ(e):e.name===`seo_report`?{...e,description:`${e.description}\n- ${kW}: What the weekly crawl of the site found (broken and redirected internal links, missing or duplicate titles, descriptions and H1s, noindex, canonicals to another page, sitemap URLs that do not return 200, thin pages, tags that JavaScript changes), with what is new, back or resolved since the crawl before. Only for sites added to the weekly site audit beta.`,input:e.input.extend({section:o.enum([...eq,$G,kW]).optional().describe(`Optional. Leave it out first; then ask for one section.`)})}:e);async function yJ(e,t,n,r=!1){let i=(r&&e.localInput||e.input).safeParse(n??{});if(!i.success)throw new I(`INVALID_INPUT`,i.error.issues.map(e=>`${e.path.join(`.`)||`input`}: ${e.message}`).join(`; `));return e.run(t,i.data)}const bJ={name:`seo`,title:`SEO Skill`,description:`Search Console, Google Analytics and keyword data for your AI agent.`,websiteUrl:`https://seoskill.dev`,icons:[{src:`https://seoskill.dev/favicon-light.png`,mimeType:`image/png`,sizes:[`512x512`],theme:`light`},{src:`https://seoskill.dev/favicon-dark.png`,mimeType:`image/png`,sizes:[`512x512`],theme:`dark`},{src:`https://seoskill.dev/favicon.svg`,mimeType:`image/svg+xml`,sizes:[`any`]}]},xJ=(e,t)=>{let n=`seo_${e.replace(`-`,`_`)}`,r=Object.entries(t??{}).filter(([,e])=>e!==void 0);return r.length?`${n} with ${r.map(([e,t])=>`${e} ${typeof t==`number`?t:`"${t}"`}`).join(`, `)}`:n},SJ={name:`seo-skill`,uri:`skill://seo/SKILL.md`,title:`SEO Skill`,description:`How to use these tools: what each command returns and how to reach it.`,mimeType:`text/markdown`},CJ=6048e5;function wJ(e,t,n=Date.now()){let r=/* @__PURE__ */ new Map;try{let i=ss().prepare(`SELECT status_json, fetched_at FROM url_inspection_cache
409
+ `)}\n- ${$G}: A list kept for the site between sessions: issues the striking-distance, low-ctr and quick-wins checks found (and the weekly crawl and Google index checks for an audited site), each with its id, the latest check and what the person marked it (open, fixed or ignored). A fixed page, or query on a page, gets its Search Console clicks and impressions for the 28 and 56 days from the day it was marked fixed against the same days before, once final data covers them. Asking for it runs those checks again. To mark issues, add mark and issues instead of section. The hosted server keeps the list only for sites in the weekly site audit; for any other site it returns the list as JSON for the agent to save and pass back as saved.\nWith site, mark and issues: records what the person decided about those issue ids (fixed, ignored, or open to undo) and runs no checks.`,input:o.object({site:lJ.optional().describe(`Leave it out for the summary of every audited site (weekly site audit beta). Otherwise a Search Console property or site, such as sc-domain:example.com, https://example.com/ or example.com.`),section:o.enum([...eq,$G]).optional().describe(`Optional, needs site. Leave it out first; then ask for one section.`),...uJ,...dJ,...fJ}),run:async(e,t)=>{if(!t.site){if(t.section)throw new I(`INVALID_INPUT`,`section needs a site: ${e.deps.call(`report`,{site:`<site>`,section:t.section})}.`);if(t.mark||t.issues)throw new I(`INVALID_INPUT`,`mark needs a site: ${e.deps.call(`report`,{site:`<site>`,mark:t.mark??`fixed`,issues:t.issues??`<id>`})}.`);return cJ(e.deps,()=>hJ(e))}let n=t.site;if(t.mark||t.issues)return gq({site:await e.resolveSite(n),section:t.section,mark:t.mark,issues:t.issues?.split(`,`),note:t.note,saved:t.saved},e.deps);let r=await e.resolveSite(n).catch(e=>e instanceof Error?e:Error(String(e)));if(r instanceof Error&&!t.section&&e.deps.bing)return _q({site:n,startDate:t.start_date,endDate:t.end_date},e.deps).catch(()=>{throw r});if(r instanceof Error&&t.section!==`bing`)throw r;return gq({site:r instanceof Error?n:r,section:t.section,startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset,saved:t.saved},e.deps)}}),mJ({name:`seo_page`,command:`page`,title:`One page`,description:`Everything for one URL: live page facts (status, redirect, title, description, H1, canonical, robots), Google’s index status from URL Inspection, and the page’s Search Console queries against the previous period.`,input:o.object({url:o.url(),...uJ,...dJ}),run:async(e,t)=>Nq({site:await e.resolveSite(t.url),url:t.url,startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset},e.deps)}),mJ({name:`seo_compare`,command:`compare`,title:`Before and after a date`,description:`Compares equal periods before and after a date, such as a site change or a Google update: totals, devices, and the pages and queries that moved, Google Analytics sessions from the property whose web stream matches the site, and Google updates in the window. Optional pages narrows it to URLs that contain that text, such as /blog/.`,input:o.object({site:lJ,date:o.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(`YYYY-MM-DD: the first day after the change.`),pages:o.string().min(1).optional().describe(`Optional URL text, such as /blog/.`)}),run:async(e,t)=>Fq({site:await e.resolveSite(t.site),date:t.date,pages:t.pages},e.deps)}),mJ({name:`seo_index`,command:`index`,title:`Google indexing reasons and pages that lost their clicks`,description:`${Bq} Use it for a first baseline and then weekly: keep the full result and compare by URL and reason to find new reasons. Also: ${zq}`,input:o.object({site:lJ,...uJ,...dJ}),localInput:o.object({site:lJ,...uJ,max_urls:o.number().int().min(1).max(jG).optional().describe(`URLs to inspect with Google, 100 by default, up to the URL Inspection quota left today.`),...dJ}),run:async(e,t)=>Vq({site:await e.resolveSite(t.site),startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset,maxUrls:t.max_urls},e.deps)}),mJ({name:`seo_research`,command:`research`,title:`Keyword, competitor, backlink and AI answer data`,description:`Data from the person’s own DataForSEO or Ahrefs account, which costs them credits. It uses the key the person saved on their seoskill.dev account (or on this machine for the CLI). Actions: keyword_metrics (keywords: volume, difficulty, CPC), ranked_keywords (target: keywords a domain or URL ranks for), serp (keywords, up to 5: the ranking pages), competitors (target: domains that share keywords), backlinks (target: referring domains), ai_answers (DataForSEO only; target: the site. Without keywords it takes the site’s top 10 chat-style and long questions from Search Console’s AI-shaped queries itself. With up to 10 questions in keywords it asks those instead, and the target can be any domain, such as a competitor's. It asks ChatGPT search, Google AI Mode and Google AI Overviews each one and says whether the answer cites or mentions the site. DataForSEO’s live prices are $0.012 a question, so up to about $0.12 a run: tell the person before running it). Rows with a domain, and the target, get a free Ahrefs domain_rating column. Numbers are provider estimates, not Search Console data.`,input:o.object({provider:o.enum(yq),action:o.enum(vq),keywords:o.array(o.string().min(1)).max(100).default([]),target:o.string().default(``).describe(`Domain or URL.`),country:o.string().regex(/^[a-z]{2}$/).default(`us`).describe(`Two-letter country code.`),...dJ}),run:async(e,t)=>{let n=e=>`seo_research with the same input, detail "rows", offset ${e}`;if(t.action===`ai_answers`){if(t.provider!==`dataforseo`)throw new I(`INVALID_INPUT`,`ai_answers uses dataforseo.`);return aW({site:t.keywords.length?t.target:await e.resolveSite(t.target),country:t.country,detail:t.detail,offset:t.offset,questions:t.keywords},await e.keys(),e.deps,n)}return Cq(t,await e.keys(),{drKey:e.drKey,call:n})}}),mJ({name:`seo_data`,command:`data`,title:`Raw rows`,description:`Raw rows for questions the report does not answer. search_console (default): rows for any dimensions, each with the previous period and the change; page and query narrow to rows whose URL or query contains that text. google_analytics: landing pages for visits from search, with Google Analytics’ own metric names. bing: Bing Webmaster query or page rows (first dimension) for the same site, by week. clicky: landing-page visits, traffic sources and search engines from a Clicky site the person saved (site is its domain or Clicky site ID; at most 31 days).`,input:o.object({source:o.enum(pJ).default(`search_console`),site:o.string().min(1).describe(`search_console and bing: the property or site. google_analytics: numeric property id from seo_sites. clicky: the site or its Clicky site ID.`),dimensions:o.array(o.enum(qU)).max(4).default([`query`]).describe(`search_console; bing uses the first, query or page.`),page:o.string().min(1).optional().describe(`search_console only: URL text, such as /blog/.`),query:o.string().min(1).optional().describe(`search_console only: query text.`),...uJ,...dJ}),run:async(e,t)=>t.source===`clicky`?wG({site:t.site,startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset},await e.keys(),{connect:e.connect,call:e=>`seo_data with source "clicky", site "${t.site}", detail "rows", offset ${e}`,refresh:e.refreshClicky}):t.source===`bing`?cG({site:t.site,dimension:t.dimensions[0]===`page`?`page`:`query`,startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset},e.deps):t.source===`google_analytics`?sW({property:t.site,startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset},await oW(await e.accounts(),t.site,e.signIn),{signIn:e.signIn,call:e=>`seo_data with source "google_analytics", site "${t.site}", detail "rows", offset ${e}`}):YU({site:await e.resolveSite(t.site),dimensions:t.dimensions,filters:[...t.page?[{dimension:`page`,operator:`contains`,expression:t.page}]:[],...t.query?[{dimension:`query`,operator:`contains`,expression:t.query}]:[]],startDate:t.start_date,endDate:t.end_date,detail:t.detail,offset:t.offset},e.deps)}),mJ({name:`seo_lint`,command:`lint`,title:`Check copy for AI writing patterns`,description:`Checks landing page copy, blog posts, titles and meta descriptions for patterns common in AI-written text (such as "isn't X, it's Y" reframes, ", not X" contrasts, two-beat fragments, stock idioms and kicker lines), weak copy (dash punctuation, buzzwords, vague link text) and SEO rules (title 30 to 60 characters, description 70 to 160, one H1). Each error and warning has a line, the words, and a concrete fix. Review items are judgement calls for you: read each quote and change it only when it fails the question. It runs offline and reads no accounts.`,input:o.object(gJ),localInput:o.object({...gJ,paths:o.array(o.string().min(1)).max(20).optional().describe(`Optional instead of text: files, folders or globs on this machine (.md, .mdx, .html, .astro, .txt). The format comes from each file name.`)}),run:async(e,t)=>WK({text:t.text,paths:t.paths,format:t.format,preset:t.preset,ignoreRules:t.ignore_rules,ignorePhrases:t.ignore_phrases},e.readFiles)})];function vJ(e){let t=`detail`in e.input.shape;return{...e,description:`${e.description} For a site in the weekly site audit beta it answers at once from the URL Inspection results this server keeps for the site, checked in the background each day, and says how many URLs are covered, how many are queued and the time left.`,run:async(n,r)=>{let i=r,a=n.deps.audit;if(!a)return e.run(n,r);let o=await n.resolveSite(i.site),s=i.detail??`summary`,c=s===`rows`?i.offset??0:0,l=s===`rows`?50:20,u=await a.checks(o,c,l);if(!u){if(await a.add(o)===`full`){let t=await e.run(n,r);return{text:`${t.text}\n\n${o} is not in the weekly site audit, and this account already audits the most sites the beta allows, so this ran the usual way. Remove a site under Beta on the account page to keep its checks here.`,data:t.data}}if(u=await a.checks(o,c,l),!u)throw Error(`The audit of ${o} did not start.`)}return qW({site:o,detail:s,offset:c,paged:t},u,n.deps.call)}}}_J.map(e=>e.name===`seo_index`?vJ(e):e.name===`seo_report`?{...e,description:`${e.description}\n- ${kW}: What the weekly crawl of the site found (broken and redirected internal links, missing or duplicate titles, descriptions and H1s, noindex, canonicals to another page, sitemap URLs that do not return 200, thin pages, tags that JavaScript changes), with what is new, back or resolved since the crawl before. Only for sites added to the weekly site audit beta.`,input:e.input.extend({section:o.enum([...eq,$G,kW]).optional().describe(`Optional. Leave it out first; then ask for one section.`)})}:e);async function yJ(e,t,n,r=!1){let i=(r&&e.localInput||e.input).safeParse(n??{});if(!i.success)throw new I(`INVALID_INPUT`,i.error.issues.map(e=>`${e.path.join(`.`)||`input`}: ${e.message}`).join(`; `));return e.run(t,i.data)}const bJ={name:`seo`,title:`SEO Skill`,description:`Search Console, Google Analytics and keyword data for your AI agent.`,websiteUrl:`https://seoskill.dev`,icons:[{src:`https://seoskill.dev/favicon-light.png`,mimeType:`image/png`,sizes:[`512x512`],theme:`light`},{src:`https://seoskill.dev/favicon-dark.png`,mimeType:`image/png`,sizes:[`512x512`],theme:`dark`},{src:`https://seoskill.dev/favicon.svg`,mimeType:`image/svg+xml`,sizes:[`any`]}]},xJ=(e,t)=>{let n=`seo_${e.replace(`-`,`_`)}`,r=Object.entries(t??{}).filter(([,e])=>e!==void 0);return r.length?`${n} with ${r.map(([e,t])=>`${e} ${typeof t==`number`?t:`"${t}"`}`).join(`, `)}`:n},SJ={name:`seo-skill`,uri:`skill://seo/SKILL.md`,title:`SEO Skill`,description:`How to use these tools: what each command returns and how to reach it.`,mimeType:`text/markdown`},CJ=6048e5;function wJ(e,t,n=Date.now()){let r=/* @__PURE__ */ new Map;try{let i=ss().prepare(`SELECT status_json, fetched_at FROM url_inspection_cache
410
410
  WHERE site_url = ? AND url = ? AND fetched_at > ?`);for(let a of t){let t=i.get(e,a,n-CJ);t&&r.set(a,{status:JSON.parse(t.status_json),inspectedAt:new Date(t.fetched_at).toISOString()})}}catch{return/* @__PURE__ */ new Map}return r}function TJ(e,t,n,r=Date.now()){let i=JSON.stringify(n);cs(n=>{n.prepare(`INSERT OR REPLACE INTO url_inspection_cache
411
411
  (site_url, url, status_json, fetched_at, expires_at)
412
412
  VALUES (?, ?, ?, ?, ?)`).run(e,t,i,r,r+CJ),ls(Buffer.byteLength(i)+t.length,n)})}function EJ(e){let t=t=>{let n=t.trim();if(!n||n.length>4096)throw new bt({provider:e.provider,operation:`credentials`,code:`configuration`,message:e.invalidMessage});return n};return{async write(n){return wk(e.secretName,t(n))},async read(n={}){let r=await Ck({name:e.secretName,envVar:e.environmentVariable,env:n.env});return r?{apiKey:t(r.value),source:r.source}:void 0},async remove(){await Tk(e.secretName)}}}const DJ=`SEO_AHREFS_API_KEY`,OJ=mk.ahrefsApiKey,kJ=EJ({provider:`ahrefs`,environmentVariable:DJ,secretName:OJ,invalidMessage:`Ahrefs needs a valid API v3 key.`});async function AJ(e){return kJ.write(e)}async function jJ(e={}){return kJ.read(e)}async function MJ(){await kJ.remove()}const NJ=`SEO_BING_API_KEY`,PJ=mk.bingApiKey,FJ=EJ({provider:`bing`,environmentVariable:NJ,secretName:PJ,invalidMessage:`Bing Webmaster needs a valid API key.`});async function IJ(e){return FJ.write(e)}async function LJ(e={}){return FJ.read(e)}async function RJ(){await FJ.remove()}const zJ=`SEO_DATAFORSEO_LOGIN`,BJ=`SEO_DATAFORSEO_PASSWORD`,VJ=mk.dataForSeoCredentials,HJ=o.object({version:o.literal(1),login:o.string().trim().min(1).max(320),password:o.string().min(1).max(2048)}).strict();function UJ(e){return new bt({provider:`dataforseo`,operation:`credentials`,code:`configuration`,message:e})}function WJ(e){let t=HJ.safeParse({version:1,login:e.login,password:e.password});if(!t.success)throw UJ(`DataForSEO credentials need both an API login and API password.`);return{login:t.data.login,password:t.data.password}}function GJ(){let e=pa();return!e.providers.dataForSeoLogin&&!e.providers.dataForSeoPassword?!1:(ma({...e,providers:{...e.providers,dataForSeoLogin:void 0,dataForSeoPassword:void 0}}),!0)}function KJ(e){let t=e[zJ]?.trim(),n=e[BJ];if(t||n){if(!t||!n)throw UJ(`Set both ${zJ} and ${BJ}.`);return WJ({login:t,password:n})}}function qJ(e){let t;try{t=JSON.parse(e)}catch{throw UJ("Saved DataForSEO credentials are invalid. Run `seo providers dataforseo disconnect`, then connect again.")}let n=HJ.safeParse(t);if(!n.success)throw UJ("Saved DataForSEO credentials are invalid. Run `seo providers dataforseo disconnect`, then connect again.");return{login:n.data.login,password:n.data.password}}async function JJ(e){let t=WJ(e),n=await wk(VJ,JSON.stringify({version:1,...t}));return GJ(),n}async function YJ(){let e=pa(),t=e.providers.dataForSeoLogin,n=e.providers.dataForSeoPassword;if(!t&&!n)return;if(!t||!n)throw UJ("Legacy DataForSEO config contains only part of the credential pair. Run `seo providers dataforseo disconnect`, then connect again.");let r=WJ({login:t,password:n}),i=await JJ(r);return{...r,source:i,migrated:!0}}async function XJ(e={}){let t=KJ(e.env??process.env);if(t)return{...t,source:`environment`,migrated:!1};let n=await Ck({name:VJ});if(n){let e=qJ(n.value),t=GJ();return{...e,source:n.source,migrated:t}}return YJ()}async function ZJ(){await Tk(VJ),GJ()}const QJ=()=>g(br().configDir,`issues.json`);function $J(){let e=wr(QJ());return e?.version===1&&e.sites?e:{version:1,sites:{}}}const eY={read:async e=>$J().sites[e]??[],write:async(e,t)=>{let n=$J();n.sites[e]=t,Tr(QJ(),n,384)}};function tY(e){return{searchAnalytics:(t,n)=>pS(t,n,{refresh:e.refresh,accountEmail:e.account}),checkPages:YK,liveCheck:ZK,inspect:async(t,n)=>Mq(await uS({siteUrl:t,inspectionUrl:n,accountEmail:e.account})),inspectionQuota:t=>sS({siteUrl:t,accountEmail:e.account}),savedInspections:{read:(t,n)=>e.refresh?/* @__PURE__ */ new Map:wJ(t,n),save:TJ},pageMarkdown:async t=>cy(await Gx(t,{refresh:e.refresh})),call:e.call,bing:iY,bingConnect:nY,analytics:{accounts:aY,signIn:Fo.signIn},issues:eY,...Mi()?{week:Ri}:{}}}const nY="Connect Bing on your seoskill.dev account and run `seo login`, or run `seo providers bing connect` with an API key from Bing Webmaster Tools (Settings, API access).";let rY;function iY(){return rY??=LJ().catch(()=>void 0).then(e=>e?qt({apiKey:e.apiKey},{signIn:nY}):Mi()?Gi():null),rY}async function aY(){return Mi()?(await Hi()).map(e=>({email:e,google:It(()=>Vi(e))})):[{email:null,google:await fY()}]}async function oY(){let[e,t,n,r]=await Promise.all([XJ().catch(()=>void 0),jJ().catch(()=>void 0),Fk().catch(()=>({})),Mi()?Ki().catch(()=>({})):{}]),i=new Headers(r);for(let[e,t]of Object.entries(n)){let n=`${fG}${e}`;i.has(n)||i.set(n,t)}return e&&(i.set(`x-dataforseo-login`,e.login),i.set(`x-dataforseo-password`,e.password)),t?.apiKey&&i.set(`x-ahrefs-key`,t.apiKey),i}async function sY(e){let t=await lY(e.site,{refresh:e.refresh}).then(t=>({property:t,query:n=>pS(t,n,{refresh:e.refresh})}),e=>({unavailable:`no Search Console property was found for this site (${e instanceof Error?e.message:String(e)})`})),n=process.env.TYPESAFE_API_KEY?.trim(),r=n?void 0:(await oY().catch(()=>new Headers)).get(`x-typesafe-key`);return{searchConsole:t,decisionModel:n?{model:JF({apiKey:n}),keySource:`environment`}:r?{model:JF({apiKey:r}),keySource:`account`}:{unavailable:`no TypeSafe key is set`}}}async function cY(){return Mi()?(await Li({refreshClicky:!0}),oY()):null}async function lY(e,t={}){let n=(await bS(t.refresh,t.account)).map(e=>e.siteUrl),r=Bn(n,e);if(!r)throw new I(`NO_ACCESS`,`This Google login has no Search Console property for ${e}. ${zn(n)}`);return r}const uY={INVALID_INPUT:`INVALID_INPUT`,NOT_CONNECTED:`AUTH_REQUIRED`,NO_ACCESS:`PROPERTY_NOT_FOUND`,NO_KEY:`AUTH_CONFIG_REQUIRED`,NO_DATA:`INSUFFICIENT_DATA`,RATE_LIMITED:`RATE_LIMITED`,GOOGLE_ERROR:`PROVIDER_UNAVAILABLE`,PROVIDER_ERROR:`PROVIDER_UNAVAILABLE`};function dY(e){return e instanceof I?new F(uY[e.code],e.message):e}async function fY(e){let{client:t}=await No(e);return Po(t)}const pY=[`https://www.googleapis.com/auth/webmasters.readonly`,`https://www.googleapis.com/auth/analytics.readonly`];async function mY(e={}){let t=br(),n=Ba(),r=to(),i=await Oa(),a=Fa(),o=i?!!Ha(i.client_source):void 0,s=[];try{let t=(e.checkDatabase??us)();s.push({id:`local-database`,label:`Local database`,status:`pass`,detail:t.dbPath})}catch(e){s.push({id:`local-database`,label:`Local database`,status:`fail`,detail:e instanceof Error?e.message:`The local database could not be opened.`,fix:"Upgrade or reinstall `seo`. If this still fails, report it at https://github.com/iannuttall/seo/issues."})}s.push({id:`config-dir`,label:`Config directory`,status:S(t.configDir)?`pass`:`warn`,detail:t.configDir,fix:S(t.configDir)?void 0:"Run `seo start` or `seo auth setup-client`."}),s.push({id:`oauth-client`,label:`Google credentials`,status:r.configured?`pass`:o===!1||r.error?`fail`:n.sharedConfigured||n.byoConfigured?`pass`:`fail`,detail:r.configured?`Service account configured for ${r.identity}.`:o===!1?`Stored Google login uses the ${i?.client_source===`shared`?`shared seo app`:`BYO client`}, but that client is not configured.`:r.error?r.error:n.sharedConfigured?`Shared client configured.`:n.byoConfigured?`BYO client configured at ${t.oauthClientFile}.`:`No shared or BYO Google OAuth client configured.`,fix:r.configured?void 0:o===!1?i?.client_source===`shared`?"Reinstall `seo`. If the shared client is still missing, report it at https://github.com/iannuttall/seo/issues.":"Restore the same client with `seo auth setup-client`, or run `seo auth logout` before signing in with a different client.":r.error?"Set one valid service account credential source, then run `seo doctor` again.":n.sharedConfigured||n.byoConfigured?void 0:"Run `seo auth setup-client` or set SEO_GOOGLE_CLIENT_ID and SEO_GOOGLE_CLIENT_SECRET."}),s.push({id:`oauth-client-file`,label:`BYO OAuth file`,status:a?`pass`:`warn`,detail:a?t.oauthClientFile:`No BYO OAuth client file. This is fine if using a shared client.`}),s.push({id:`google-login`,label:`Google login`,status:r.configured||i?`pass`:`fail`,detail:r.configured?`Using service account ${r.identity}.`:i?`Signed in as ${i.account_email}.`:`No Google token found.`,fix:r.configured||i?void 0:"Run `seo auth login`, or configure a service account for CI."});let c=new Set(i?.scope.split(/\s+/).filter(Boolean)??[]);for(let e of pY)s.push({id:`scope:${e}`,label:e.endsWith(`webmasters.readonly`)?`Search Console scope`:`Google Analytics scope`,status:r.configured||i&&c.has(e)?`pass`:i?`fail`:`warn`,detail:r.configured?`${e} requested by the service account.`:i?c.has(e)?e:`Missing ${e}.`:`Cannot inspect scopes until login is complete.`,fix:!r.configured&&i&&!c.has(e)?"Run `seo auth logout` then `seo auth login`.":void 0});return{ok:s.every(e=>e.status!==`fail`),generatedAt:(/* @__PURE__ */ new Date()).toISOString(),checks:s}}const hY=/* @__PURE__ */ new Set([`.md`,`.mdx`,`.markdown`,`.html`,`.htm`,`.astro`,`.txt`]),gY=/* @__PURE__ */ new Set([`node_modules`,`.git`,`dist`,`.astro`,`.next`]);function _Y(){return new F(`INVALID_INPUT`,`More than 100 files match. Point seo lint at fewer files or a smaller folder.`)}async function vY(e,t){let n=await Ce(e,{withFileTypes:!0});n.sort((e,t)=>e.name.localeCompare(t.name));for(let r of n){if(r.name.startsWith(`.`)&&r.isDirectory())continue;let n=g(e,r.name);if(r.isDirectory()?gY.has(r.name)||await vY(n,t):hY.has(m(r.name).toLowerCase())&&t.push(n),t.length>100)throw _Y()}}async function yY(e,t=process.cwd()){let n=[];for(let r of e){if(/[*?[{]/.test(r)){let e=[];for await(let i of ye(r,{cwd:t}))if(e.push(v(t,i)),n.length+e.length>100)throw _Y();n.push(...e.sort());continue}let e=v(t,r),i=await Te(e).catch(()=>null);if(!i)throw new F(`INVALID_INPUT`,`No file or folder at ${r}.`);if(i.isDirectory()?await vY(e,n):n.push(e),n.length>100)throw _Y()}return[...new Set(n)]}async function bY(e,t=process.cwd()){let n=await yY(e,t);if(!n.length)throw new F(`INVALID_INPUT`,`No Markdown, MDX, HTML, Astro or text files match.`);let r=[];for(let e of n){let n=_(t,e)||e,i=n.startsWith(`..`)?e:n,a=await Te(e);if(a.size>8e5){r.push({path:i,skipped:`It is ${Math.round(a.size/1e3).toLocaleString(`en-US`)} KB; seo lint reads files up to ${XH/1e3} KB of text.`});continue}let o=await Se(e,`utf8`);o.length>2e5?r.push({path:i,skipped:`It has ${o.length.toLocaleString(`en-US`)} characters; seo lint reads up to ${XH.toLocaleString(`en-US`)}.`}):r.push({path:i,text:o})}return r}function xY(e){let t=Object.entries(e);if(t.length>100)throw Error(`Provider action input is too large.`);for(let[e]of t)if(!/^[A-Za-z][A-Za-z0-9-]{0,63}$/u.test(e))throw Error(`Provider action parameter names are invalid.`);let n=SY(Object.fromEntries(t));return CY(n,{maxBytes:65536,maxDepth:16,maxValues:5e3,message:`Provider action input is too large.`}),n}function SY(e){return e===null||typeof e==`string`||typeof e==`boolean`||typeof e==`number`?e:Array.isArray(e)?e.map(SY):Object.fromEntries(Object.entries(e).sort(([e],[t])=>e<t?-1:+(e>t)).map(([e,t])=>[e,SY(t)]))}function CY(e,t){let n=0,r=/* @__PURE__ */ new WeakSet,i=(e,a)=>{if(n+=1,n>t.maxValues||a>t.maxDepth)throw Error(t.message);if(e!==null&&typeof e!=`string`&&typeof e!=`boolean`&&!(typeof e==`number`&&Number.isFinite(e))){if(typeof e!=`object`)throw Error(`Provider action result must contain JSON values only.`);if(r.has(e))throw Error(`Provider action result must not contain cycles.`);if(r.add(e),Array.isArray(e))for(let t of e)i(t,a+1);else for(let[t,n]of Object.entries(e)){if(t.length>256)throw Error(`Provider action result contains a key that is too long.`);i(n,a+1)}r.delete(e)}};i(e,0);let a=JSON.stringify(e);if(Buffer.byteLength(a)>t.maxBytes)throw Error(t.message);return JSON.parse(a)}function wY(e,t,n){let r;try{r=o.fromJSONSchema(e)}catch{throw Error(`Provider action ${n} schema is invalid.`)}let i=r.safeParse(t);if(!i.success)throw Error(`Provider action ${n} does not match its JSON schema.`);return i.data}async function TY(e){let t={...e.supplied};return Object.keys(t).length===0&&(t=await lA({providerId:e.providerId,account:e.account,fields:e.fields})),t}async function EY(e){let t=await MA(),n=t.registry.get(e.providerId),r=t.registry.action(e.providerId,e.actionId);if(!n)throw Error(`Provider ${e.providerId} is not installed.`);if(!r)throw Error(`Provider ${e.providerId} does not support action ${e.actionId}.`);let i=jA(n,e.account),a=SY(wY(r.inputSchema,xY(e.params??{}),`input`)),s={provider:e.providerId,credentialScope:lk(e.providerId,JSON.stringify(i)),operation:`action:${r.id}`,request:{account:i,params:a}};if(!e.refresh&&r.cacheTtlMs){let t=fk(s,o.unknown());if(t)return{provider:e.providerId,action:r.id,cache:`hit`,data:SY(wY(r.outputSchema,t.data,`output`))}}let c=await TY({providerId:e.providerId,account:i,fields:n.connection.fields,supplied:e.credentials}),l;try{l=await r.run({account:i,credentials:c,params:a},PA(e.providerId,e.runtime??{}))}catch(t){throw NA(e.providerId,t,c)}let u=SY(wY(r.outputSchema,CY(l,{maxBytes:10485760,maxDepth:20,maxValues:1e5,message:`Provider action result is too large.`}),`output`));if(r.cacheTtlMs){let e=u&&typeof u==`object`&&!Array.isArray(u)&&Array.isArray(u.rows)?u.rows.length:null;pk(s,{data:u,ttlMs:r.cacheTtlMs,rowCount:e,sourceCostMicros:null,taskIds:[]})}return{provider:e.providerId,action:r.id,cache:r.cacheTtlMs?e.refresh?`bypass`:`miss`:`disabled`,data:u}}function DY(e){let t=Gk.parse(e);return{...pa().providers.connections?.[t]??{}}}function OY(e,t){let n=Gk.parse(e),r=pa(),i=Object.fromEntries(Object.entries(t).map(([e,t])=>[e,t.trim()]).filter(([,e])=>e));return ma({...r,providers:{...r.providers,connections:{...r.providers.connections,[n]:i}}}),i}function kY(e){let t=Gk.parse(e),n=pa(),r={...n.providers.connections};delete r[t],ma({...n,providers:{...n.providers,connections:r}})}async function AY(){let e=bA().packages,t=await AA({installed:e}),n=new Map(t.failures.map(e=>[e.id,e]));return{schemaVersion:1,installed:e.map(e=>({...e,loadStatus:n.has(e.id)?`failed`:`ready`,error:n.get(e.id)?.message}))}}async function jY(e){let t=await AA(),n=t.registry.get(e);if(!n){let n=t.failures.find(t=>t.id===e);throw Error(n?`Provider ${e} could not load: ${n.message}`:`Provider ${e} is not installed.`)}return{schemaVersion:1,id:n.id,displayName:n.displayName,description:n.description,package:n.package,version:n.version,kinds:n.kinds,connection:n.connection.fields.map(e=>({id:e.id,label:e.label,description:e.description,kind:e.kind,required:e.required!==!1,envVar:e.envVar})),capabilities:n.capabilities.map(e=>JSON.parse(JSON.stringify(e))),actions:(n.actions??[]).map(e=>({id:e.id,description:e.description,inputSchema:e.inputSchema,outputSchema:e.outputSchema,cacheTtlMs:e.cacheTtlMs??0}))}}const MY=De(Oe),NY=5242880,PY=o.object({name:Kk,version:qk,dependencies:o.record(o.string(),o.string()).optional(),optionalDependencies:o.record(o.string(),o.string()).optional(),bundledDependencies:o.array(o.string()).optional(),dist:o.object({integrity:Jk,unpackedSize:o.number().int().nonnegative()}).passthrough(),seo:nA,description:o.string().trim().max(500).nullish(),repository:o.union([o.string().trim().max(2048),o.object({url:o.string().trim().max(2048)}).passthrough()]).nullish(),author:o.union([o.string().trim().max(500),o.object({name:o.string().trim().max(200)}).passthrough()]).nullish(),maintainers:o.array(o.union([o.string().trim().max(200),o.object({name:o.string().trim().max(200)}).passthrough()])).max(100).nullish(),_npmUser:o.union([o.string().trim().max(200),o.object({name:o.string().trim().max(200)}).passthrough()]).nullish()}).passthrough(),FY=o.object({package:Kk,version:qk,integrity:Jk,unpackedSize:o.number().int().nonnegative(),apiVersion:o.literal(1),entryPoints:o.number().int().min(1).max(16),description:o.string().trim().max(500).optional(),repository:o.string().trim().max(2048).optional(),publisher:o.string().trim().max(200).optional()}).strict(),IY=o.object({packages:o.record(o.string(),o.object({version:qk.optional(),integrity:Jk.optional()}).passthrough())}).passthrough();async function LY(e,t,n){let r=await MY(e,[...t],{cwd:n.cwd,env:{...process.env,NPM_CONFIG_UPDATE_NOTIFIER:`false`},maxBuffer:1048576});return{stdout:r.stdout,stderr:r.stderr}}function RY(e){xr(),C(e,{recursive:!0,mode:448});let t=g(e,`package.json`);S(t)||Tr(t,{name:`seo-local-provider-packages`,private:!0,type:`module`,dependencies:{}},420)}function zY(e){let t=Object.keys(e.dependencies??{}),n=Object.keys(e.optionalDependencies??{}),r=e.bundledDependencies??[];if(t.length>0||n.length>0||r.length>0)throw Error(`Provider packages must bundle their runtime code and declare no runtime, optional, or bundled dependencies.`);if(e.dist.unpackedSize>NY)throw Error(`Provider package exceeds the ${NY} byte unpacked-size limit.`)}function BY(e,t){let n=IY.parse(JSON.parse(w(g(e,`package-lock.json`),`utf8`))).packages[`node_modules/${t.package}`];if(n?.version!==t.version||n.integrity!==t.integrity)throw Error(`${t.package} does not match the approved package release.`)}function VY(e){let t=e.trim(),n=t,r;if(t.startsWith(`@`)){let e=t.indexOf(`/`),i=t.lastIndexOf(`@`);e>1&&i>e&&(n=t.slice(0,i),r=t.slice(i+1))}else{let e=t.lastIndexOf(`@`);e>0&&(n=t.slice(0,e),r=t.slice(e+1))}if(!Kk.safeParse(n).success||r!==void 0&&!qk.safeParse(r).success)throw Error(`Provider package must be an npm package name with an optional exact version, such as @scope/provider or @scope/provider@1.2.3.`);return{package:n,...r?{version:r}:{}}}function HY(e){return(typeof e==`string`?e:e?.url)?.replace(/^git\+/u,``).replace(/\.git$/u,``)}function UY(e){if(typeof e._npmUser==`string`)return e._npmUser;if(e._npmUser?.name)return e._npmUser.name;if(typeof e.author==`string`)return e.author;if(e.author?.name)return e.author.name;let t=e.maintainers?.[0];return typeof t==`string`?t:t?.name}async function WY(e,t,n){let r=await n.run(process.platform===`win32`?`npm.cmd`:`npm`,[`view`,t?`${e}@${t}`:e,`--json`],{cwd:n.packagesDir}),i=PY.parse(JSON.parse(r.stdout));if(i.name!==e)throw Error(`npm returned metadata for ${i.name}, not ${e}.`);if(t&&i.version!==t)throw Error(`npm returned ${i.name}@${i.version}, not ${e}@${t}.`);return zY(i),i}async function GY(e,t={}){let n=t.packagesDir??br().providerPackagesDir,r=t.run??LY,i=VY(e);RY(n);let a=await WY(i.package,i.version,{packagesDir:n,run:r});return FY.parse({package:a.name,version:a.version,integrity:a.dist.integrity,unpackedSize:a.dist.unpackedSize,apiVersion:a.seo.apiVersion,entryPoints:a.seo.providers.length,...a.description?{description:a.description}:{},...HY(a.repository)?{repository:HY(a.repository)}:{},...UY(a)?{publisher:UY(a)}:{}})}async function KY(e,t={}){let n=FY.parse(e),r=t.packagesDir??br().providerPackagesDir,i=t.run??LY,a=bA();if(a.packages.some(e=>e.package===n.package))throw Error(`${n.package} is already installed. Remove it before installing another version.`);RY(r);let o=await WY(n.package,n.version,{packagesDir:r,run:i});if(o.version!==n.version||o.dist.integrity!==n.integrity||o.seo.apiVersion!==n.apiVersion||o.seo.providers.length!==n.entryPoints)throw Error(`${n.package} npm metadata changed after approval. Run the install command again to review it.`);await i(process.platform===`win32`?`npm.cmd`:`npm`,[`install`,`--ignore-scripts`,`--no-audit`,`--no-fund`,`--legacy-peer-deps`,`--omit=optional`,`--save-exact`,`${n.package}@${n.version}`],{cwd:r});try{BY(r,n);let e=await kA({package:n.package,version:n.version},{packagesDir:r});if(e.length!==1)throw Error(`${n.package} must register exactly one provider; received ${e.length}.`);let i=e[0];if(!i)throw Error(`${n.package} did not register a provider.`);if(a.packages.some(e=>e.id===i.id))throw Error(`Provider ${i.id} is already installed.`);let o={id:i.id,package:n.package,version:n.version,integrity:n.integrity,enabled:!0,installedAt:(t.now??(()=>/* @__PURE__ */ new Date))().toISOString()};return SA(o),o}catch(e){throw await i(process.platform===`win32`?`npm.cmd`:`npm`,[`uninstall`,`--ignore-scripts`,`--no-audit`,`--no-fund`,n.package],{cwd:r}).catch(()=>void 0),e}}async function qY(e,t={}){let n=t.packagesDir??br().providerPackagesDir,r=t.run??LY,i=bA().packages.find(t=>t.id===e);if(i)return await r(process.platform===`win32`?`npm.cmd`:`npm`,[`uninstall`,`--ignore-scripts`,`--no-audit`,`--no-fund`,i.package],{cwd:n}),CA(e)}const JY=o.object({limits_and_usage:o.object({api_key_expiration_date:o.string().trim().min(1).max(100),subscription:o.string().trim().min(1).max(200),units_limit_api_key:o.number().int().nonnegative().nullable(),units_limit_workspace:o.number().int().nonnegative().nullable(),units_usage_api_key:o.number().int().nonnegative(),units_usage_workspace:o.number().int().nonnegative().nullable(),usage_reset_date:o.string().trim().min(1).max(100)}).strict()}).strict();function YY(e,t){return e===null||t===null?null:Math.max(0,e-t)}var XY=class{apiKey;credentials;fetch;baseUrl;timeoutMs;now;constructor(e={}){this.apiKey=e.apiKey?.trim(),this.credentials=e.credentials,this.fetch=e.fetch??ne,this.baseUrl=e.baseUrl??`https://api.ahrefs.com/v3/`,this.timeoutMs=e.timeoutMs??1e4,this.now=e.now??(()=>/* @__PURE__ */ new Date)}async getApiKey(e){let t=this.apiKey??await this.credentials?.()??(await jJ())?.apiKey;if(!t?.trim()||t.trim().length>4096)throw new bt({provider:`ahrefs`,operation:e,code:`configuration`,message:"Ahrefs is not connected. Run `seo providers ahrefs connect`, or set SEO_AHREFS_API_KEY for this process."});return t.trim()}async limitsAndUsageForKey(e){let t=`limits-and-usage`,n=(await _A({provider:`ahrefs`,operation:t,url:new URL(`subscription-info/limits-and-usage`,this.baseUrl),fetch:this.fetch,maxResponseBytes:65536,timeoutMs:this.timeoutMs,retry:`safe`,schema:JY,init:{method:`GET`,headers:{accept:`application/json`,authorization:`Bearer ${e}`}}}).catch(e=>{throw e instanceof bt&&e.code===`authentication`?new bt({provider:`ahrefs`,operation:t,code:`authentication`,status:e.status??void 0,message:`Ahrefs rejected the API v3 key. Create or copy a key from Ahrefs Account settings, then connect again.`,cause:e}):e})).limits_and_usage;return{provider:`ahrefs`,apiVersion:3,subscription:n.subscription,apiKeyExpiresAt:n.api_key_expiration_date,usageResetsAt:n.usage_reset_date,apiKeyUnits:{limit:n.units_limit_api_key,used:n.units_usage_api_key,remaining:YY(n.units_limit_api_key,n.units_usage_api_key)},workspaceUnits:{limit:n.units_limit_workspace,used:n.units_usage_workspace,remaining:YY(n.units_limit_workspace,n.units_usage_workspace)},observedAt:this.now().toISOString(),requestCostUnits:0}}async limitsAndUsage(){return this.limitsAndUsageForKey(await this.getApiKey(`limits-and-usage`))}};const ZY=o.object({total:o.number().finite().nonnegative().optional(),balance:o.number().finite().optional()}).passthrough(),QY=o.object({login:o.string().min(1).max(320),timezone:o.string().min(1).max(100).optional(),money:ZY.optional()}).passthrough(),$Y=o.object({id:o.string().min(1).max(100).optional(),status_code:o.number().int(),status_message:o.string().max(1e3),cost:o.number().finite().nonnegative().optional(),result_count:o.number().int().nonnegative().optional(),result:o.array(QY).max(10).nullable()}).passthrough(),eX=o.object({version:o.string().min(1).max(100).optional(),status_code:o.number().int(),status_message:o.string().max(1e3),cost:o.number().finite().nonnegative().optional(),tasks_count:o.number().int().nonnegative(),tasks_error:o.number().int().nonnegative(),tasks:o.array($Y).max(10)}).passthrough();function tX(e){return e===void 0?null:Math.round(e*1e6)}function nX(e){return e>=40100&&e<40200?`authentication`:e===40202?`rate-limit`:`remote-error`}var rX=class{fetch;credentials;baseUrl;timeoutMs;maxResponseBytes;now;constructor(e={}){this.fetch=e.fetch??ne,this.credentials=e.credentials??XJ,this.baseUrl=e.baseUrl??`https://api.dataforseo.com/`,this.timeoutMs=e.timeoutMs??15e3,this.maxResponseBytes=e.maxResponseBytes??5242880,this.now=e.now??(()=>/* @__PURE__ */ new Date)}async getCredentials(e){let t=await this.credentials();if(!t)throw new bt({provider:`dataforseo`,operation:e,code:`configuration`,message:"DataForSEO is not connected. Run `seo providers dataforseo connect`, or set SEO_DATAFORSEO_LOGIN and SEO_DATAFORSEO_PASSWORD."});return t}authorization(e){return`Basic ${Buffer.from(`${e.login}:${e.password}`).toString(`base64`)}`}async userDataForCredentials(e){let t;try{t=await _A({provider:`dataforseo`,operation:`account-status`,url:new URL(`v3/appendix/user_data`,this.baseUrl),fetch:this.fetch,maxResponseBytes:this.maxResponseBytes,timeoutMs:this.timeoutMs,retry:`safe`,schema:eX,init:{method:`GET`,headers:{authorization:this.authorization(e)}}})}catch(e){throw e instanceof bt&&e.code===`authentication`?new bt({provider:`dataforseo`,operation:`account-status`,code:`authentication`,status:e.status??void 0,message:"DataForSEO rejected the API login or API password. Run `seo providers dataforseo connect` again, or check SEO_DATAFORSEO_LOGIN and SEO_DATAFORSEO_PASSWORD.",cause:e}):e}let n=t.tasks.find(e=>e.status_code!==2e4);if(t.status_code!==2e4||t.tasks_error>0||n){let e=n?.status_code??t.status_code;throw new bt({provider:`dataforseo`,operation:`account-status`,code:nX(e),message:`DataForSEO could not return account status (${e}).`,retryable:e===40202})}let r=t.tasks[0]?.result?.[0];if(!r)throw new bt({provider:`dataforseo`,operation:`account-status`,code:`invalid-response`,message:`DataForSEO returned account status without an account row.`});return{provider:`dataforseo`,login:r.login,timezone:r.timezone??null,balanceMicros:tX(r.money?.balance),depositedMicros:tX(r.money?.total),apiVersion:t.version??null,requestCostMicros:tX(t.cost)??0,taskIds:t.tasks.flatMap(e=>e.id?[e.id]:[]),observedAt:this.now().toISOString()}}async userData(){return this.userDataForCredentials(await this.getCredentials(`account-status`))}};new class{getGoogleOAuthClient(){let e=Fa();if(e)return{clientId:e.clientId,clientSecret:e.clientSecret,source:`byo`};let t=pa(),n=process.env.SEO_GOOGLE_CLIENT_ID??process.env.GSC_CLIENT_ID??za.clientId??t.auth.sharedClientId,r=process.env.SEO_GOOGLE_CLIENT_SECRET??process.env.GSC_CLIENT_SECRET??za.clientSecret??t.auth.sharedClientSecret;if(n&&r)return{clientId:n,clientSecret:r,source:`shared`}}readGoogleTokens(){return Oa()}writeGoogleTokens(e){return ja(e)}deleteGoogleTokens(){return Ma()}};const iX={missing_title:X(`fail`,`Give the page a title.`),title_duplicate:X(`fail`,`Give every page its own title.`),title_too_wide:X(`warn`,`Put the important words first in the title.`),missing_meta_description:X(`fail`,`Give the page a meta description.`),meta_description_duplicate:X(`fail`,`Give every page its own meta description.`),canonical_missing:X(`fail`,`Add a canonical link to the page URL.`),canonical_invalid:X(`fail`,`Fix the canonical URL.`),canonical_conflict:X(`fail`,`Keep one canonical link per page.`),canonical_multiple:X(`fail`,`Keep one canonical link per page.`),canonical_outside_head:X(`fail`,`Move the canonical link into <head>.`),canonical_non_absolute:X(`fail`,`Use an absolute canonical URL.`),canonical_mismatch:X(`fail`,`Point the canonical at the URL the page is served from.`),canonicalized_page:X(`fail`,`Build only the canonical version of a page.`),noindex:X(`fail`,`Remove noindex. Improve a thin page, merge it with a 301, or do not build it.`),x_robots_noindex:X(`fail`,`Remove the noindex header.`),nofollow:X(`fail`,`Remove nofollow from the robots meta tag.`),client_rendered_content:X(`fail`,`Put the page content in the built HTML, not only in JavaScript.`),near_empty_content:X(`warn`,`Add real content from the data, or do not build the page.`),duplicate_content:X(`fail`,`Merge pages with the same content, or make each one useful on its own.`),orphan_page:X(`fail`,`Link to the page from a hub or a related page.`),deep_page:X(`warn`,`Link to the page from a page nearer the home page.`),large_html:X(`warn`,`Make the page smaller.`),image_missing_alt:X(`fail`,`Give every image alt text.`),viewport_missing:X(`fail`,`Add the viewport meta tag.`),lang_missing:X(`fail`,`Set the lang attribute on <html>.`),hreflang_invalid:X(`fail`,`Fix the hreflang links.`),hreflang_duplicate:X(`fail`,`Keep one hreflang link per language.`),hreflang_incomplete:X(`fail`,`Make hreflang links point both ways.`),structured_data_missing:X(`fail`,`Add the page JSON-LD graph.`),jsonld_invalid:X(`fail`,`Fix the JSON-LD so it parses.`),rich_result_required_fields_missing:X(`fail`,`Add the required fields to the structured data.`),og_title_missing:X(`fail`,`Add og:title.`),og_description_missing:X(`fail`,`Add og:description.`),og_image_missing:X(`fail`,`Add og:image.`),twitter_card_missing:X(`fail`,`Add the twitter:card tag.`)},aX={one_h1:X(`fail`,`Give the page exactly one H1.`),title_length:X(`fail`,`Keep the title between 10 and 65 characters.`),description_length:X(`fail`,`Keep the meta description between 50 and 170 characters.`),heading_order:X(`warn`,`Do not skip heading levels.`),broken_link:X(`fail`,`Point the link at a page or file in the build.`),not_found_page_missing:X(`fail`,`Add a 404 page (src/pages/404.astro).`),sitemap_missing:X(`fail`,`Build sitemap.xml.`),sitemap_page_missing:X(`fail`,`List every indexable page in a sitemap.`),sitemap_extra_url:X(`fail`,`List only built, indexable pages in the sitemaps.`),sitemap_file_too_large:X(`fail`,`Split sitemap files at 5,000 URLs under one sitemap index.`),robots_missing:X(`fail`,`Add robots.txt.`),robots_blocks_site:X(`fail`,`Do not disallow the whole site.`),robots_sitemap_missing:X(`fail`,`List the sitemap in robots.txt.`),robots_content_signal_missing:X(`warn`,`Add a Content-Signal line to robots.txt.`),og_image_file_missing:X(`fail`,`Draw the sharing image for the page at build time.`),og_image_size:X(`warn`,`Draw sharing images at 1200 by 630.`),markdown_copy_missing:X(`fail`,`Build a Markdown copy of the page and link it as an alternate.`),llms_txt_missing:X(`fail`,`Build llms.txt.`),llms_txt_broken_link:X(`fail`,`Point llms.txt links at built files.`),faq_schema_mismatch:X(`fail`,`Mark up only the questions and answers the page shows, word for word.`),copy_dash:oX(`Use a full stop, comma or brackets instead of a dash.`),copy_semicolon:oX(`Split the sentence instead of using a semicolon.`),copy_contrast:oX(`Say what the thing is. Drop the "not X but Y" line.`),copy_puffery:oX(`Use a plain word, or cut the claim.`),copy_throat_clearing:oX(`Start with the answer.`),copy_process_words:oX(`Keep the build, the tools and search terms out of page copy.`),copy_coming_soon:oX(`Publish it when it exists, or leave it out.`),design_uppercase:sX(`Use sentence case.`),design_letter_spacing:sX(`Remove the letter spacing.`),design_gradient:sX(`Use a flat colour.`),design_shadow:sX(`Separate things with spacing or a hairline.`),design_left_border:sX(`Remove the coloured left border.`),design_wide_grid:sX(`Use one or two columns, or a table.`),design_raw_colour:sX(`Use a theme class such as bg-muted, text-foreground or text-primary. Colours live in src/styles/globals.css.`),design_style_block:sX(`Use Tailwind classes, or add the style to src/styles/globals.css.`),data_source_missing:cX(`Link each fact block to its source.`),data_checked_missing:cX(`Show the date each fact block was last checked.`),data_checked_invalid:cX(`Use a real, past date for the checked date.`)};function X(e,t){return{group:`pages`,level:e,fix:t}}function oX(e){return{group:`copy`,level:`fail`,fix:e}}function sX(e){return{group:`design`,level:`fail`,fix:e}}function cX(e){return{group:`data`,level:`fail`,fix:e}}function lX(e){let t=aX[e]??iX[e];if(!t)throw Error(`Missing site check rule ${e}`);return t}const uX=e=>RegExp(`\\b(?:${e.join(`|`)})\\b`,`gi`),dX=[[`copy_dash`,/[\u2013\u2014]/g],[`copy_semicolon`,/;/g],[`copy_contrast`,/\b(?:it(?:'|\u2019)?s|it is|this is|that(?:'|\u2019)?s) not (?:just |only |about )?[^.,!?]{1,40}, it(?:'|\u2019)?s\b|\bnot (?:just|only) [^.,!?]{1,40} but\b|\bless about [^.,!?]{1,40} (?:and )?more about\b/gi],[`copy_puffery`,uX([`revolutionary`,`revolutioni[sz]e`,`seamless(?:ly)?`,`robust`,`ultimate`,`game[- ]chang(?:er|ing)`,`cutting[- ]edge`,`state[- ]of[- ]the[- ]art`,`world[- ]class`,`best[- ]in[- ]class`,`unparalleled`,`unlock(?:s|ing)?`,`unleash(?:es|ing)?`,`leverag(?:e|es|ing)`,`supercharg(?:e|es|ing)`,`effortless(?:ly)?`,`holistic`,`delve`,`next[- ]level`,`one[- ]stop shop`,`look no further`])],[`copy_throat_clearing`,uX([`in this (?:guide|article|post)`,`let(?:'|’)?s dive in`,`dive into`,`did you know`,`have you ever wondered`,`it is (?:important|worth) (?:to note|noting)`,`it(?:'|’)?s worth noting`,`the truth is`,`the honest answer`,`in today(?:'|’)?s`,`at the time of writing`,`as of writing`,`without the hype`])],[`copy_process_words`,uX([`astro`,`cloudflare`,`wrangler`,`scrap(?:e|ed|er|ers|ing)`,`llms?`,`ai[- ]generated`,`placeholder`,`lorem ipsum`,`todo`,`tbd`,`deploy(?:s|ed|ing|ment)?`,`pull request`,`search volume`,`keywords?`,`programmatic seo`])],[`copy_coming_soon`,uX([`coming soon`,`under construction`,`work in progress`,`stay tuned`,`not yet available`])]];function fX(e,t){let n=e.normalize(`NFKC`);for(let e of t)e&&(n=n.split(e).join(` `));let r=[];for(let[e,t]of dX){let i=[...new Set([...n.matchAll(t)].map(e=>e[0]))].slice(0,5);i.length&&r.push({rule:e,found:i})}return r}const pX=[[`design_uppercase`,/text-transform\s*:\s*uppercase|font-variant(?:-caps)?\s*:\s*(?:all-)?small-caps/gi],[`design_letter_spacing`,/letter-spacing\s*:\s*\+?(?:0?\.\d*[1-9]|[1-9][\d.]*)/gi],[`design_gradient`,/(?:repeating-)?(?:linear|radial|conic)-gradient\(/gi],[`design_shadow`,/(?:box|text)-shadow\s*:\s*(?!none\b|0\s*0\s*(?:0\s*)?(?:#0000|transparent)|initial\b|unset\b|var\(--tw-inset-shadow\))[^;}]+|--tw-shadow\s*:\s*(?!0\s+0\s+#0000)[^;}]+|drop-shadow\(/gi],[`design_left_border`,/border-(?:left|inline-start)(?:-width)?\s*:\s*[^;}]*?(?:\b(?:[2-9]|\d{2,})(?:\.\d+)?px|\b(?:0?\.[2-9]|[1-9])[\d.]*(?:r?em)|\b(?:medium|thick)\b)/gi],[`design_wide_grid`,/grid-template-columns\s*:\s*(?:repeat\(\s*(?:[3-9]|\d{2,}|auto-fill|auto-fit)\b|(?:[^;}]*?\b\d*\.?\d+fr\b){3})/gi]];function mX(e){let t=e.replace(/\/\*[\s\S]*?\*\//g,``),n=[];for(let[e,r]of pX){let i=[...new Set([...t.matchAll(r)].map(e=>e[0].trim()))].slice(0,3);i.length&&n.push({rule:e,found:i})}return n}const hX=[[`design_raw_colour`,new RegExp([String.raw`(?<=[\s:(\[,'"])#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})(?![\w-])`,String.raw`\b(?:rgba?|hsla?|hwb|oklch|oklab|lab|lch|color)\(`,String.raw`(?<![\w-])(?:[a-z-]+:)*(?:bg|text|border(?:-[trblxy])?|ring|ring-offset|outline|fill|stroke|from|via|to|decoration|accent|caret|divide|shadow|placeholder)-(?:(?:${`slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose`})-\d{2,3}|black|white)(?:\/\d+)?(?![\w-])`].join(`|`),`gi`)],[`design_style_block`,/<style\b[^>]*>/gi]];function gX(e){let t=e.replace(/<!--[\s\S]*?-->/g,``).replace(/\/\*[\s\S]*?\*\//g,``).replace(/(^|[^:])\/\/[^\n]*/g,`$1`).replace(/\bhref=(["'])[^"']*\1/gi,``),n=[];for(let[e,r]of hX){let i=[...new Set([...t.matchAll(r)].map(e=>e[0].trim()))].slice(0,3);i.length&&n.push({rule:e,found:i})}return n}const _X=new Set(`address article aside blockquote caption dd details dialog div dl dt fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr li main nav ol option p section summary table tbody td tfoot th thead tr ul`.split(` `)),vX=new Set(`script style template noscript code pre kbd samp svg math`.split(` `));function yX(e){let t=[],n=``,r=()=>{let e=n.replace(/\s+/g,` `).trim();e&&t.push(e),n=``},i=e=>{if(e.type===`text`){n+=e.data??``;return}if(!e.name||vX.has(e.name))return;if(e.name===`br`){n+=` `;return}let t=_X.has(e.name);t&&r();for(let t of e.children??[])i(t);t&&r()};for(let t of e(`body`).toArray())i(t);return r(),t}function bX(e){let t=[e(`title`).first().text()];return e(`meta`).each((n,r)=>{let i=(e(r).attr(`name`)??e(r).attr(`property`)??``).toLowerCase();/^(?:description|og:title|og:description|og:image:alt|twitter:title|twitter:description)$/.test(i)&&t.push(e(r).attr(`content`)??``)}),e(`[alt], [aria-label], [title], [placeholder]`).each((n,r)=>{if(!e(r).closest(`head, script, style, template, svg`).length)for(let n of[`alt`,`aria-label`,`title`,`placeholder`]){let i=e(r).attr(n);i&&t.push(i)}}),t.map(e=>e.replace(/\s+/g,` `).trim()).filter(Boolean)}const xX=1e4,SX=1e3,CX=/^src\/(?:pages|components|layouts)\/.+\.(?:astro|tsx?|jsx?|mdx?|html)$/,wX={source:`network`,cache:`bypass`,fetched:!0,rendered:!1,blocked:!1,durationMs:0,retries:0,rateLimit:{host:`build`,concurrency:1,intervalCap:1,intervalMs:0}};function TX(e){return`/${e}`.replace(/(?:\/index)?\.html$/,``)||`/`}const EX=(e,t)=>e<t?-1:+(e>t),DX=e=>e.replace(/\s+/g,` `).trim();async function OX(e){let t=new URL(e.origin).origin,n=[...e.files].sort(EX),r=new Set(n),i=e.allowWords??[],a=e.now??/* @__PURE__ */ new Date,o=[],s=(e,t,n)=>{let{group:r,level:i,fix:a}=lX(e);o.push({rule:e,group:r,level:i,file:t,found:n,fix:a})},c=n.filter(e=>e.endsWith(`.html`)),l=c.slice(0,xX),u=new Set(l.map(TX)),d=[],f=/* @__PURE__ */ new Set,p=(e,t)=>{if(e.trim()&&!f.has(e)){f.add(e);for(let{rule:n,found:r}of mX(e))s(n,t,r.join(`, `))}};r.has(`404.html`)||s(`not_found_page_missing`,`404.html`,``);for(let n of l){let o=await e.readText(n),c=TX(n),l=new URL(c,t).toString(),f=n===`404.html`,m=(await qO(l,{url:l,finalUrl:l,status:f?404:200,headers:{"content-type":`text/html; charset=utf-8`},html:o,usedJs:!1,diagnostics:wX,warnings:[]})).page;if(!m)continue;let h=R_(o),g=kX(h,l,t);d.push({file:n,path:c,snapshot:m,links:g}),h(`style`).each((e,t)=>p(h(t).text(),n)),p(h(`[style]`).map((e,t)=>h(t).attr(`style`)).get().join(`;`),n);for(let e of g)AX(e,u,r)||s(`broken_link`,n,e);f||(jX(h,n,s),MX(h,n,i,s),NX(h,n,a,s),await PX(h,m,n,t,e,r,s))}for(let t of n.filter(e=>e.endsWith(`.css`)))p(await e.readText(t),t);if(e.sources){let t=e.sources.files.filter(e=>CX.test(e)).sort(EX).slice(0,xX);for(let n of t){let t=await e.sources.readText(n);for(let{rule:e,found:r}of gX(t))s(e,n,r.join(`, `))}}RX(d);let m=new Map(d.map(e=>[e.snapshot.url,e.file]));for(let e of ST(d.map(e=>e.snapshot),{startUrl:`${t}/`}))e.ruleId in iX&&s(e.ruleId,m.get(e.url)??e.url,e.detail??``);return await zX(e,r,t,d.filter(e=>e.snapshot.status===200&&e.snapshot.indexable),s),await VX(e,r,t,u,s),HX({origin:t,issues:o,pagesFound:c.length,pagesChecked:l.length})}function kX(e,t,n){let r=/* @__PURE__ */ new Set;return e(`a[href], link[rel="alternate"][href]`).each((i,a)=>{let o=e(a).attr(`href`)??``;try{let e=new URL(o,t);if(e.origin!==n)return;r.add(decodeURI(e.pathname))}catch{r.add(o)}}),[...r].sort(EX)}function AX(e,t,n){return t.has(e)?!0:n.has(e.replace(/^\//,``))}function jX(e,t,n){let r=e(`h1`).length;r!==1&&n(`one_h1`,t,`${r} H1 elements`);let i=DX(e(`title`).first().text());i&&(i.length<10||i.length>65)&&n(`title_length`,t,`${i.length} characters: ${i}`);let a=DX(e(`meta[name="description"]`).first().attr(`content`)??``);a&&(a.length<50||a.length>170)&&n(`description_length`,t,`${a.length} characters`);let o=0;for(let r of e(`main h1, main h2, main h3, main h4, main h5, main h6`).toArray()){let e=Number(r.tagName.slice(1));if(o&&e>o+1){n(`heading_order`,t,`H${o} to H${e}`);break}o=e}e(`h1, h2, h3, h4, h5, h6, nav a, button, label, th`).each((r,i)=>{let a=DX(e(i).text()),o=a.replace(/[^a-z]/gi,``);o.length>=8&&o===o.toUpperCase()&&n(`design_uppercase`,t,a.slice(0,80))})}function MX(e,t,n,r){let i=/* @__PURE__ */ new Map;for(let t of[...bX(e),...yX(e)])for(let e of fX(t,n)){let t=i.get(e.rule)??/* @__PURE__ */ new Set;for(let n of e.found)t.add(n);i.set(e.rule,t)}for(let[e,n]of i)r(e,t,[...n].slice(0,5).join(`, `))}function NX(e,t,n,r){e(`[data-facts]`).each((i,a)=>{let o=e(a),s=o.attr(`data-facts`)||`fact block ${i+1}`;o.find(`a[data-source][href]`).first().attr(`href`)||r(`data_source_missing`,t,s);let c=o.find(`time[data-checked]`).first();if(!c.length){r(`data_checked_missing`,t,s);return}let l=c.attr(`datetime`)??``,u=new Date(l);(!/^\d{4}-\d{2}-\d{2}/.test(l)||Number.isNaN(u.getTime())||u.getTime()>n.getTime())&&r(`data_checked_invalid`,t,`${s}: ${l||`no date`}`)})}async function PX(e,t,n,r,i,a,o){let s=t.openGraphImage;if(s){let e=FX(s,r);if(e!==void 0&&!a.has(e))o(`og_image_file_missing`,n,s);else if(e?.endsWith(`.png`)){let t=IX(await i.readBytes(e));t&&(t.width!==1200||t.height!==630)&&o(`og_image_size`,n,`${t.width} by ${t.height}`)}}if(!t.indexable)return;let c=e(`link[rel~="alternate"][type="text/markdown"]`).first().attr(`href`),l=c?FX(c,r,n):``;l&&a.has(l)||o(`markdown_copy_missing`,n,c??`no alternate link`),LX(e,n,o)}function FX(e,t,n=``){try{let r=new URL(e,new URL(`/${n}`,t));return r.origin===t?decodeURI(r.pathname).replace(/^\//,``):void 0}catch{return``}}function IX(e){if(e.length<24||e[0]!==137||e[1]!==80)return null;let t=new DataView(e.buffer,e.byteOffset,e.byteLength);return{width:t.getUint32(16),height:t.getUint32(20)}}function LX(e,t,n){let r=DX(yX(e).join(` `)),i=[];e(`script[type="application/ld+json"]`).each((t,n)=>{try{let t=JSON.parse(e(n).text()),r=Array.isArray(t[`@graph`])?t[`@graph`]:[t];for(let e of r){let t=e[`@type`];if(!(Array.isArray(t)?t:[t]).includes(`FAQPage`))continue;let n=e.mainEntity;Array.isArray(n)&&i.push(...n)}}catch{}});for(let e of i){let i=DX(String(e.name??``)),a=DX(R_(`<p>${String(e.acceptedAnswer?.text??``)}</p>`)(`p`).text());!i||!r.includes(i)?n(`faq_schema_mismatch`,t,`Question not on the page: ${i}`):a&&r.includes(a)||n(`faq_schema_mismatch`,t,`Answer not on the page: ${i}`)}}function RX(e){let t=new Map(e.map(e=>[e.path,e])),n=/* @__PURE__ */ new Map;for(let r of e)if(r.snapshot.status===200)for(let e of r.links)e!==r.path&&t.has(e)&&n.set(e,(n.get(e)??0)+1);let r=/* @__PURE__ */ new Map([[`/`,0]]),i=[`/`];for(let e=0;e<i.length;e+=1){let n=i[e];for(let e of t.get(n)?.links??[])!r.has(e)&&t.has(e)&&(r.set(e,(r.get(n)??0)+1),i.push(e))}for(let t of e)t.snapshot.internalInlinkCount=n.get(t.path)??0,t.snapshot.crawlDepth=r.get(t.path)}async function zX(e,t,n,r,i){let a=[`sitemap.xml`];if(!t.has(`robots.txt`))i(`robots_missing`,`robots.txt`,``);else{let t=await e.readText(`robots.txt`);BX(t)&&i(`robots_blocks_site`,`robots.txt`,``),/^content-signal\s*:/im.test(t)||i(`robots_content_signal_missing`,`robots.txt`,``);let r=[...t.matchAll(/^sitemap\s*:\s*(\S+)/gim)].map(e=>e[1]);r.length||i(`robots_sitemap_missing`,`robots.txt`,``);let o=r.map(e=>FX(e,n)).filter(e=>!!e);o.length&&(a=o)}let o=/* @__PURE__ */ new Set,s=a.filter(e=>t.has(e)?!0:(i(`sitemap_missing`,e,``),!1)),c=/* @__PURE__ */ new Set;for(let r=0;r<s.length&&r<1e3;r+=1){let a=s[r];if(c.has(a))continue;c.add(a);let l=await e.readText(a),u=[...l.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/g)].map(e=>e[1].replace(/&amp;/g,`&`));if(/<sitemapindex\b/.test(l))for(let e of u){let r=FX(e,n);r&&t.has(r)?s.push(r):i(`sitemap_missing`,a,e)}else{u.length>5e3&&i(`sitemap_file_too_large`,a,`${u.length} URLs`);for(let e of u)o.add(e)}}let l=new Set(r.map(e=>e.snapshot.url));for(let e of r)o.has(e.snapshot.url)||i(`sitemap_page_missing`,e.file,e.snapshot.url);for(let e of[...o].sort(EX))l.has(e)||i(`sitemap_extra_url`,`sitemap.xml`,e)}function BX(e){let t=!1;for(let n of e.split(/\r?\n/)){let e=n.replace(/#.*/,``).trim(),r=/^user-agent\s*:\s*(.+)$/i.exec(e);if(r)t=r[1]?.trim()===`*`;else if(t&&/^disallow\s*:\s*\/\s*$/i.test(e))return!0}return!1}async function VX(e,t,n,r,i){if(!t.has(`llms.txt`)){i(`llms_txt_missing`,`llms.txt`,``);return}let a=await e.readText(`llms.txt`);for(let e of a.matchAll(/\]\(([^)\s]+)\)/g)){let a=FX(e[1],n);a!==void 0&&(AX(`/${a}`,r,t)||i(`llms_txt_broken_link`,`llms.txt`,e[1]))}}function HX(e){let t=[...e.issues].sort((e,t)=>(e.level===t.level?0:e.level===`fail`?-1:1)||EX(e.group,t.group)||EX(e.rule,t.rule)||EX(e.file,t.file)||EX(e.found,t.found)),n=/* @__PURE__ */ new Map;for(let e of t)n.set(e.rule,(n.get(e.rule)??0)+1);let r=[...n].map(([e,t])=>({rule:e,count:t,...lX(e)})).sort((e,t)=>(e.level===t.level?0:e.level===`fail`?-1:1)||t.count-e.count||EX(e.rule,t.rule)),i=t.filter(e=>e.level===`fail`).length,a=e.pagesChecked<e.pagesFound;return{status:i?`failed`:a?`partial`:`passed`,origin:e.origin,pagesFound:e.pagesFound,pagesChecked:e.pagesChecked,failures:i,warnings:t.length-i,rules:r,issues:t.slice(0,SX),issuesOmitted:Math.max(0,t.length-SX)}}async function UX(){let e=(await Promise.allSettled([Ma(),Ek()])).flatMap(e=>e.status===`rejected`?[e.reason]:[]);if(e.length>0)throw Error("Reset stopped before deleting local files because saved credentials could not be removed. Unlock the system keychain and run `seo reset --yes` again.",{cause:AggregateError(e)});let t=br();D(t.configDir,{recursive:!0,force:!0}),D(t.cacheDir,{recursive:!0,force:!0}),D(t.logDir,{recursive:!0,force:!0})}const WX=65536;let GX=!1;function KX(e,t){if(e.npm_command===`exec`||/npx/i.test(e.npm_execpath??``))return!0;if(!t)return!1;let n=t;try{n=T(t)}catch{}return/[\\/](?:_npx|dlx)[\\/]/.test(n)}function qX(e,t){let n=e=>e.replace(/^v/,``).split(`-`)[0]?.split(`.`).map(e=>Number.parseInt(e,10))??[],r=n(e),i=n(t);for(let e=0;e<3;e+=1){let t=r[e]??0,n=i[e]??0;if(!Number.isFinite(t)||!Number.isFinite(n))return!1;if(n>t)return!0;if(n<t)return!1}return!1}function JX(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.checkedAt==`number`&&typeof t.latest==`string`&&/^\d+\.\d+\.\d+$/.test(t.latest)}function YX(e,t){if(GX)return;GX=!0;let n=()=>{GX=!1},r=ke(`https://registry.npmjs.org/seo/latest`,{headers:{accept:`application/json`}}),i=setTimeout(()=>r.destroy(),2e3);i.unref(),r.on(`socket`,e=>e.unref()),r.on(`error`,()=>void 0),r.on(`close`,()=>{clearTimeout(i),n()}),r.on(`response`,n=>{let i=[],a=0;n.on(`error`,()=>void 0),n.on(`data`,e=>{a+=e.length,a>WX?r.destroy():i.push(e)}),n.on(`end`,()=>{n.statusCode!==200||a>WX||Nt(new Response(Buffer.concat(i)),`The npm registry`).then(n=>{let r={checkedAt:t,latest:n.version};JX(r)&&Tr(e,r,384)}).catch(()=>void 0)})}),r.end()}function XX(e={}){try{let t=e.env??process.env,n=e.argv??process.argv;if(t.NO_UPDATE_NOTIFIER||n.includes(`--no-update-notifier`)||ei(t))return;let r=e.cacheFile??g(br().cacheDir,`latest-version.json`),i=(e.now??/* @__PURE__ */ new Date).getTime(),a=wr(r),o=JX(a)?a:void 0;(!o||i-o.checkedAt>=864e5)&&YX(r,i);let s=e.version??Kr;return!o||!qX(s,o.latest)?void 0:KX(t,e.cliPath??n[1])?`seo ${o.latest} is out (you have ${s}). Run npx -y seo@latest; in an MCP config use seo@latest, then restart the client.`:`seo ${o.latest} is out (you have ${s}). Update with: npm i -g seo`}catch{return}}function ZX(e){return!!e._zod}function QX(e){let t=Object.values(e);if(t.length===0)return je.object({});let n=t.every(ZX),r=t.every(e=>!ZX(e));if(n)return je.object(e);if(r)return Ae.object(e);throw Error(`Mixed Zod versions detected in object shape.`)}function $X(e,t){return ZX(e)?je.safeParse(e,t):e.safeParse(t)}async function eZ(e,t){return ZX(e)?await je.safeParseAsync(e,t):await e.safeParseAsync(t)}function tZ(e){if(!e)return;let t;if(t=ZX(e)?e._zod?.def?.shape:e.shape,t){if(typeof t==`function`)try{return t()}catch{return}return t}}function nZ(e){if(e){if(typeof e==`object`){let t=e,n=e;if(!t._def&&!n._zod){let t=Object.values(e);if(t.length>0&&t.every(e=>typeof e==`object`&&!!e&&(e._def!==void 0||e._zod!==void 0||typeof e.parse==`function`)))return QX(e)}}if(ZX(e)){let t=e._zod?.def;if(t&&(t.type===`object`||t.shape!==void 0))return e}else if(e.shape!==void 0)return e}}function rZ(e){return e.length===0?`object root`:e.reduce((e,t,n)=>n===0?String(t):typeof t==`number`?`${e}[${t}]`:`${e}.${t}`,``)}function iZ(e){if(e&&typeof e==`object`){if(`issues`in e&&Array.isArray(e.issues)&&e.issues.length>0)return e.issues.map(e=>e.path?.length?`${e.message} at ${rZ(e.path)}`:e.message).join(`
@@ -446,4 +446,4 @@ if(!Array.isArray(r))throw Error(`ajv implementation error`);let a=n.name(`valid
446
446
  4. seo_lint checks copy you are writing or editing (a landing page, blog post, title or meta description) for patterns common in AI-written text and for title and description length. It reads only the text or files you give it.
447
447
  seo_crawl and seo_internal_links run on this machine. Keys and Google sign-in come from \`seo start\` and \`seo providers <name> connect\`.`}),n=()=>({clientName:t.server.getClientVersion()?.name});v3(t,{telemetry:e.telemetry===!1?void 0:n}),m4(t);let r=y3();if(r){let{name:e,uri:n,...i}=SJ;t.registerResource(e,n,i,async e=>({contents:[{uri:e.href,mimeType:i.mimeType,text:r}]}))}return e.telemetry!==!1&&(t.server.oninitialized=()=>{yi(n())}),t}async function x3(e={}){let t=b3();if(e.test){process.stdout.write(`seo MCP server constructed successfully.
448
448
  `);return}let n=new s4;await t.connect(n),XX()}export{_q as $,br as $n,cE as $t,tY as A,Oi as An,MF as At,DJ as B,gi as Bn,uj as Bt,bY as C,La as Cn,Bz as Ct,sY as D,Aa as Dn,JL as Dt,dY as E,Oa as En,$L as Et,JJ as F,zi as Fn,mP as Ft,Wq as G,Si as Gn,uA as Gt,jJ as H,ri as Hn,AA as Ht,NJ as I,Zi as In,ZN as It,Nq as J,Ti as Jn,jk as Jt,Fq as K,Ci as Kn,Dk as Kt,RJ as L,Ji as Ln,pM as Lt,BJ as M,Mi as Mn,oF as Mt,ZJ as N,Xi as Nn,iF as Nt,cY as O,Pa as On,GL as Ot,XJ as P,Yi as Pn,LP as Pt,eq as Q,xr as Qn,lE as Qt,LJ as R,qi as Rn,Y as Rt,EY as S,Ma as Sn,aB as St,aY as T,ka as Tn,AR as Tt,AJ as U,_i as Un,dA as Ut,MJ as V,yi as Vn,FA as Vt,cJ as W,wi as Wn,lA as Wt,yq as X,Hr as Xn,Lk as Xt,vq as Y,Kr as Yn,Ik as Yt,Cq as Z,Ur as Zn,aE as Zt,jY as _,fs as _n,ZB as _t,p3 as a,UT as an,F as ar,jG as at,DY as b,So as bn,KB as bt,qX as c,Bw as cn,Xe as cr,cG as ct,OX as d,pS as dn,aW as dt,uE as en,An as er,gq as et,rX as f,uS as fn,qU as ft,qY as g,ms as gn,qB as gt,KY as h,_s as hn,JH as ht,f3 as i,tE as in,Zt as ir,QG as it,zJ as j,Li as jn,NF as jt,oY as k,Ia as kn,dL as kt,XX as l,IS as ln,Ye as lr,oW as lt,GY as m,vs as mn,YH as mt,d4 as n,sE as nn,Tn as nr,$G as nt,d3 as o,zT as on,Tt as or,fG as ot,XY as p,Ks as pn,YU as pt,Vq as q,xi as qn,Rk as qt,b3 as r,nE as rn,qt as rr,eK as rt,x3 as s,Xw as sn,Ct as sr,wG as st,K4 as t,oE as tn,Bn as tr,WK as tt,UX as u,bS as un,sW as ut,AY as v,Fo as vn,JB as vt,mY as w,Na as wn,ZR as wt,OY as x,bo as xn,kB as xt,kY as y,jo as yn,YB as yt,IJ as z,li as zn,Wj as zt};
449
- //# sourceMappingURL=dist-pwGymked.js.map
449
+ //# sourceMappingURL=dist-DoPZlCJq.js.map