@seoagent-official/seoagent 1.101.1 → 1.101.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +1 -1
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -83,7 +83,7 @@ description: Chronological history of changes to this OKF bundle.
83
83
  # Change log
84
84
 
85
85
  - Bundle scaffolded.
86
- `,"utf-8"),r.push("log.md")),{created:r,okfDir:n}}import{log as At}from"@clack/prompts";var u={info:e=>At.info(e),warn:e=>At.warn(e),error:e=>At.error(e),success:e=>At.success(e),step:e=>At.step(e),message:e=>At.message(e)};function S(e,t,n,r=n){e?e.stop(r):u[t](n)}var Aa=[{suffixes:[".wordpress.com"],platform:{id:"wordpress.com",label:"WordPress.com",api:"wordpress",enableHint:"an application password (Users \u2192 Profile \u2192 Application Passwords) or a WordPress.com OAuth token"}},{suffixes:[".wixsite.com",".wix.com"],platform:{id:"wix",label:"Wix",api:"wix",enableHint:"an API key from the Wix API Keys Manager with Blog permissions, plus the site ID"}},{suffixes:[".myshopify.com"],platform:{id:"shopify",label:"Shopify",api:"shopify",enableHint:"a Shopify Admin API access token with blog write scope"}},{suffixes:[".webflow.io"],platform:{id:"webflow",label:"Webflow",api:"webflow",enableHint:"a Webflow site API token with CMS write access"}},{suffixes:[".ghost.io"],platform:{id:"ghost",label:"Ghost",api:"ghost",enableHint:"a Ghost Admin API key"}},{suffixes:[".blogspot.com"],platform:{id:"blogger",label:"Blogger",api:"blogger",enableHint:"Blogger API access"}},{suffixes:[".squarespace.com"],platform:{id:"squarespace",label:"Squarespace",api:null,enableHint:""}},{suffixes:[".weebly.com"],platform:{id:"weebly",label:"Weebly",api:null,enableHint:""}},{suffixes:[".godaddysites.com"],platform:{id:"godaddy",label:"GoDaddy Website Builder",api:null,enableHint:""}},{suffixes:[".carrd.co"],platform:{id:"carrd",label:"Carrd",api:null,enableHint:""}},{suffixes:[".notion.site"],platform:{id:"notion",label:"Notion",api:null,enableHint:""}},{suffixes:[".substack.com"],platform:{id:"substack",label:"Substack",api:null,enableHint:""}},{suffixes:[".medium.com"],platform:{id:"medium",label:"Medium",api:null,enableHint:""}},{suffixes:[".tumblr.com"],platform:{id:"tumblr",label:"Tumblr",api:null,enableHint:""}},{suffixes:[".framer.website",".framer.app"],platform:{id:"framer",label:"Framer",api:null,enableHint:""}}];function Pa(e){if(!e)return null;let t=e.trim().toLowerCase().replace(/^sc-domain:/,"").replace(/^https?:\/\//,"").replace(/^www\./,"").replace(/[/?#].*$/,"").replace(/:\d+$/,"");if(!t)return null;for(let{suffixes:n,platform:r}of Aa)if(n.some(o=>t.endsWith(o)&&t.length>o.length))return r;return null}var $x=Aa.flatMap(({suffixes:e,platform:t})=>e.map(n=>({suffix:n,id:t.id,api:t.api})));function Ia(e,t){let n=[` \u{1F3E0} ${e} is hosted on ${t.label} \u2014 there is no repository here and`," none to find; this folder is the workspace. Audit, keywords, strategy"," and writing all work as normal. Delivery differs: read",' references/publishing.md \xA7 "No repo" (next to the skill) BEFORE the'," audit."];return t.api?n.push(` Ask your human once to set up the ${t.label} connector:`,` ${t.enableHint}. Credentials go in .env in this folder, never`," in a tracked file. Posts are created as drafts unless told otherwise."):n.push(` ${t.label} has no public API for posts, so the connector is the`," editor: write each article to .seoagent/content/<slug>.md with the exact"," title, slug, meta description and body, walk your human through pasting"," it, then `seoagent content track --slug <slug> --url <live-url>`."),n}import{randomBytes as If}from"crypto";import{isCancel as Of,spinner as Fa,text as Tf}from"@clack/prompts";import de from"picocolors";import{exec as Pf}from"child_process";function yr(e){let t=process.platform==="darwin"?`open "${e}"`:process.platform==="win32"?`start "" "${e}"`:`xdg-open "${e}"`;Pf(t,()=>{})}function Oa(e,t){if(e>=500)return{status:"error",terminal:!1,message:`HTTP ${e} (server error)`};if(e>=400)return{status:"error",terminal:!0,message:`HTTP ${e} (client error)`};if(!t||typeof t!="object")return{status:"error",terminal:!0,message:`Malformed response: expected an object, got ${typeof t}`};let n=t;if(n.status==="ready"){if(typeof n.user_token=="string"&&typeof n.website_token=="string"){let r=typeof n.email=="string"&&n.email?n.email:void 0,o=typeof n.domain=="string"&&n.domain?n.domain:void 0;return{status:"ready",user_token:n.user_token,website_token:n.website_token,...r?{email:r}:{},...o?{domain:o}:{}}}return{status:"error",terminal:!0,message:"Malformed response: status=ready without user_token+website_token"}}return n.status==="pending"?{status:"pending"}:n.status==="expired"?{status:"expired"}:{status:"error",terminal:!0,message:`Malformed response: unknown status=${JSON.stringify(n.status)}`}}var La=/^sa_[0-9a-f]{24}$/;function Ta(e){switch(e){case"code_not_found":return"That setup code was not recognized: it was never issued. Codes cannot be guessed or retyped from memory; copy one exactly from seoagent.com/grok. A scheduled run on a fresh machine needs the SEOAGENT_AUTH secret (`seoagent whoami --env` on the bound machine), not a setup code.";case"code_already_used":return"That setup code has already been used \u2014 each code works exactly once. Generate a new one at seoagent.com/grok.";case"code_revoked":return"That setup code was replaced by a newer one. Use the latest code, or generate a new one at seoagent.com/grok.";case"code_expired":return"That setup code has expired. Generate a new one at seoagent.com/grok.";case"setup_codes_unavailable":return"Setup codes are not enabled on this server yet. Run `seoagent login` (browser flow) instead.";case"domain_required":return"This code has no site attached and the project has no domain. Run `seoagent init` first so the project knows its domain.";default:return typeof e=="string"&&e?`Login failed: ${e}`:"Login failed."}}function Na(e,t){let n=t&&typeof t=="object"?t:null;if(e>=500)return e===503&&n?.error==="setup_codes_unavailable"?{status:"error",terminal:!0,message:Ta(n.error)}:{status:"error",terminal:!1,message:`HTTP ${e} (server error)`};if(e>=400){let r=typeof n?.error=="string"&&n.error?n.error:void 0;return{status:"error",terminal:!0,message:Ta(n?.error),...r?{code:r}:{}}}if(!n)return{status:"error",terminal:!0,message:`Malformed response: expected an object, got ${typeof t}`};if(n.status==="ready"){if(typeof n.user_token=="string"&&typeof n.website_token=="string"){let r=typeof n.email=="string"&&n.email?n.email:void 0,o=typeof n.domain=="string"&&n.domain?n.domain:void 0;return{status:"ready",user_token:n.user_token,website_token:n.website_token,...r?{email:r}:{},...o?{domain:o}:{}}}return{status:"error",terminal:!0,message:"Malformed response: status=ready without user_token+website_token"}}return{status:"error",terminal:!0,message:`Malformed response: unknown status=${JSON.stringify(n.status)}`}}var Lf=1500,Nf=900*1e3,ja="Login timed out. If the browser page shows a `seoagent login --code \u2026` line, run that here; otherwise run `seoagent login` again.",jf=3,Df=2e3;function wr(e,t){if(ce())return;let n=me(e);n&&Fe({installId:n,event:t,cliVersion:R})}function Mf(){return If(16).toString("hex")}function Ff(e,t,n){let r=new URLSearchParams({session:t});return n&&n!=="unknown"&&r.set("domain",n),`${e}/cli/auth?${r.toString()}`}function Da(e){let t=e.trim().replace(/^https?:\/\//i,"").replace(/^www\./i,"").replace(/\/.*$/,"");return!t||!/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(t)?null:t.toLowerCase()}async function Uf(e,t,n){if(!e||e.website_token===t)return null;try{let r=await n(e);return r.verdict!=="unverified"?r.cloudDomain:null}catch{return null}}async function Bf(e,t){try{let n=`${e}/api/cli/auth/poll?session=${encodeURIComponent(t)}`,r=await fetch(n,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(1e4)}),o=null;try{o=await r.json()}catch{}return Oa(r.status,o)}catch(n){return{status:"error",terminal:!1,message:n.message||"network error"}}}function Ua(e){return new Promise(t=>setTimeout(t,e))}async function Ba(e,t,n,r){let o=M(),i=await Uf(o,e.website_token,c=>Ye({apiBase:c.api_base,authHeader:`Bearer ${c.user_token}:${c.website_token}`,localDomain:""}));i&&o&&yo(o,i),yo({user_token:e.user_token,website_token:e.website_token,api_base:t},e.domain||n);let s=e.email&&e.domain?`Logged in as ${e.email} (${e.domain}). Future SEO work in this repo will sync to your dashboard.`:e.email?`Logged in as ${e.email}. Future SEO work in this repo will sync to your dashboard.`:"Logged in. Future SEO work in this repo will sync to your dashboard.",a=e.email&&e.domain?`Logged in as ${de.bold(e.email)} (${de.cyan(e.domain)}).`:e.email?`Logged in as ${de.bold(e.email)}.`:"Logged in.";S(r,"success",s,de.green(a)),r&&u.info("Future SEO work in this repo will sync to your dashboard.");let l=Je().filter(c=>c!==$e(e.domain||n));l.length>0&&u.info(`Also logged in to: ${l.join(", ")}. Each project auto-selects its own site \u2014 no need to log out first.`)}function Gf(e,t,n){return{code:e,...t&&t!=="unknown"?{domain:t}:{},...n?{install_id:n.installId,cli_version:n.cliVersion,platform:process.platform}:{}}}function jo(e){if(ce())return null;let t=me(e);return t?{installId:t,cliVersion:R}:null}async function Hf(e,t,n,r){try{let o=await fetch(`${e}/api/cli/auth/redeem`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(Gf(t,n,r)),signal:AbortSignal.timeout(1e4)}),i=null;try{i=await o.json()}catch{}return Na(o.status,i)}catch(o){return{status:"error",terminal:!1,message:o.message||"network error"}}}var Wf=new Set(["code_already_used","code_revoked","code_expired"]);function Kf(e,t){return`That setup code was for ${e}, but this directory is the ${t} project \u2014 it stays ${t}, and running \`${m("sync")}\` here syncs ${t}. Credentials for ${e} are saved. One site per directory: run \`${m("init")} --domain ${e}\` in an empty directory of its own (or in the repo that builds ${e}) \u2014 it is already logged in there.`}var qf="Scheduled runs on a fresh machine: store the output of `seoagent whoami --env` as the task's SEOAGENT_AUTH secret. A setup code works once and cannot be guessed; never reuse or invent one.";function Do(e){return e!=="ready"&&e!=="already"}function Vf(e,t,n){if(!e||!n||t&&t!=="unknown")return!1;try{if(!q(e,{domain:n}))return!1}catch{return!1}return u.info(`Saved domain: ${n} \u2192 .seoagent/project.md (from the setup code).`),!0}async function Mo(e,t,n,r,o,i={}){if(!La.test(e))return u.warn("That does not look like a setup code (expected `sa_` + 24 hex characters). Generate one at seoagent.com/grok."),"invalid";let s=!!(i.siteAlreadyBound&&n),a=s?null:n,c=!!process.stdout.isTTY?Fa():null;c?.start(de.dim("Redeeming setup code\u2026"));let d="network error";for(let f=0;f<jf;f++){f>0&&await Ua(Df);let p=await Hf(t,e,a,r);if(p.status==="ready")return await Ba(p,t,n,c),s&&p.domain&&$e(p.domain)!==$e(n)?(u.warn(Kf(p.domain,n)),"other-site"):(Vf(o,n,p.domain),u.info(qf),"ready");if(p.terminal)return s&&p.code&&Wf.has(p.code)?(S(c,"info",`Already logged in for ${n} \u2014 that setup code is spent (each works once); nothing to do.`,de.dim(`Already logged in for ${n} \u2014 setup code spent, nothing to do.`)),"already"):(S(c,"warn",p.message,de.red(p.message)),"rejected");d=p.message}return S(c,"warn",`Could not reach the server (${d}). Check your connection and run \`${m("login")} --code <code>\` again.`,de.yellow("Could not reach the server.")),c&&u.warn(`Run \`${m("login")} --code <code>\` again to retry.`),"unreachable"}function br(e){return e&&e!=="unknown"?Ps(e):!!M()}function Ma(){return`Next: run \`${m("sync")}\` in this repo \u2014 it pulls your strategy, briefs and inbox into .seoagent/. Nothing syncs until it runs.`}async function on(e={}){let t=process.cwd(),n=C(t),r=(e.apiBase||N.BASE||je).replace(/\/$/,""),o=n?.domain??null;if(o==="unknown"&&process.stdin.isTTY){let g=await Tf({message:".seoagent/project.md has `domain: unknown`. What is your site's URL?",placeholder:"example.com",validate(y){if(!String(y??"").trim())return"A site URL is required.";if(!Da(String(y)))return"Please enter a valid domain like `example.com`."}});if(Of(g)){u.warn("Login cancelled.");return}let h=Da(String(g));if(h){o=h;try{q(t,{domain:h}),u.success(`Saved domain: ${h} \u2192 .seoagent/project.md`)}catch{}}}let i=(e.code??process.env.SEOAGENT_SETUP_CODE??"").trim(),s=o&&o!=="unknown";if(!i&&br(o)){let g=s?` for ${o}`:"";u.info(`Already logged in${g}. To switch accounts${g}, run \`${m("logout")}\` first.`);return}if(i){let g=await Mo(i,r,o,jo(t),t,{siteAlreadyBound:!!s&&br(o)});Do(g)?process.exitCode=1:g==="ready"&&u.info(Ma());return}let a=Mf(),l=Ff(r,a,o);u.message("Opening seoagent.com to connect this CLI to your account..."),u.info(`If the browser does not open, visit: ${l}`),yr(l),u.info('In your browser: sign in (if needed) and click "Connect this CLI" to finish.');let d=!!process.stdout.isTTY?Fa():null;d?.start(de.dim("Waiting for browser confirmation\u2026"));let f=Date.now()+Nf,p=!0;for(;Date.now()<f;){p||await Ua(Lf),p=!1;let g=await Bf(r,a);if(g.status==="ready"){await Ba(g,r,o,d),wr(t,"login_browser_ok"),u.info(Ma());return}if(g.status==="expired"){wr(t,"login_browser_expired"),S(d,"warn",`Session expired. Run \`${m("login")}\` again.`,de.red("Session expired.")),d&&u.warn(`Run \`${m("login")}\` again.`);return}if(g.status==="error"){if(g.terminal){S(d,"warn",`Login failed: ${g.message}. Run \`${m("login")}\` again.`,de.red(`Login failed: ${g.message}.`)),d&&u.warn(`Run \`${m("login")}\` again.`),wr(t,"login_browser_failed");return}continue}}S(d,"warn",ja,de.yellow("Login timed out.")),d&&u.warn(ja),wr(t,"login_browser_timeout")}function sn(e={}){let t=e.all?null:C(process.cwd()),n=!e.all&&t?.domain&&t.domain!=="unknown"?t.domain:null;if(!Is(e.all?null:n)){u.info(n?`You were not logged in for ${n}.`:"You were not logged in.");return}u.success(n?`Logged out of ${n}.`:"Logged out.");let o=Je();o.length>0&&u.info(`Still logged in to: ${o.join(", ")}. Run \`${m("logout")} --all\` to log out of everything.`)}function kr(e,t=process.env){return(e.code??t.SEOAGENT_SETUP_CODE??"").trim()}function Xf(e,t=m("init")){return e?`${t} --code ${e}`:t}async function Va(e,t,n){if(!n)return"none";let r=t!==Ge,o=(N.BASE||je).replace(/\/$/,"");return Mo(n,o,r?t:null,jo(e),e,{siteAlreadyBound:r&&br(t)})}var Ka=[{value:"saas",label:"SaaS / App"},{value:"service",label:"Service business"},{value:"product",label:"E-commerce / Product"},{value:"content",label:"Content / Blog"},{value:"marketplace",label:"Marketplace"},{value:"tool",label:"Tool / Utility"},{value:"nonprofit",label:"Nonprofit / Community"},{value:"unknown",label:"Not sure"}],Ge="unknown",Qf=new Set(["saas","service","product","content","marketplace","tool","app","nonprofit","community","unknown"]);function Zf(e){return e.field==="context"?`${e.detail} \u2014 ${e.source}`:e.field==="domain"?`Domain: ${e.detail} \u2014 ${e.source}`:`Site type: ${e.detail} \u2014 ${e.source}`}function vr(e){if(!e?.trim())return null;let t=e.trim().toLowerCase();return Qf.has(t)?t:null}function za(e,t){t.agentsMdPath&&u.info(`Added a SEOAgent section to ${Pt(e,t.agentsMdPath)} so agents that read it (Codex, others) find the skill.`),t.cursorRulePath&&u.info(`Wrote ${Pt(e,t.cursorRulePath)} so Cursor agents find the skill.`)}async function an(e={}){let t=process.cwd();if(ws(t)){let l=C(t),c=St(t);lt(t),q(t,{skill_version:R}),ct(t);let d=Pt(t,c.skillFile),f=c.claudeSymlink?` (symlinked at ${Pt(t,c.claudeSymlink)})`:"",p=l?.skill_version&&l.skill_version!==R?`${l.skill_version} \u2192 ${R}`:R;if(u.success(`Refreshed the SEOAgent skill (${p}).`),u.info("Your `.seoagent/` knowledge was left untouched."),u.info(`Skill at ${d}${f} \u2014 agents read this file at session start.`),za(t,c),u.info(`Run \`${m("status")}\` to see your project, or \`${m("uninstall")}\` to start fresh.`),!ce()){let h=me(t);h&&await Fe({installId:h,event:"reinit",cliVersion:R})&&u.info(Vt)}let g=await Va(t,l?.domain??Ge,kr(e));(g==="rejected"||g==="unreachable"||g==="invalid")&&u.warn(eg()),g!=="none"&&Do(g)&&(process.exitCode=1);return}let n=!!e.yes||!process.stdin.isTTY,r=xa(t);if(r.isWorktree&&!e.allowWorktree){let l=Xf(kr(e)),c=r.mainRoot?`cd ${r.mainRoot} && ${l}`:`run ${l} from the main repository root`;n&&(u.warn("This directory is a temporary git worktree \u2014 a `.seoagent/` created here is deleted with the worktree, and images/un-synced edits cannot be recovered from the cloud."),u.info(`Init at the main checkout instead: ${c}`),u.info("To init here anyway (not recommended), pass --allow-worktree."),process.exit(1)),Ga("SEOAgent \u2014 AI SEO Agent");let d=await Wa({message:"This looks like a temporary git worktree \u2014 .seoagent/ created here is lost when the worktree is removed. Create it here anyway?",initialValue:!1});(ft(d)||!d)&&(gt(`Good call. ${c}`),process.exit(0))}else n||Ga("SEOAgent \u2014 AI SEO Agent");let o=ca(t,l=>u.step(l)),i=()=>{if(o.evidence.length!==0){u.info("Inferred from your project:");for(let l of o.evidence)u.info(` \u2022 ${Zf(l)}`)}},s=e.domain?.trim()||process.env.SEOAGENT_DOMAIN?.trim()||o.domain||null,a=vr(e.siteType)||vr(process.env.SEOAGENT_SITE_TYPE)||o.siteType||null;if(n){e.siteType?.trim()&&vr(e.siteType)===null&&(u.warn("Invalid --site-type. Use: saas, service, product, content, marketplace, tool, nonprofit, unknown, app, community."),process.exit(1)),process.env.SEOAGENT_SITE_TYPE?.trim()&&vr(process.env.SEOAGENT_SITE_TYPE)===null&&(u.warn("Invalid SEOAGENT_SITE_TYPE environment value."),process.exit(1)),i(),s||(u.info("Couldn't detect your site URL from this repo \u2014 scaffolding anyway; you'll be prompted to supply it."),s=Ge),a||(a="unknown"),await qa(t,s,a,kr(e));return}if(i(),!s){let l=await Ha({message:"Website domain",placeholder:"example.com",validate:c=>c.trim()?void 0:"Domain is required"});ft(l)&&(gt("Cancelled"),process.exit(0)),s=String(l)}if(!a){let l=await Fo({message:"What kind of site is this?",options:[...Ka]});ft(l)&&(gt("Cancelled"),process.exit(0)),a=l}for(;;){let l=await Wa({message:`Create .seoagent for ${s} (${a})?`});if(ft(l)&&(gt("Cancelled"),process.exit(0)),l)break;let c=await Fo({message:"What should we change?",options:[{value:"domain",label:"Domain"},{value:"site_type",label:"Site type"},{value:"abort",label:"Cancel setup"}]});if((ft(c)||c==="abort")&&(gt("Cancelled"),process.exit(0)),c==="domain"){let d=await Ha({message:"Website domain",placeholder:"example.com",validate:f=>f.trim()?void 0:"Domain is required"});ft(d)&&(gt("Cancelled"),process.exit(0)),s=String(d)}else{let d=await Fo({message:"What kind of site is this?",options:[...Ka]});ft(d)&&(gt("Cancelled"),process.exit(0)),a=d}}await qa(t,s,a,kr(e))}async function qa(e,t,n,r){let o=Jf();o.start("Setting up your SEO project");let i=ta(e),s=Qe(e),a=nr(e),l=qt(),c={domain:t,site_type:n,language:ns(t),initialized_at:new Date().toISOString(),seoagent_version:R,skill_version:R,install_id:l,...i.cms!=="none"?{cms:i.cms}:{},...i.blog_path?{blog_path:i.blog_path}:{},...s.provider!=="none"?{image_provider:s.provider}:{},...a.dir?{public_dir:a.dir}:{}},d=ce(),f=d?Promise.resolve(!1):Fe({installId:l,event:"init",cliVersion:R}),p=d||t===Ge?Promise.resolve(null):Os({domain:t,installId:l}),g=M(t),h=g&&t!==Ge?Ye({apiBase:g.api_base,authHeader:`Bearer ${g.user_token}:${g.website_token}`,localDomain:t}).catch(()=>null):Promise.resolve(null);ks(e),bs(e,c),ct(e),Qs(e,n),Us(e);let y=hr(e,t!==Ge?t:null),w=St(e),b=Pt(e,w.skillFile),A=zf(b),x=`${A}/references/audit-checks.md`,E=w.claudeSymlink?Pt(e,w.claudeSymlink):void 0;lt(e);let I=await Va(e,t,r),z=I==="ready"||I==="already";if(I==="ready"&&t===Ge){let J=C(e)?.domain;J&&J!==Ge&&(t=J,c.domain=J)}let D=t!==Ge,K=I==="rejected"||I==="unreachable"||I==="invalid",Q=I==="other-site";(K||Q)&&(process.exitCode=1);let ee={pages:[],dynamicCount:0},se={wrote:!1,added:0,total:0,merged:!1,file:"pages.md"};try{ee=_e(e,D?t:null),se=ur(e,ee.pages)}catch{}if(o.stop(D?`Created .seoagent/ project for ${c.domain}`:"Created .seoagent/ project (site URL not detected yet \u2014 see step 2)"),i.cms!=="none"){let J=i.evidence.map(Oe=>`${Oe.detail} (${Oe.source})`).slice(0,2).join(", ");u.info(`CMS detected: ${i.cms} \u2014 ${J}`)}if(i.blog_path&&u.info(`Blog route: ${i.blog_path}`),a.dir&&u.info(`Static files: ${a.dir}/ (${a.evidence}) \u2014 your llms.txt and OKF bundle publish there.`),y.created.length>0&&u.info("Scaffolded .seoagent/okf/ \u2014 the AI-readable knowledge bundle answer engines cite. The audit will offer to fill it."),s.provider!=="none"&&u.info(`Image generation: ${s.provider} (detected ${s.matched_key}) \u2014 articles can get AI hero images.`),ee.pages.length>0){let J=se.merged?se.added>0?`Re-scanned your codebase: +${se.added} new page${se.added===1?"":"s"} merged into .seoagent/pages.md (${se.total} total)`:`Re-scanned your codebase: .seoagent/pages.md already current (${se.total} pages)`:`Found ${ee.pages.length} page${ee.pages.length===1?"":"s"} in your codebase \u2192 .seoagent/pages.md`;u.info(J+(ee.dynamicCount>0?` (plus ${ee.dynamicCount} dynamic route template${ee.dynamicCount===1?"":"s"} \u2014 the audit will discover their instances)`:""))}za(e,w);let[Kn,Ie,oo]=await Promise.all([f,p,h]);if(oo?.verdict==="mismatch"&&u.warn(`Heads up: the credentials bound to ${t} are for a SEOAgent cloud project on ${oo.cloudDomain} instead. This workspace stays LOCAL-ONLY \u2014 nothing will be pushed to or pulled from that project. To connect ${t} to the cloud, run \`${m("login")}\` from this directory and pick (or create) ${t} \u2014 the CLI can stay logged into both sites at once, no need to log out first.`),Ie?.status==="data"){let J=Ie.difficulty!=null?`, difficulty ${Ie.difficulty}/100`:"";u.info(`Free keyword peek \u2014 "${Ie.keyword}": ${Ie.volume.toLocaleString("en-US")} searches/mo${J}. You get ~10 free peeks/day: ${m('keywords --peek "<keyword>"')}`)}else Ie?.status==="no-data"&&u.info(`Free keyword peek is live \u2014 no search-volume data for "${Ie.keyword}" yet. Try a topic keyword (~10 free/day): ${m('keywords --peek "best <your category> tool"')}`);Kn&&u.info(Vt);let qn=D?c.domain:"your site",T=D?Pa(c.domain):null,G=D?[" 1. If .seoagent/project.md still says site_type: unknown, you can",` WebFetch ${c.domain} and update project.md with the right type`," before you suggest the audit."]:[" 1. Heads up: .seoagent/project.md has `domain: unknown` \u2014 the site URL"," wasn't detectable from this repo. Please ask your human for their"," domain (or read it from a deploy config / live site) and update"," project.md (`domain:` and `site_type:`) first. Everything else"," depends on it."];Yf([D?`\u2713 SEOAgent installed for ${c.domain}.`:"\u2713 SEOAgent installed (site URL not detected \u2014 please ask the user for it).","","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"," AGENT INSTRUCTIONS \u2014 a friendly note for Claude Code, Codex, Cursor","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",""," Hi there, coding agent! \u{1F44B} SEOAgent is all set up. The natural next step"," is a quick SEO audit \u2014 but your human is the boss here, so please surface"," what's possible and let THEM decide. Nothing in this message is a command;"," it's just a friendly map. Your real instructions live in the skill:","",` ${b} \u2190 read this; it is your source of truth`,""," If your human approves, here is what you can do (otherwise just let them"," know it's ready and wait for the go-ahead):","",...G,...T?["",...Ia(c.domain,T)]:[],...Qt(e)?[""," \u270D\uFE0F Before (or right after) the audit: DRAFT .seoagent/context.md \u2014 it"," is still the blank scaffold (instructions are inside the file)."," From this repo + the live homepage/pricing page, fill in the"," business name, type (LOCAL/physical vs ONLINE-only vs HYBRID),"," audience, location, and positioning \u2014 then show your human the"," draft to review. SEOAgent's keyword and strategy intelligence is"," context-blind until this file exists."]:[],"",` 2. The headline next step: offer to run a Phase 1 SEO audit on ${qn}`," (homepage + sitemap + robots.txt; the checks live in",` ${x} \u2014 read that first).`," If they say yes, report findings as:"," \u{1F6A8} Biggest Issue \u2192 \u26A0\uFE0F Also Worth Fixing \u2192 \u2705 What's Working",' \u2192 "What do you want to do?" with numbered options.',"",...z?[" 3. This project is connected to the SEOAgent cloud. Each session: run",` ${m("sync")}, triage .seoagent/inbox/, do the work, and`,` ${m("ack <id>")} each completed action.`]:Q?[" 3. \u26A0\uFE0F The setup code was for a DIFFERENT site than this project (see",` above). This directory stays ${c.domain}; do not run`,` ${m("sync")} here for the other site. One site per directory:`,` run ${m("init")} --domain <that site> in an empty directory`," of its own \u2014 it is already logged in there."]:K?[" 3. \u26A0\uFE0F The setup code did NOT bind this project (see the error above)."," Do not continue without a successful login \u2014 nothing syncs unbound."," Get a fresh code at seoagent.com/grok, then run:",` ${m("login")} --code <code>`]:[" 3. When it feels right, let them know about the free cloud dashboard and"," offer to start it (their call) \u2014 and that keyword strategy (Phase 2 in"," the skill) is there whenever they want it:",` ${m("login")}`],"","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"," HUMAN USERS (if you ran this in your terminal, no AI agent)","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",""," Open Claude Code in this directory and say:",""," audit my site","",` The skill at ${b} is picked up automatically.`,...T?[` Your site is on ${T.label}, so there is no code to edit here \u2014 this folder`,T.api?` is the workspace. The agent will ask you once for ${T.enableHint}.`:` is the workspace. The agent writes each article for you to paste into ${T.label}.`]:[],...E?[` Symlinked at ${E} \u2192 ${A} so Claude Code finds it too.`]:[],"",...z?[" Cloud dashboard: connected. Pull your first work with:","",` ${m("sync")}`]:Q?[` \u26A0\uFE0F That setup code was for another site; this project stays ${c.domain}.`," Give the other site a directory of its own:","",` ${m("init")} --domain <that site>`]:K?[" \u26A0\uFE0F The setup code did not bind this project. Get a fresh code at"," seoagent.com/grok, then run:","",` ${m("login")} --code <code>`]:[" Want the cloud dashboard now? Free account, ~30 seconds:","",` ${m("login")}`],""].join(`
86
+ `,"utf-8"),r.push("log.md")),{created:r,okfDir:n}}import{log as At}from"@clack/prompts";var u={info:e=>At.info(e),warn:e=>At.warn(e),error:e=>At.error(e),success:e=>At.success(e),step:e=>At.step(e),message:e=>At.message(e)};function S(e,t,n,r=n){e?e.stop(r):u[t](n)}var Aa=[{suffixes:[".wordpress.com"],platform:{id:"wordpress.com",label:"WordPress.com",api:"wordpress",enableHint:"an application password (Users \u2192 Profile \u2192 Application Passwords) or a WordPress.com OAuth token"}},{suffixes:[".wixsite.com",".wix.com"],platform:{id:"wix",label:"Wix",api:"wix",enableHint:"an API key from the Wix API Keys Manager with Blog permissions, plus the site ID"}},{suffixes:[".myshopify.com"],platform:{id:"shopify",label:"Shopify",api:"shopify",enableHint:"a Shopify Admin API access token with blog write scope"}},{suffixes:[".webflow.io"],platform:{id:"webflow",label:"Webflow",api:"webflow",enableHint:"a Webflow site API token with CMS write access"}},{suffixes:[".ghost.io"],platform:{id:"ghost",label:"Ghost",api:"ghost",enableHint:"a Ghost Admin API key"}},{suffixes:[".blogspot.com"],platform:{id:"blogger",label:"Blogger",api:"blogger",enableHint:"Blogger API access"}},{suffixes:[".squarespace.com"],platform:{id:"squarespace",label:"Squarespace",api:null,enableHint:""}},{suffixes:[".weebly.com"],platform:{id:"weebly",label:"Weebly",api:null,enableHint:""}},{suffixes:[".godaddysites.com"],platform:{id:"godaddy",label:"GoDaddy Website Builder",api:null,enableHint:""}},{suffixes:[".carrd.co"],platform:{id:"carrd",label:"Carrd",api:null,enableHint:""}},{suffixes:[".notion.site"],platform:{id:"notion",label:"Notion",api:null,enableHint:""}},{suffixes:[".substack.com"],platform:{id:"substack",label:"Substack",api:null,enableHint:""}},{suffixes:[".medium.com"],platform:{id:"medium",label:"Medium",api:null,enableHint:""}},{suffixes:[".tumblr.com"],platform:{id:"tumblr",label:"Tumblr",api:null,enableHint:""}},{suffixes:[".framer.website",".framer.app"],platform:{id:"framer",label:"Framer",api:null,enableHint:""}}];function Pa(e){if(!e)return null;let t=e.trim().toLowerCase().replace(/^sc-domain:/,"").replace(/^https?:\/\//,"").replace(/^www\./,"").replace(/[/?#].*$/,"").replace(/:\d+$/,"");if(!t)return null;for(let{suffixes:n,platform:r}of Aa)if(n.some(o=>t.endsWith(o)&&t.length>o.length))return r;return null}var $x=Aa.flatMap(({suffixes:e,platform:t})=>e.map(n=>({suffix:n,id:t.id,api:t.api})));function Ia(e,t){let n=[` \u{1F3E0} ${e} is hosted on ${t.label} \u2014 there is no repository here and`," none to find; this folder is the workspace. Audit, keywords, strategy"," and writing all work as normal. Delivery differs: read",' references/publishing.md \xA7 "No repo" (next to the skill) BEFORE the'," audit."];return t.api?n.push(` Ask your human once to set up the ${t.label} connector:`,` ${t.enableHint}. Credentials go in .env in this folder, never`," in a tracked file. Posts are created as drafts unless told otherwise."):n.push(` ${t.label} has no public API for posts, so the connector is the`," editor: write each article to .seoagent/content/<slug>.md with the exact"," title, slug, meta description and body, walk your human through pasting"," it, then `seoagent content track --slug <slug> --url <live-url>`."),n}import{randomBytes as If}from"crypto";import{isCancel as Of,spinner as Fa,text as Tf}from"@clack/prompts";import de from"picocolors";import{exec as Pf}from"child_process";function yr(e){let t=process.platform==="darwin"?`open "${e}"`:process.platform==="win32"?`start "" "${e}"`:`xdg-open "${e}"`;Pf(t,()=>{})}function Oa(e,t){if(e>=500)return{status:"error",terminal:!1,message:`HTTP ${e} (server error)`};if(e>=400)return{status:"error",terminal:!0,message:`HTTP ${e} (client error)`};if(!t||typeof t!="object")return{status:"error",terminal:!0,message:`Malformed response: expected an object, got ${typeof t}`};let n=t;if(n.status==="ready"){if(typeof n.user_token=="string"&&typeof n.website_token=="string"){let r=typeof n.email=="string"&&n.email?n.email:void 0,o=typeof n.domain=="string"&&n.domain?n.domain:void 0;return{status:"ready",user_token:n.user_token,website_token:n.website_token,...r?{email:r}:{},...o?{domain:o}:{}}}return{status:"error",terminal:!0,message:"Malformed response: status=ready without user_token+website_token"}}return n.status==="pending"?{status:"pending"}:n.status==="expired"?{status:"expired"}:{status:"error",terminal:!0,message:`Malformed response: unknown status=${JSON.stringify(n.status)}`}}var La=/^sa_[0-9a-f]{24}$/;function Ta(e){switch(e){case"code_not_found":return"That setup code was not recognized: it was never issued. Codes cannot be guessed or retyped from memory; copy one exactly from seoagent.com/grok. A scheduled run on a fresh machine needs the SEOAGENT_AUTH secret (`seoagent whoami --env` on the bound machine), not a setup code.";case"code_already_used":return"That setup code has already been used \u2014 each code works exactly once. Generate a new one at seoagent.com/grok.";case"code_revoked":return"That setup code was replaced by a newer one. Use the latest code, or generate a new one at seoagent.com/grok.";case"code_expired":return"That setup code has expired. Generate a new one at seoagent.com/grok.";case"setup_codes_unavailable":return"Setup codes are not enabled on this server yet. Run `seoagent login` (browser flow) instead.";case"domain_required":return"This code has no site attached and the project has no domain. Your code has NOT been used \u2014 run `seoagent init` in the project root (or pass `--domain example.com`) and retry with the same code.";default:return typeof e=="string"&&e?`Login failed: ${e}`:"Login failed."}}function Na(e,t){let n=t&&typeof t=="object"?t:null;if(e>=500)return e===503&&n?.error==="setup_codes_unavailable"?{status:"error",terminal:!0,message:Ta(n.error)}:{status:"error",terminal:!1,message:`HTTP ${e} (server error)`};if(e>=400){let r=typeof n?.error=="string"&&n.error?n.error:void 0;return{status:"error",terminal:!0,message:Ta(n?.error),...r?{code:r}:{}}}if(!n)return{status:"error",terminal:!0,message:`Malformed response: expected an object, got ${typeof t}`};if(n.status==="ready"){if(typeof n.user_token=="string"&&typeof n.website_token=="string"){let r=typeof n.email=="string"&&n.email?n.email:void 0,o=typeof n.domain=="string"&&n.domain?n.domain:void 0;return{status:"ready",user_token:n.user_token,website_token:n.website_token,...r?{email:r}:{},...o?{domain:o}:{}}}return{status:"error",terminal:!0,message:"Malformed response: status=ready without user_token+website_token"}}return{status:"error",terminal:!0,message:`Malformed response: unknown status=${JSON.stringify(n.status)}`}}var Lf=1500,Nf=900*1e3,ja="Login timed out. If the browser page shows a `seoagent login --code \u2026` line, run that here; otherwise run `seoagent login` again.",jf=3,Df=2e3;function wr(e,t){if(ce())return;let n=me(e);n&&Fe({installId:n,event:t,cliVersion:R})}function Mf(){return If(16).toString("hex")}function Ff(e,t,n){let r=new URLSearchParams({session:t});return n&&n!=="unknown"&&r.set("domain",n),`${e}/cli/auth?${r.toString()}`}function Da(e){let t=e.trim().replace(/^https?:\/\//i,"").replace(/^www\./i,"").replace(/\/.*$/,"");return!t||!/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(t)?null:t.toLowerCase()}async function Uf(e,t,n){if(!e||e.website_token===t)return null;try{let r=await n(e);return r.verdict!=="unverified"?r.cloudDomain:null}catch{return null}}async function Bf(e,t){try{let n=`${e}/api/cli/auth/poll?session=${encodeURIComponent(t)}`,r=await fetch(n,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(1e4)}),o=null;try{o=await r.json()}catch{}return Oa(r.status,o)}catch(n){return{status:"error",terminal:!1,message:n.message||"network error"}}}function Ua(e){return new Promise(t=>setTimeout(t,e))}async function Ba(e,t,n,r){let o=M(),i=await Uf(o,e.website_token,c=>Ye({apiBase:c.api_base,authHeader:`Bearer ${c.user_token}:${c.website_token}`,localDomain:""}));i&&o&&yo(o,i),yo({user_token:e.user_token,website_token:e.website_token,api_base:t},e.domain||n);let s=e.email&&e.domain?`Logged in as ${e.email} (${e.domain}). Future SEO work in this repo will sync to your dashboard.`:e.email?`Logged in as ${e.email}. Future SEO work in this repo will sync to your dashboard.`:"Logged in. Future SEO work in this repo will sync to your dashboard.",a=e.email&&e.domain?`Logged in as ${de.bold(e.email)} (${de.cyan(e.domain)}).`:e.email?`Logged in as ${de.bold(e.email)}.`:"Logged in.";S(r,"success",s,de.green(a)),r&&u.info("Future SEO work in this repo will sync to your dashboard.");let l=Je().filter(c=>c!==$e(e.domain||n));l.length>0&&u.info(`Also logged in to: ${l.join(", ")}. Each project auto-selects its own site \u2014 no need to log out first.`)}function Gf(e,t,n){return{code:e,...t&&t!=="unknown"?{domain:t}:{},...n?{install_id:n.installId,cli_version:n.cliVersion,platform:process.platform}:{}}}function jo(e){if(ce())return null;let t=me(e);return t?{installId:t,cliVersion:R}:null}async function Hf(e,t,n,r){try{let o=await fetch(`${e}/api/cli/auth/redeem`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(Gf(t,n,r)),signal:AbortSignal.timeout(1e4)}),i=null;try{i=await o.json()}catch{}return Na(o.status,i)}catch(o){return{status:"error",terminal:!1,message:o.message||"network error"}}}var Wf=new Set(["code_already_used","code_revoked","code_expired"]);function Kf(e,t){return`That setup code was for ${e}, but this directory is the ${t} project \u2014 it stays ${t}, and running \`${m("sync")}\` here syncs ${t}. Credentials for ${e} are saved. One site per directory: run \`${m("init")} --domain ${e}\` in an empty directory of its own (or in the repo that builds ${e}) \u2014 it is already logged in there.`}var qf="Scheduled runs on a fresh machine: store the output of `seoagent whoami --env` as the task's SEOAGENT_AUTH secret. A setup code works once and cannot be guessed; never reuse or invent one.";function Do(e){return e!=="ready"&&e!=="already"}function Vf(e,t,n){if(!e||!n||t&&t!=="unknown")return!1;try{if(!q(e,{domain:n}))return!1}catch{return!1}return u.info(`Saved domain: ${n} \u2192 .seoagent/project.md (from the setup code).`),!0}async function Mo(e,t,n,r,o,i={}){if(!La.test(e))return u.warn("That does not look like a setup code (expected `sa_` + 24 hex characters). Generate one at seoagent.com/grok."),"invalid";let s=!!(i.siteAlreadyBound&&n),a=s?null:n,c=!!process.stdout.isTTY?Fa():null;c?.start(de.dim("Redeeming setup code\u2026"));let d="network error";for(let f=0;f<jf;f++){f>0&&await Ua(Df);let p=await Hf(t,e,a,r);if(p.status==="ready")return await Ba(p,t,n,c),s&&p.domain&&$e(p.domain)!==$e(n)?(u.warn(Kf(p.domain,n)),"other-site"):(Vf(o,n,p.domain),u.info(qf),"ready");if(p.terminal)return s&&p.code&&Wf.has(p.code)?(S(c,"info",`Already logged in for ${n} \u2014 that setup code is spent (each works once); nothing to do.`,de.dim(`Already logged in for ${n} \u2014 setup code spent, nothing to do.`)),"already"):(S(c,"warn",p.message,de.red(p.message)),"rejected");d=p.message}return S(c,"warn",`Could not reach the server (${d}). Check your connection and run \`${m("login")} --code <code>\` again.`,de.yellow("Could not reach the server.")),c&&u.warn(`Run \`${m("login")} --code <code>\` again to retry.`),"unreachable"}function br(e){return e&&e!=="unknown"?Ps(e):!!M()}function Ma(){return`Next: run \`${m("sync")}\` in this repo \u2014 it pulls your strategy, briefs and inbox into .seoagent/. Nothing syncs until it runs.`}async function on(e={}){let t=process.cwd(),n=C(t),r=(e.apiBase||N.BASE||je).replace(/\/$/,""),o=n?.domain??null;if(o==="unknown"&&process.stdin.isTTY){let g=await Tf({message:".seoagent/project.md has `domain: unknown`. What is your site's URL?",placeholder:"example.com",validate(y){if(!String(y??"").trim())return"A site URL is required.";if(!Da(String(y)))return"Please enter a valid domain like `example.com`."}});if(Of(g)){u.warn("Login cancelled.");return}let h=Da(String(g));if(h){o=h;try{q(t,{domain:h}),u.success(`Saved domain: ${h} \u2192 .seoagent/project.md`)}catch{}}}let i=(e.code??process.env.SEOAGENT_SETUP_CODE??"").trim(),s=o&&o!=="unknown";if(!i&&br(o)){let g=s?` for ${o}`:"";u.info(`Already logged in${g}. To switch accounts${g}, run \`${m("logout")}\` first.`);return}if(i){let g=await Mo(i,r,o,jo(t),t,{siteAlreadyBound:!!s&&br(o)});Do(g)?process.exitCode=1:g==="ready"&&u.info(Ma());return}let a=Mf(),l=Ff(r,a,o);u.message("Opening seoagent.com to connect this CLI to your account..."),u.info(`If the browser does not open, visit: ${l}`),yr(l),u.info('In your browser: sign in (if needed) and click "Connect this CLI" to finish.');let d=!!process.stdout.isTTY?Fa():null;d?.start(de.dim("Waiting for browser confirmation\u2026"));let f=Date.now()+Nf,p=!0;for(;Date.now()<f;){p||await Ua(Lf),p=!1;let g=await Bf(r,a);if(g.status==="ready"){await Ba(g,r,o,d),wr(t,"login_browser_ok"),u.info(Ma());return}if(g.status==="expired"){wr(t,"login_browser_expired"),S(d,"warn",`Session expired. Run \`${m("login")}\` again.`,de.red("Session expired.")),d&&u.warn(`Run \`${m("login")}\` again.`);return}if(g.status==="error"){if(g.terminal){S(d,"warn",`Login failed: ${g.message}. Run \`${m("login")}\` again.`,de.red(`Login failed: ${g.message}.`)),d&&u.warn(`Run \`${m("login")}\` again.`),wr(t,"login_browser_failed");return}continue}}S(d,"warn",ja,de.yellow("Login timed out.")),d&&u.warn(ja),wr(t,"login_browser_timeout")}function sn(e={}){let t=e.all?null:C(process.cwd()),n=!e.all&&t?.domain&&t.domain!=="unknown"?t.domain:null;if(!Is(e.all?null:n)){u.info(n?`You were not logged in for ${n}.`:"You were not logged in.");return}u.success(n?`Logged out of ${n}.`:"Logged out.");let o=Je();o.length>0&&u.info(`Still logged in to: ${o.join(", ")}. Run \`${m("logout")} --all\` to log out of everything.`)}function kr(e,t=process.env){return(e.code??t.SEOAGENT_SETUP_CODE??"").trim()}function Xf(e,t=m("init")){return e?`${t} --code ${e}`:t}async function Va(e,t,n){if(!n)return"none";let r=t!==Ge,o=(N.BASE||je).replace(/\/$/,"");return Mo(n,o,r?t:null,jo(e),e,{siteAlreadyBound:r&&br(t)})}var Ka=[{value:"saas",label:"SaaS / App"},{value:"service",label:"Service business"},{value:"product",label:"E-commerce / Product"},{value:"content",label:"Content / Blog"},{value:"marketplace",label:"Marketplace"},{value:"tool",label:"Tool / Utility"},{value:"nonprofit",label:"Nonprofit / Community"},{value:"unknown",label:"Not sure"}],Ge="unknown",Qf=new Set(["saas","service","product","content","marketplace","tool","app","nonprofit","community","unknown"]);function Zf(e){return e.field==="context"?`${e.detail} \u2014 ${e.source}`:e.field==="domain"?`Domain: ${e.detail} \u2014 ${e.source}`:`Site type: ${e.detail} \u2014 ${e.source}`}function vr(e){if(!e?.trim())return null;let t=e.trim().toLowerCase();return Qf.has(t)?t:null}function za(e,t){t.agentsMdPath&&u.info(`Added a SEOAgent section to ${Pt(e,t.agentsMdPath)} so agents that read it (Codex, others) find the skill.`),t.cursorRulePath&&u.info(`Wrote ${Pt(e,t.cursorRulePath)} so Cursor agents find the skill.`)}async function an(e={}){let t=process.cwd();if(ws(t)){let l=C(t),c=St(t);lt(t),q(t,{skill_version:R}),ct(t);let d=Pt(t,c.skillFile),f=c.claudeSymlink?` (symlinked at ${Pt(t,c.claudeSymlink)})`:"",p=l?.skill_version&&l.skill_version!==R?`${l.skill_version} \u2192 ${R}`:R;if(u.success(`Refreshed the SEOAgent skill (${p}).`),u.info("Your `.seoagent/` knowledge was left untouched."),u.info(`Skill at ${d}${f} \u2014 agents read this file at session start.`),za(t,c),u.info(`Run \`${m("status")}\` to see your project, or \`${m("uninstall")}\` to start fresh.`),!ce()){let h=me(t);h&&await Fe({installId:h,event:"reinit",cliVersion:R})&&u.info(Vt)}let g=await Va(t,l?.domain??Ge,kr(e));(g==="rejected"||g==="unreachable"||g==="invalid")&&u.warn(eg()),g!=="none"&&Do(g)&&(process.exitCode=1);return}let n=!!e.yes||!process.stdin.isTTY,r=xa(t);if(r.isWorktree&&!e.allowWorktree){let l=Xf(kr(e)),c=r.mainRoot?`cd ${r.mainRoot} && ${l}`:`run ${l} from the main repository root`;n&&(u.warn("This directory is a temporary git worktree \u2014 a `.seoagent/` created here is deleted with the worktree, and images/un-synced edits cannot be recovered from the cloud."),u.info(`Init at the main checkout instead: ${c}`),u.info("To init here anyway (not recommended), pass --allow-worktree."),process.exit(1)),Ga("SEOAgent \u2014 AI SEO Agent");let d=await Wa({message:"This looks like a temporary git worktree \u2014 .seoagent/ created here is lost when the worktree is removed. Create it here anyway?",initialValue:!1});(ft(d)||!d)&&(gt(`Good call. ${c}`),process.exit(0))}else n||Ga("SEOAgent \u2014 AI SEO Agent");let o=ca(t,l=>u.step(l)),i=()=>{if(o.evidence.length!==0){u.info("Inferred from your project:");for(let l of o.evidence)u.info(` \u2022 ${Zf(l)}`)}},s=e.domain?.trim()||process.env.SEOAGENT_DOMAIN?.trim()||o.domain||null,a=vr(e.siteType)||vr(process.env.SEOAGENT_SITE_TYPE)||o.siteType||null;if(n){e.siteType?.trim()&&vr(e.siteType)===null&&(u.warn("Invalid --site-type. Use: saas, service, product, content, marketplace, tool, nonprofit, unknown, app, community."),process.exit(1)),process.env.SEOAGENT_SITE_TYPE?.trim()&&vr(process.env.SEOAGENT_SITE_TYPE)===null&&(u.warn("Invalid SEOAGENT_SITE_TYPE environment value."),process.exit(1)),i(),s||(u.info("Couldn't detect your site URL from this repo \u2014 scaffolding anyway; you'll be prompted to supply it."),s=Ge),a||(a="unknown"),await qa(t,s,a,kr(e));return}if(i(),!s){let l=await Ha({message:"Website domain",placeholder:"example.com",validate:c=>c.trim()?void 0:"Domain is required"});ft(l)&&(gt("Cancelled"),process.exit(0)),s=String(l)}if(!a){let l=await Fo({message:"What kind of site is this?",options:[...Ka]});ft(l)&&(gt("Cancelled"),process.exit(0)),a=l}for(;;){let l=await Wa({message:`Create .seoagent for ${s} (${a})?`});if(ft(l)&&(gt("Cancelled"),process.exit(0)),l)break;let c=await Fo({message:"What should we change?",options:[{value:"domain",label:"Domain"},{value:"site_type",label:"Site type"},{value:"abort",label:"Cancel setup"}]});if((ft(c)||c==="abort")&&(gt("Cancelled"),process.exit(0)),c==="domain"){let d=await Ha({message:"Website domain",placeholder:"example.com",validate:f=>f.trim()?void 0:"Domain is required"});ft(d)&&(gt("Cancelled"),process.exit(0)),s=String(d)}else{let d=await Fo({message:"What kind of site is this?",options:[...Ka]});ft(d)&&(gt("Cancelled"),process.exit(0)),a=d}}await qa(t,s,a,kr(e))}async function qa(e,t,n,r){let o=Jf();o.start("Setting up your SEO project");let i=ta(e),s=Qe(e),a=nr(e),l=qt(),c={domain:t,site_type:n,language:ns(t),initialized_at:new Date().toISOString(),seoagent_version:R,skill_version:R,install_id:l,...i.cms!=="none"?{cms:i.cms}:{},...i.blog_path?{blog_path:i.blog_path}:{},...s.provider!=="none"?{image_provider:s.provider}:{},...a.dir?{public_dir:a.dir}:{}},d=ce(),f=d?Promise.resolve(!1):Fe({installId:l,event:"init",cliVersion:R}),p=d||t===Ge?Promise.resolve(null):Os({domain:t,installId:l}),g=M(t),h=g&&t!==Ge?Ye({apiBase:g.api_base,authHeader:`Bearer ${g.user_token}:${g.website_token}`,localDomain:t}).catch(()=>null):Promise.resolve(null);ks(e),bs(e,c),ct(e),Qs(e,n),Us(e);let y=hr(e,t!==Ge?t:null),w=St(e),b=Pt(e,w.skillFile),A=zf(b),x=`${A}/references/audit-checks.md`,E=w.claudeSymlink?Pt(e,w.claudeSymlink):void 0;lt(e);let I=await Va(e,t,r),z=I==="ready"||I==="already";if(I==="ready"&&t===Ge){let J=C(e)?.domain;J&&J!==Ge&&(t=J,c.domain=J)}let D=t!==Ge,K=I==="rejected"||I==="unreachable"||I==="invalid",Q=I==="other-site";(K||Q)&&(process.exitCode=1);let ee={pages:[],dynamicCount:0},se={wrote:!1,added:0,total:0,merged:!1,file:"pages.md"};try{ee=_e(e,D?t:null),se=ur(e,ee.pages)}catch{}if(o.stop(D?`Created .seoagent/ project for ${c.domain}`:"Created .seoagent/ project (site URL not detected yet \u2014 see step 2)"),i.cms!=="none"){let J=i.evidence.map(Oe=>`${Oe.detail} (${Oe.source})`).slice(0,2).join(", ");u.info(`CMS detected: ${i.cms} \u2014 ${J}`)}if(i.blog_path&&u.info(`Blog route: ${i.blog_path}`),a.dir&&u.info(`Static files: ${a.dir}/ (${a.evidence}) \u2014 your llms.txt and OKF bundle publish there.`),y.created.length>0&&u.info("Scaffolded .seoagent/okf/ \u2014 the AI-readable knowledge bundle answer engines cite. The audit will offer to fill it."),s.provider!=="none"&&u.info(`Image generation: ${s.provider} (detected ${s.matched_key}) \u2014 articles can get AI hero images.`),ee.pages.length>0){let J=se.merged?se.added>0?`Re-scanned your codebase: +${se.added} new page${se.added===1?"":"s"} merged into .seoagent/pages.md (${se.total} total)`:`Re-scanned your codebase: .seoagent/pages.md already current (${se.total} pages)`:`Found ${ee.pages.length} page${ee.pages.length===1?"":"s"} in your codebase \u2192 .seoagent/pages.md`;u.info(J+(ee.dynamicCount>0?` (plus ${ee.dynamicCount} dynamic route template${ee.dynamicCount===1?"":"s"} \u2014 the audit will discover their instances)`:""))}za(e,w);let[Kn,Ie,oo]=await Promise.all([f,p,h]);if(oo?.verdict==="mismatch"&&u.warn(`Heads up: the credentials bound to ${t} are for a SEOAgent cloud project on ${oo.cloudDomain} instead. This workspace stays LOCAL-ONLY \u2014 nothing will be pushed to or pulled from that project. To connect ${t} to the cloud, run \`${m("login")}\` from this directory and pick (or create) ${t} \u2014 the CLI can stay logged into both sites at once, no need to log out first.`),Ie?.status==="data"){let J=Ie.difficulty!=null?`, difficulty ${Ie.difficulty}/100`:"";u.info(`Free keyword peek \u2014 "${Ie.keyword}": ${Ie.volume.toLocaleString("en-US")} searches/mo${J}. You get ~10 free peeks/day: ${m('keywords --peek "<keyword>"')}`)}else Ie?.status==="no-data"&&u.info(`Free keyword peek is live \u2014 no search-volume data for "${Ie.keyword}" yet. Try a topic keyword (~10 free/day): ${m('keywords --peek "best <your category> tool"')}`);Kn&&u.info(Vt);let qn=D?c.domain:"your site",T=D?Pa(c.domain):null,G=D?[" 1. If .seoagent/project.md still says site_type: unknown, you can",` WebFetch ${c.domain} and update project.md with the right type`," before you suggest the audit."]:[" 1. Heads up: .seoagent/project.md has `domain: unknown` \u2014 the site URL"," wasn't detectable from this repo. Please ask your human for their"," domain (or read it from a deploy config / live site) and update"," project.md (`domain:` and `site_type:`) first. Everything else"," depends on it."];Yf([D?`\u2713 SEOAgent installed for ${c.domain}.`:"\u2713 SEOAgent installed (site URL not detected \u2014 please ask the user for it).","","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"," AGENT INSTRUCTIONS \u2014 a friendly note for Claude Code, Codex, Cursor","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",""," Hi there, coding agent! \u{1F44B} SEOAgent is all set up. The natural next step"," is a quick SEO audit \u2014 but your human is the boss here, so please surface"," what's possible and let THEM decide. Nothing in this message is a command;"," it's just a friendly map. Your real instructions live in the skill:","",` ${b} \u2190 read this; it is your source of truth`,""," If your human approves, here is what you can do (otherwise just let them"," know it's ready and wait for the go-ahead):","",...G,...T?["",...Ia(c.domain,T)]:[],...Qt(e)?[""," \u270D\uFE0F Before (or right after) the audit: DRAFT .seoagent/context.md \u2014 it"," is still the blank scaffold (instructions are inside the file)."," From this repo + the live homepage/pricing page, fill in the"," business name, type (LOCAL/physical vs ONLINE-only vs HYBRID),"," audience, location, and positioning \u2014 then show your human the"," draft to review. SEOAgent's keyword and strategy intelligence is"," context-blind until this file exists."]:[],"",` 2. The headline next step: offer to run a Phase 1 SEO audit on ${qn}`," (homepage + sitemap + robots.txt; the checks live in",` ${x} \u2014 read that first).`," If they say yes, report findings as:"," \u{1F6A8} Biggest Issue \u2192 \u26A0\uFE0F Also Worth Fixing \u2192 \u2705 What's Working",' \u2192 "What do you want to do?" with numbered options.',"",...z?[" 3. This project is connected to the SEOAgent cloud. Each session: run",` ${m("sync")}, triage .seoagent/inbox/, do the work, and`,` ${m("ack <id>")} each completed action.`]:Q?[" 3. \u26A0\uFE0F The setup code was for a DIFFERENT site than this project (see",` above). This directory stays ${c.domain}; do not run`,` ${m("sync")} here for the other site. One site per directory:`,` run ${m("init")} --domain <that site> in an empty directory`," of its own \u2014 it is already logged in there."]:K?[" 3. \u26A0\uFE0F The setup code did NOT bind this project (see the error above)."," Do not continue without a successful login \u2014 nothing syncs unbound."," Get a fresh code at seoagent.com/grok, then run:",` ${m("login")} --code <code>`]:[" 3. When it feels right, let them know about the free cloud dashboard and"," offer to start it (their call) \u2014 and that keyword strategy (Phase 2 in"," the skill) is there whenever they want it:",` ${m("login")}`],"","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"," HUMAN USERS (if you ran this in your terminal, no AI agent)","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",""," Open Claude Code in this directory and say:",""," audit my site","",` The skill at ${b} is picked up automatically.`,...T?[` Your site is on ${T.label}, so there is no code to edit here \u2014 this folder`,T.api?` is the workspace. The agent will ask you once for ${T.enableHint}.`:` is the workspace. The agent writes each article for you to paste into ${T.label}.`]:[],...E?[` Symlinked at ${E} \u2192 ${A} so Claude Code finds it too.`]:[],"",...z?[" Cloud dashboard: connected. Pull your first work with:","",` ${m("sync")}`]:Q?[` \u26A0\uFE0F That setup code was for another site; this project stays ${c.domain}.`," Give the other site a directory of its own:","",` ${m("init")} --domain <that site>`]:K?[" \u26A0\uFE0F The setup code did not bind this project. Get a fresh code at"," seoagent.com/grok, then run:","",` ${m("login")} --code <code>`]:[" Want the cloud dashboard now? Free account, ~30 seconds:","",` ${m("login")}`],""].join(`
87
87
  `))}function eg(){return`The setup code did not bind this project. Do not continue without a successful login \u2014 get a fresh code at seoagent.com/grok, then run \`${m("login")} --code <code>\`.`}import{existsSync as Xa}from"fs";import{intro as ng,outro as rg,confirm as og,isCancel as ig,cancel as sg}from"@clack/prompts";import{existsSync as $r,rmSync as tg}from"fs";import{join as xr}from"path";function Ya(e,t={}){let n=[],r=xr(e,k);$r(r)&&n.push({label:".seoagent/ \u2014 project state + all generated work",path:r,kind:"dir"});let o=xr(e,Ne.CLAUDE);$r(o)&&n.push({label:".claude/skills/seoagent/ \u2014 installed skill bundle",path:o,kind:"dir"});let i=xr(e,Ne.AGENTS);return $r(i)&&n.push({label:".agents/skills/seoagent/ \u2014 installed skill bundle",path:i,kind:"dir"}),Gs(e)&&n.push({label:".claude/settings.json \u2014 sync hook entry (other settings preserved)",path:xr(e,".claude","settings.json"),kind:"hook"}),t.global&&$r(Y)&&n.push({label:"~/.config/seoagent/ \u2014 login credentials + sync state (all projects)",path:Y,kind:"global"}),n}function Ja(e,t){for(let n of t)n.kind==="hook"?Hs(e):tg(n.path,{recursive:!0,force:!0})}async function ln(e={}){let t=process.cwd(),n=Ya(t,{global:e.global});if(n.length===0){u.info("No SEOAgent install found in this directory \u2014 nothing to remove."),!e.global&&Xa(Y)&&u.info("Your seoagent.com login is still stored at ~/.config/seoagent/. Re-run with --global to wipe it too.");return}let r=!!e.yes||!process.stdin.isTTY,o=n.map(s=>` \u2022 ${s.label}`).join(`
88
88
  `);if(!r){ng("SEOAgent \u2014 uninstall"),u.message(`This will remove:
89
89
  ${o}`);let s=await og({message:"Remove these and start fresh?"});if(ig(s)||!s){sg("Cancelled \u2014 nothing was removed.");return}}Ja(t,n);let i=n.map(s=>` \u2713 ${s.label}`).join(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seoagent-official/seoagent",
3
- "version": "1.101.1",
3
+ "version": "1.101.2",
4
4
  "description": "The persistent AI SEO agent for Claude Code. Audits, keyword strategy, briefs, articles, real product screenshots from your repo, and the autopilot loop (cloud detects → CLI executes → ack closes) — other SEO tools write the prompt, SEOAgent runs it.",
5
5
  "type": "module",
6
6
  "bin": {