@seoagent-official/seoagent 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # SEOAgent
2
+
3
+ **A persistent AI SEO agent for Claude Code.** Audits your site, builds keyword strategy, plans content, and writes optimized articles — all persisted across sessions in `.seoagent/`.
4
+
5
+ ## Install
6
+
7
+ Open Claude Code in your project's repo root and paste this prompt:
8
+
9
+ ```
10
+ Set up SEOAgent in this project. Do these steps in order, without asking for confirmation:
11
+
12
+ 1. Infer my site domain from package.json ("homepage" field) or .env files
13
+ (NEXT_PUBLIC_SITE_URL, SITE_URL, NEXT_PUBLIC_URL, NEXTAUTH_URL). If you
14
+ can't infer it, ask me once.
15
+
16
+ 2. Run in the repo root:
17
+ npm install --legacy-peer-deps @seoagent-official/seoagent && npx @seoagent-official/seoagent init --yes --domain <inferred>
18
+
19
+ 3. Read .claude/skills/seoagent/SKILL.md and follow its "Session Initialization"
20
+ protocol from now on. Use the Edit tool (not Write) to modify project.md,
21
+ context.md, roadmap.md, or any file init already created.
22
+
23
+ 4. Confirm site_type by reading the live homepage. If init wrote
24
+ `site_type: unknown` and the homepage clearly shows a SaaS / e-commerce /
25
+ content site, edit .seoagent/project.md to update it.
26
+
27
+ 5. Run a first technical SEO audit (Phase 1). Read
28
+ .claude/skills/seoagent/references/audit-checks.md FIRST, before any
29
+ WebFetch. Use the operator output template (Biggest Issue → Also Worth
30
+ Fixing → What's Working → numbered next steps) to report findings.
31
+
32
+ 6. End by asking me whether to continue to keyword strategy.
33
+ ```
34
+
35
+ That's it. Claude installs the package, scaffolds `.seoagent/`, installs the skill, runs an audit of your site, and tells you what to fix first. ~30 seconds.
36
+
37
+ ### Or via terminal
38
+
39
+ ```bash
40
+ npm install @seoagent-official/seoagent && npx @seoagent-official/seoagent init
41
+ ```
42
+
43
+ The CLI scans your repo, infers domain + site type, and asks for anything missing. Then open Claude Code and say *"audit my site."*
44
+
45
+ ## Why SEOAgent?
46
+
47
+ If you use [marketingskills](https://github.com/coreyhaines31/marketingskills) for SEO, you know the pain: every session starts from scratch. Your audit findings vanish. Your content strategy disappears. Claude forgets everything between conversations.
48
+
49
+ SEOAgent fixes this with one unified skill that persists all work:
50
+
51
+ | | marketingskills | SEOAgent |
52
+ | -------------- | ---------------------------------------------- | --------------------------------------------------------------- |
53
+ | **State** | Stateless — every session starts over | Persistent `.seoagent/` directory across sessions |
54
+ | **Workflow** | 6 separate skills, no shared context | 1 unified workflow: audit → strategize → plan → write → monitor |
55
+ | **Output** | Unstructured chat text | Structured markdown + frontmatter, machine-readable |
56
+ | **Execution** | Advisory — Claude audits differently each time | Protocols — consistent, comparable results |
57
+ | **Continuity** | None | Roadmap compounds over time, changelog tracks progress |
58
+ | **Page types** | Generic | Dedicated protocols for landing / pillar / sub_pillar / long_tail / programmatic |
59
+
60
+ ## What You Get (Free, No Account)
61
+
62
+ **Technical SEO Audit** — Claude fetches your pages and runs a structured audit: meta tags, headings, internal links, Core Web Vitals, schema readiness, AI search optimization. Findings saved with severity tags to `.seoagent/audit/latest.md` as markdown checkboxes you can flip when fixed.
63
+
64
+ **Hub-and-Spoke Keyword Strategy** — Claude researches your niche with `web_search`, builds clusters with `PILLAR | SUB_PILLAR | LONG_TAIL` roles, internal links funneling authority UP to pillars. Strategy saved to `.seoagent/strategy/clusters/`.
65
+
66
+ **Page-Type-Aware Content Briefs** — Detailed briefs that follow the right protocol for landing pages, pillar articles, sub-pillars, long-tails, or programmatic pages. Each gets its own URL pattern, section structure, JSON-LD schema, and word count target. Saved to `.seoagent/briefs/`.
67
+
68
+ **SEO-Optimized Articles** — Articles written from briefs with full SEO frontmatter: meta_title, meta_description, canonical, OpenGraph, Twitter cards, JSON-LD (Article + FAQPage + HowTo as appropriate), image plans. Saved to `.seoagent/content/`.
69
+
70
+ **Image Generation (Bring Your Own Key)** — Detect `OPENAI_API_KEY`, `FAL_KEY`, or `REPLICATE_API_TOKEN` from your env. Generate hero + inline images with `seoagent generate-image`. You pay the LLM provider directly.
71
+
72
+ **Compounding Roadmap** — Prioritized action plan that updates after every action. Saved to `.seoagent/roadmap.md`. Persistent changelog at `.seoagent/changelog.md`.
73
+
74
+ ## Project Structure
75
+
76
+ ```
77
+ .seoagent/
78
+ project.md # Domain, site type, image_provider
79
+ context.md # Business context, tone, banned topics
80
+ audit/
81
+ latest.md # Most recent audit, findings as [ ] / [x] checkboxes
82
+ strategy/
83
+ discovery.md # Top opportunities + competitor gaps
84
+ clusters/
85
+ {cluster-slug}.md # Article table + hub-and-spoke link graph
86
+ briefs/
87
+ {article-slug}.md # Brief with role, outline, internal links, guidelines
88
+ content/
89
+ {article-slug}.md # Article with full SEO frontmatter + JSON-LD
90
+ images/ # Generated hero + inline images
91
+ pages.md # Sitemap inventory (URL list)
92
+ competitors.md # Competitor profiles + gaps
93
+ keywords.md # Master keyword inventory (assigned + backlog)
94
+ roadmap.md # Prioritized next steps
95
+ changelog.md # History of all SEO work
96
+
97
+ .claude/skills/seoagent/
98
+ SKILL.md # Orchestration layer (459 lines)
99
+ references/
100
+ landing-pages.md # Landing page protocol + JSON-LD
101
+ pillar-articles.md # Pillar article protocol
102
+ sub-pillar-articles.md # Sub-pillar protocol
103
+ long-tail-articles.md # Long-tail protocol
104
+ programmatic.md # 12 programmatic SEO playbooks
105
+ schema-markup.md # JSON-LD library by entity type
106
+ keyword-research.md # WebSearch query patterns
107
+ rewrite-protocol.md # Phase 4b refresh procedure
108
+ audit-checks.md # Full audit check list with severity tiers
109
+ ```
110
+
111
+ ## CLI Commands
112
+
113
+ ```bash
114
+ seoagent init # Create .seoagent/ project + install skill
115
+ seoagent status # Show project state summary
116
+ seoagent login # Connect this CLI to seoagent.com (browser flow)
117
+ seoagent logout # Remove stored credentials
118
+ seoagent sync # Push artifacts to dashboard (no-op when not logged in)
119
+ seoagent env-check # Detect image generation provider (OPENAI/FAL/REPLICATE)
120
+ seoagent generate-image # Generate an image via your provider
121
+ seoagent upgrade # Open SEOAgent Cloud pricing page
122
+ ```
123
+
124
+ ## Auto-Sync Hook
125
+
126
+ `init` writes a `PostToolUse` hook to `.claude/settings.json` so every Write/Edit to `.seoagent/` triggers `seoagent sync` automatically. No-op when not logged in. Merges into existing settings without clobbering them.
127
+
128
+ ## SEOAgent Cloud
129
+
130
+ The free skill handles audits, strategy, briefs, articles, and persistent state using Claude's native capabilities. For teams and production SEO, [SEOAgent Cloud](https://seoagent.com/pricing?ref=readme) adds:
131
+
132
+ - **Real keyword data** — Actual search volumes, difficulty scores, SERP features
133
+ - **Deep crawling** — Firecrawl-powered JS rendering (finds issues in SPAs)
134
+ - **AI-generated images + autopilot** — Hero + inline images created and uploaded to your CMS automatically
135
+ - **GSC integration** — Real clicks, impressions, CTR, position tracking
136
+ - **CMS publishing** — WordPress, Ghost, Webflow, Shopify, Strapi
137
+ - **Team collaboration** — Invite members, share strategy, coordinate publishing
138
+ - **Cloud dashboard** — See everything Claude Code did at seoagent.com (also free with any account)
139
+
140
+ Run `seoagent login` for the free dashboard, or `seoagent upgrade` for paid features.
141
+
142
+ ## Pattern Note
143
+
144
+ Most Claude skills are markdown-only or marketplace plugins. SEOAgent uses **npm + `init`** so setup is versioned and repo-aware. The result is still a project-local skill Claude loads like any other — with the addition of:
145
+
146
+ - A persistent `.seoagent/` workspace for cross-session state
147
+ - A reference library Claude pulls from on demand (page-type protocols, JSON-LD library, etc.)
148
+ - A `PostToolUse` hook for transparent cloud sync
149
+ - Image-generation adapters for OpenAI / fal.ai / Replicate
150
+
151
+ ## License
152
+
153
+ MIT
package/index.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import{Command as Zn}from"commander";import{intro as qt,outro as Zt,text as Ve,select as ie,spinner as Qt,isCancel as w,cancel as b,confirm as en}from"@clack/prompts";import{existsSync as ke,mkdirSync as Ie,readFileSync as Et,writeFileSync as we}from"fs";import{join as ne}from"path";var _=[".env.local",".env.production",".env"],ve=["NEXT_PUBLIC_SITE_URL","SITE_URL","NEXT_PUBLIC_URL","NEXTAUTH_URL","VITE_SITE_URL"];import{homedir as gt}from"os";import{join as L}from"path";var E=".seoagent",_e=["audit","strategy/clusters","briefs","content","content/images","performance"],Z={AGENTS:".agents/skills/seoagent",CLAUDE:".claude/skills/seoagent"},Ee=".claude/settings.json",xe=`# SEOAgent local files
3
+ .legacy/
4
+ content/images/*
5
+ !content/images/.gitkeep
6
+ `,yt=process.env.XDG_CONFIG_HOME&&process.env.XDG_CONFIG_HOME.trim().length>0?process.env.XDG_CONFIG_HOME:L(gt(),".config"),F=L(yt,"seoagent"),g=L(F,"auth.json"),Q=L(F,"state");var R="https://seoagent.com",x=process.env.SEOAGENT_API_BASE&&process.env.SEOAGENT_API_BASE.trim().length>0?process.env.SEOAGENT_API_BASE.replace(/\/$/,""):R,k={BASE:x,PRICING:`${x}/pricing`,LEAD_API:`${x}/api/cli/lead`,CLI_AUTH_PAGE:`${x}/cli/auth`,CLI_AUTH_POLL:`${x}/api/cli/auth/poll`,CLI_SYNC:`${x}/api/cli/sync`};var D="0.2.0";import{existsSync as ht,readFileSync as St}from"fs";var U="---";function vt(e){let t=e.trim();return t===""?"":t==="null"||t==="~"?null:t==="true"?!0:t==="false"?!1:t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'")?t.slice(1,-1):/^-?\d+$/.test(t)||/^-?\d+\.\d+$/.test(t)?Number(t):t}function ee(e){let t=e.split(/\r?\n/);if(t[0]?.trim()!==U)return{data:{},body:e};let n={},r=1;for(;r<t.length;r++){if(t[r].trim()===U){r++;break}let o=t[r],i=o.indexOf(":");if(i===-1)continue;let s=o.slice(0,i).trim(),a=o.slice(i+1);s&&(n[s]=vt(a))}return{data:n,body:t.slice(r).join(`
7
+ `)}}function G(e){if(!ht(e))return null;try{return ee(St(e,"utf-8"))}catch{return null}}function _t(e){if(e===null)return"null";if(typeof e=="boolean")return e?"true":"false";if(typeof e=="number")return String(e);let t=String(e);return t===""||/[:#\[\]{}&*!|>'"%@`,]/.test(t)||/^\s|\s$/.test(t)?`"${t.replace(/"/g,'\\"')}"`:t}function te(e,t=""){let n=[U];for(let[o,i]of Object.entries(e))i!==void 0&&n.push(`${o}: ${_t(i)}`);n.push(U);let r=t.length===0?"":t.startsWith(`
8
+ `)?t:`
9
+ ${t}`;return n.join(`
10
+ `)+r+(r.endsWith(`
11
+ `)?"":`
12
+ `)}var xt=new Set(["strapi","wordpress","sanity","contentful","ghost","webflow","shopify","payload","directus","mdx-local","none"]),kt="project.md";function O(e){return ne(e,E)}function M(e){return ne(O(e),kt)}function be(e){return ke(M(e))}function p(e){let t=G(M(e));if(!t)return null;let n=t.data;if(typeof n.domain!="string"||!n.domain)return null;let r=typeof n.image_provider=="string"?n.image_provider:void 0,o=typeof n.cms=="string"?n.cms:void 0,i=o&&xt.has(o)?o:void 0;return{domain:n.domain,site_type:typeof n.site_type=="string"?n.site_type:"unknown",language:typeof n.language=="string"?n.language:"en",initialized_at:typeof n.initialized_at=="string"?n.initialized_at:"",seoagent_version:typeof n.seoagent_version=="string"?n.seoagent_version:"",image_provider:r==="openai"||r==="fal"||r==="replicate"||r==="none"?r:void 0,cms:i,blog_path:typeof n.blog_path=="string"&&n.blog_path?n.blog_path:void 0}}function It(e){return["",`# SEOAgent Project \u2014 ${e.domain}`,"","This file holds project-level configuration for the SEOAgent skill.","The skill reads it on every session to know which domain it is working on.","",`Initialized ${e.initialized_at} with seoagent ${e.seoagent_version}.`,""].join(`
13
+ `)}function wt(e){let t={domain:e.domain,site_type:e.site_type,language:e.language,initialized_at:e.initialized_at,seoagent_version:e.seoagent_version};return e.image_provider&&(t.image_provider=e.image_provider),e.cms&&(t.cms=e.cms),e.blog_path&&(t.blog_path=e.blog_path),t}function Pe(e,t){let n=O(e);Ie(n,{recursive:!0});let r=te(wt(t),It(t));we(M(e),r,"utf-8")}function Ce(e,t){let n=M(e);if(!ke(n))return!1;let r=ee(Et(n,"utf-8")),o={...r.data,...t};return we(n,te(o,r.body),"utf-8"),!0}function Ae(e){let t=O(e);for(let n of _e)Ie(ne(t,n),{recursive:!0})}import{existsSync as K,mkdirSync as B,writeFileSync as Te,readFileSync as Re,readdirSync as bt,statSync as Pt}from"fs";import{join as m,dirname as Oe,relative as Ct}from"path";import{fileURLToPath as At}from"url";var Tt=Oe(At(import.meta.url));function $e(){return m(Tt,"skills")}function Rt(){let e=m($e(),"seoagent.md");if(K(e))return Re(e,"utf-8");throw new Error(`Could not find skill file at: ${e}`)}function je(e,t){let n=[];if(!K(e))return n;for(let r of bt(e,{withFileTypes:!0})){let o=m(e,r.name),i=m(t,r.name);r.isDirectory()?(B(i,{recursive:!0}),n.push(...je(o,i))):r.isFile()&&(B(Oe(i),{recursive:!0}),Te(i,Re(o)),n.push(i))}return n}function Ne(e,t){return Ot(e,t).skillFile}function Ot(e,t){let n=K(m(e,".agents")),r=m(e,n?Z.AGENTS:Z.CLAUDE),o=m(r,"SKILL.md");B(r,{recursive:!0}),Te(o,t??Rt(),"utf-8");let i=m($e(),"references"),s=m(r,"references"),a=[];try{K(i)&&Pt(i).isDirectory()&&(B(s,{recursive:!0}),a=je(i,s).map(u=>Ct(r,u)))}catch{}return{skillFile:o,referenceFiles:a}}import{existsSync as Le,mkdirSync as re,readFileSync as $t,writeFileSync as z}from"fs";import{dirname as Fe,join as W}from"path";var jt="npx -y @seoagent-official/seoagent sync --silent";function De(e){let t=W(e,E);re(t,{recursive:!0});let n=W(t,".gitignore");z(n,xe,"utf-8");let r=W(t,"content","images",".gitkeep");return re(Fe(r),{recursive:!0}),Le(r)||z(r,"","utf-8"),n}function Nt(e){if(!Le(e))return{};try{let t=$t(e,"utf-8");return JSON.parse(t)}catch{return{}}}function Lt(e){return e.command.includes("@seoagent-official/seoagent")&&e.command.includes("sync")}function Ue(e){let t=W(e,Ee);re(Fe(t),{recursive:!0});let n=Nt(t);n.hooks=n.hooks??{};let r=n.hooks.PostToolUse=n.hooks.PostToolUse??[],o=r.find(i=>(i.matcher??"").includes("Write"));return o||(o={matcher:"Write|Edit",hooks:[]},r.push(o)),o.hooks.some(Lt)?(z(t,JSON.stringify(n,null,2)+`
14
+ `,"utf-8"),{file:t,added:!1}):(o.hooks.push({type:"command",command:jt}),z(t,JSON.stringify(n,null,2)+`
15
+ `,"utf-8"),{file:t,added:!0})}import{readFileSync as Tr,writeFileSync as Ft,existsSync as Rr}from"fs";import{join as Dt}from"path";function Ge(e){let t=[];t.push("# Business Context"),t.push("");let n=[["Name","name"],["Type","type"],["Audience","audience"],["Industry","industry"],["Location","location"],["Description","description"]];for(let[o,i]of n){let s=e.business[i];t.push(`- **${o}:** ${s||""}`)}let r=new Set(n.map(([,o])=>o));for(let[o,i]of Object.entries(e.business))if(!r.has(o)&&i){let s=o.charAt(0).toUpperCase()+o.slice(1);t.push(`- **${s}:** ${i}`)}if(t.push(""),t.push("# Writing Instructions"),t.push(""),e.writingInstructions.length>0)for(let o of e.writingInstructions)t.push(`- ${o}`);else t.push("- (Add your content writing guidelines here)");if(t.push(""),t.push("# Reference URLs"),t.push(""),e.referenceUrls.length>0)for(let o of e.referenceUrls)o.description?t.push(`- ${o.url} \u2014 ${o.description}`):t.push(`- ${o.url}`);else t.push("- (Add URLs the agent should reference for tone/style)");if(t.push(""),t.push("# Topics to Avoid"),t.push(""),e.topicsToAvoid.length>0)for(let o of e.topicsToAvoid)t.push(`- ${o}`);else t.push("- (Add topics the agent should never write about)");return t.push(""),t.push("# Content Tone"),t.push(""),t.push(e.contentTone||"professional"),t.push(""),t.push("# Additional Notes"),t.push(""),t.push(e.additionalNotes||"(Any other context you want the agent to know about your business, products, or audience.)"),t.push(""),t.join(`
16
+ `)}var Ut="context.md";function Gt(e,t){let n=Dt(e,".seoagent",Ut),r=Ge(t);Ft(n,r,"utf-8")}function Me(e,t){Gt(e,{business:{type:t||""},writingInstructions:[],referenceUrls:[],topicsToAvoid:[],contentTone:null,additionalNotes:null})}import{existsSync as $,readFileSync as Ke,readdirSync as Mt,statSync as Be}from"fs";import{join as y}from"path";var Kt=[{cms:"strapi",deps:["strapi","@strapi/strapi","@strapi/"],envKeys:["STRAPI_URL","STRAPI_API_URL","NEXT_PUBLIC_STRAPI_URL","STRAPI_API_TOKEN"],fsMarkers:["strapi/","apps/strapi/","packages/strapi/","cms/"]},{cms:"wordpress",deps:["wpapi","wp-graphql","@wordpress/api-fetch","@wordpress/"],envKeys:["WORDPRESS_API_URL","WP_API_URL","NEXT_PUBLIC_WORDPRESS_URL"],fsMarkers:[]},{cms:"sanity",deps:["@sanity/client","next-sanity","sanity"],envKeys:["SANITY_PROJECT_ID","NEXT_PUBLIC_SANITY_PROJECT_ID","SANITY_API_TOKEN"],fsMarkers:["sanity/","studio/","sanity.config.ts","sanity.config.js"]},{cms:"contentful",deps:["contentful","@contentful/rich-text-react-renderer","@contentful/"],envKeys:["CONTENTFUL_SPACE_ID","CONTENTFUL_ACCESS_TOKEN","NEXT_PUBLIC_CONTENTFUL_SPACE_ID"],fsMarkers:[]},{cms:"ghost",deps:["@tryghost/content-api","@tryghost/"],envKeys:["GHOST_URL","GHOST_API_KEY","NEXT_PUBLIC_GHOST_URL"],fsMarkers:[]},{cms:"webflow",deps:["webflow-api"],envKeys:["WEBFLOW_API_TOKEN","WEBFLOW_SITE_ID"],fsMarkers:[]},{cms:"shopify",deps:["@shopify/hydrogen","@shopify/storefront-api-client","@shopify/shopify-api","@shopify/"],envKeys:["SHOPIFY_STOREFRONT_TOKEN","SHOPIFY_STORE_DOMAIN","SHOPIFY_ADMIN_API_TOKEN"],fsMarkers:["shopify.config.ts","shopify.config.js"]},{cms:"payload",deps:["payload","@payloadcms/next","@payloadcms/"],envKeys:["PAYLOAD_SECRET","PAYLOAD_PUBLIC_SERVER_URL"],fsMarkers:["payload.config.ts","payload.config.js"]},{cms:"directus",deps:["@directus/sdk","directus"],envKeys:["DIRECTUS_URL","DIRECTUS_TOKEN"],fsMarkers:[]}];function Bt(e){let t=y(e,"package.json");if(!$(t))return{deps:{},source:"package.json"};try{let n=JSON.parse(Ke(t,"utf-8"));return{deps:{...n.dependencies,...n.devDependencies,...n.peerDependencies},source:"package.json"}}catch{return{deps:{},source:"package.json"}}}function Wt(e,t){for(let n of t)if(n.endsWith("/")){if(e.startsWith(n))return!0}else if(e===n)return!0;return!1}function zt(e){let t={};for(let n of _){let r=y(e,n);if($(r))try{let o=Ke(r,"utf-8");for(let i of o.split(/\r?\n/)){let s=i.trim();if(!s||s.startsWith("#"))continue;let a=s.indexOf("=");if(a===-1)continue;let u=s.slice(0,a).trim();u&&!(u in t)&&(t[u]=n)}}catch{}}return t}function Ht(e,t){let n=y(e,t.replace(/\/$/,""));if(!$(n))return!1;if(t.endsWith("/"))try{return Be(n).isDirectory()}catch{return!1}return!0}function Yt(e){let t=["content","_posts","posts",y("src","content")];for(let n of t){let r=y(e,n);if($(r))try{if(!Be(r).isDirectory())continue;let o=[r],i=0;for(;o.length>0&&i<50;){let s=o.pop();for(let a of Mt(s,{withFileTypes:!0}))if(i++,!a.name.startsWith(".")){if(a.isDirectory()&&o.length<5){o.push(y(s,a.name));continue}if(a.isFile()&&/\.(md|mdx)$/i.test(a.name))return{type:"directory",detail:n,source:`${n}/${a.name}`}}}}catch{}}return null}function Vt(e){let t=[{marker:"app/blog/page.tsx",path:"/blog"},{marker:"app/blog/page.jsx",path:"/blog"},{marker:"app/blog/page.js",path:"/blog"},{marker:"src/app/blog/page.tsx",path:"/blog"},{marker:"src/app/blog/page.jsx",path:"/blog"},{marker:"pages/blog/index.tsx",path:"/blog"},{marker:"pages/blog/index.jsx",path:"/blog"},{marker:"pages/blog/index.js",path:"/blog"},{marker:"src/pages/blog/index.tsx",path:"/blog"},{marker:"app/(blog)/page.tsx",path:"/blog"},{marker:"app/articles/page.tsx",path:"/articles"},{marker:"pages/articles/index.tsx",path:"/articles"},{marker:"app/posts/page.tsx",path:"/posts"},{marker:"pages/posts/index.tsx",path:"/posts"},{marker:"app/learn/page.tsx",path:"/learn"},{marker:"app/resources/page.tsx",path:"/resources"}];for(let n of t)if($(y(e,n.marker)))return n.path;return null}function We(e){let t=[],{deps:n}=Bt(e),r=zt(e),o="none";for(let s of Kt){let a=[];for(let u of Object.keys(n))if(Wt(u,s.deps)){a.push({type:"dep",detail:u,source:"package.json"});break}for(let u of s.envKeys)if(r[u]){a.push({type:"env",detail:u,source:r[u]});break}for(let u of s.fsMarkers)if(Ht(e,u)){let l=u.endsWith("/")?"directory":"file";a.push({type:l,detail:u,source:u});break}if(a.length>0){o=s.cms,t.push(...a);break}}if(o==="none"){let s=Yt(e);s&&(o="mdx-local",t.push(s))}let i=Vt(e);return{cms:o,blog_path:i,evidence:t}}import{existsSync as h,readFileSync as oe}from"fs";import{join as S}from"path";function ze(e){try{return new URL(e).hostname}catch{return e}}function He(e){let t=e.toLowerCase();return!!(t==="github.com"||t.endsWith(".github.com")||t==="gitlab.com"||t.endsWith(".gitlab.com")||t==="bitbucket.org"||t.endsWith(".bitbucket.org")||t==="dev.azure.com"||t==="visualstudio.com"||t.endsWith(".visualstudio.com")||t==="npmjs.com"||t.endsWith(".npmjs.com"))}function Jt(e){for(let t of _){let n=S(e,t);if(!h(n))continue;let r=oe(n,"utf-8");for(let o of ve){let i=r.match(new RegExp(`^${o}=(.+)$`,"m"));if(i){let s=i[1].trim().replace(/^["']|["']$/g,""),a=ze(s);if(He(a))continue;return{value:a,source:`${t} (${o})`}}}}return null}function Xt(e){let t=S(e,"package.json");if(!h(t))return null;try{let n=JSON.parse(oe(t,"utf-8"));if(!n.homepage)return null;let r=ze(n.homepage);return He(r)?null:{value:r,source:"package.json (homepage)"}}catch{return null}}function Ye(e,t){let n=[];t?.("Checking for monorepo / workspace layout\u2026"),h(S(e,"pnpm-workspace.yaml"))&&n.push({field:"context",detail:"Monorepo workspace detected",source:"pnpm-workspace.yaml \u2014 using this directory for project signals"});let r=null;t?.("Scanning .env files for public / site URLs\u2026");let o=Jt(e);if(o)r=o.value,n.push({field:"domain",detail:o.value,source:o.source});else{t?.("Reading package.json homepage (skipping GitHub/GitLab repo URLs)\u2026");let a=Xt(e);a&&(r=a.value,n.push({field:"domain",detail:a.value,source:a.source}))}let i=S(e,"package.json"),s=null;if(h(i))try{t?.("Reading dependencies to infer site type\u2026");let a=JSON.parse(oe(i,"utf-8")),u=Object.keys({...a.dependencies,...a.devDependencies}),l=(...d)=>d.some(v=>u.includes(v));if(l("@shopify/hydrogen","@shopify/polaris")||h(S(e,"shopify.config.js"))||h(S(e,"shopify.config.ts"))){s="product";let d=l("@shopify/hydrogen","@shopify/polaris")?"Shopify-related dependencies in package.json":h(S(e,"shopify.config.ts"))?"shopify.config.ts":"shopify.config.js";n.push({field:"site_type",detail:"product",source:d})}else{let d=l("stripe","@stripe/stripe-js","@stripe/react-stripe-js","paddle","@paddle/paddle-js"),v=l("next-auth","@auth/core","lucia","clerk","@clerk/nextjs","@clerk/clerk-react"),A=l("next");d&&(A||v)?(s="saas",n.push({field:"site_type",detail:"saas",source:A?"payment SDK + Next.js in package.json":"payment SDK + auth library in package.json"})):!d&&l("astro","gatsby","contentlayer","next-mdx-remote","mdx-bundler","vitepress","vuepress","@docusaurus/core","docusaurus","nextra")&&(s="content",n.push({field:"site_type",detail:"content",source:l("vitepress","vuepress","@docusaurus/core","docusaurus","nextra")?"documentation / site generator in package.json (VitePress, Docusaurus, Nextra, etc.)":"content-oriented dependencies in package.json (no payment SDKs detected)"}))}}catch{}return{domain:r,siteType:s,evidence:n}}import{log as I}from"@clack/prompts";var c={info:e=>I.info(e),warn:e=>I.warn(e),error:e=>I.error(e),success:e=>I.success(e),step:e=>I.step(e),message:e=>I.message(e)};var Je=[{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"}],tn=new Set(["saas","service","product","content","marketplace","tool","app","nonprofit","community","unknown"]);function nn(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 H(e){if(!e?.trim())return null;let t=e.trim().toLowerCase();return tn.has(t)?t:null}async function se(e={}){let t=process.cwd();if(be(t)){c.warn("SEOAgent project already exists in this directory."),c.info("Run `seoagent status` to see your project state.");return}e.yes||qt("SEOAgent \u2014 AI SEO Agent");let n=Ye(t,s=>c.step(s)),r=()=>{if(n.evidence.length!==0){c.info("Inferred from your project:");for(let s of n.evidence)c.info(` \u2022 ${nn(s)}`)}},o=e.domain?.trim()||process.env.SEOAGENT_DOMAIN?.trim()||n.domain||null,i=H(e.siteType)||H(process.env.SEOAGENT_SITE_TYPE)||n.siteType||null;if(e.yes){e.siteType?.trim()&&H(e.siteType)===null&&(c.warn("Invalid --site-type. Use: saas, service, product, content, marketplace, tool, nonprofit, unknown, app, community."),process.exit(1)),process.env.SEOAGENT_SITE_TYPE?.trim()&&H(process.env.SEOAGENT_SITE_TYPE)===null&&(c.warn("Invalid SEOAGENT_SITE_TYPE environment value."),process.exit(1)),r(),o||(c.warn("Domain required in non-interactive mode. Use --domain or set SEOAGENT_DOMAIN, or run without --yes."),process.exit(1)),i||(i="unknown"),await Xe(t,o,i);return}if(r(),!o){let s=await Ve({message:"Website domain",placeholder:"example.com",validate:a=>a.trim()?void 0:"Domain is required"});w(s)&&(b("Cancelled"),process.exit(0)),o=String(s)}if(!i){let s=await ie({message:"What kind of site is this?",options:[...Je]});w(s)&&(b("Cancelled"),process.exit(0)),i=s}for(;;){let s=await en({message:`Create .seoagent for ${o} (${i})?`});if(w(s)&&(b("Cancelled"),process.exit(0)),s)break;let a=await ie({message:"What should we change?",options:[{value:"domain",label:"Domain"},{value:"site_type",label:"Site type"},{value:"abort",label:"Cancel setup"}]});if((w(a)||a==="abort")&&(b("Cancelled"),process.exit(0)),a==="domain"){let u=await Ve({message:"Website domain",placeholder:"example.com",validate:l=>l.trim()?void 0:"Domain is required"});w(u)&&(b("Cancelled"),process.exit(0)),o=String(u)}else{let u=await ie({message:"What kind of site is this?",options:[...Je]});w(u)&&(b("Cancelled"),process.exit(0)),i=u}}await Xe(t,o,i)}async function Xe(e,t,n){let r=Qt();r.start("Setting up your SEO project");let o=We(e),i={domain:t,site_type:n,language:"en",initialized_at:new Date().toISOString(),seoagent_version:D,...o.cms!=="none"?{cms:o.cms}:{},...o.blog_path?{blog_path:o.blog_path}:{}};if(Ae(e),Pe(e,i),Me(e,n),De(e),Ne(e),Ue(e),r.stop(`Created .seoagent/ project for ${i.domain}`),o.cms!=="none"){let s=o.evidence.map(a=>`${a.detail} (${a.source})`).slice(0,2).join(", ");c.info(`CMS detected: ${o.cms} \u2014 ${s}`)}o.blog_path&&c.info(`Blog route: ${o.blog_path}`),Zt([`\u2713 SEOAgent installed for ${i.domain}.`,"","\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510","\u2502 Next: open Claude Code in this directory and paste this prompt: \u2502","\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518","","Read .claude/skills/seoagent/SKILL.md and follow its Session","Initialization protocol. Use Edit (not Write) on existing files.","Confirm site_type from the live homepage. Run a first audit (Phase 1) \u2014","read references/audit-checks.md FIRST. Use the operator output template.","End by asking whether to continue to keyword strategy.","","\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",""," Mirror your work to seoagent.com (free dashboard) \u2014 see your audit,"," roadmap, briefs, and articles in the browser, share with collaborators,"," and unlock real keyword volumes + automated CMS publishing on paid plans.",""," \u2192 Run `seoagent login` now (takes ~30 seconds, opens your browser)",""].join(`
17
+ `))}import{existsSync as j,readdirSync as ae,statSync as rn,readFileSync as qe}from"fs";import{join as P}from"path";function on(e){let t=(e.match(/^\s*-\s+\[\s\]/gm)??[]).length,n=(e.match(/^\s*-\s+\[x\]/gim)??[]).length;return{open:t,done:n}}function sn(e){let t=P(e,"audit","latest.md");if(!j(t))return null;let n=G(t);if(!n)return null;let r=qe(t,"utf-8"),o=on(r);return{exists:!0,date:typeof n.data.audited_at=="string"?n.data.audited_at:void 0,issueCount:o.open}}function an(e){try{let n=qe(e,"utf-8").split(/\r?\n/),r=!1,o=0;for(let i of n){let s=i.trim();if(s.startsWith("|")&&s.endsWith("|")){if(/^\|\s*-+\s*(\|\s*-+\s*)+\|$/.test(s)){r=!0;continue}r&&o++}else if(r&&s==="")break}return o}catch{return 0}}function cn(e){let t=P(e,"strategy","clusters");if(!j(t))return null;let n=ae(t).filter(o=>o.endsWith(".md"));if(n.length===0)return null;let r=0;for(let o of n)r+=an(P(t,o));return{exists:!0,clusterCount:n.length,articleCount:r}}function un(e){let t=P(e,"briefs");if(!j(t))return null;let n=ae(t).filter(r=>r.endsWith(".md"));return n.length>0?{exists:!0,count:n.length}:null}function ln(e){let t=P(e,"content");if(!j(t))return null;let n=ae(t).filter(r=>r.endsWith(".md"));return n.length===0?null:{exists:!0,count:n.length}}function pn(e){let t=P(e,"roadmap.md");return j(t)?{exists:!0,updatedAt:rn(t).mtime.toISOString()}:null}function Ze(e){let t=p(e);if(!t)return null;let n=O(e);return{domain:t.domain,audit:sn(n),strategy:cn(n),briefs:un(n),content:ln(n),roadmap:pn(n)}}function ce(e){let t=Date.now()-new Date(e).getTime(),n=Math.floor(t/6e4);if(n<60)return`${n} minutes ago`;let r=Math.floor(n/60);return r<24?`${r} hours ago`:`${Math.floor(r/24)} days ago`}function ue(){let e=Ze(process.cwd());if(!e){c.warn("No SEOAgent project found in this directory."),c.info("Run `seoagent init` to get started.");return}c.message(`SEOAgent Status \u2014 ${e.domain}`),c.info(e.audit?.exists?`Audit: last run ${ce(e.audit.date??"")} (${e.audit.issueCount??0} issues found)`:"Audit: not yet run"),c.info(e.strategy?.exists?`Strategy: ${e.strategy.clusterCount} topic clusters, ${e.strategy.articleCount} article ideas`:"Strategy: not yet created"),c.info(e.briefs?.exists?`Briefs: ${e.briefs.count} ready`:"Briefs: none created"),e.content?.exists?c.success(`Content: ${e.content.count} articles written`):c.info("Content: no articles yet"),c.info(e.roadmap?.exists?`Roadmap: .seoagent/roadmap.md (updated ${ce(e.roadmap.updatedAt??"")})`:"Roadmap: not yet created")}import{exec as dn}from"child_process";function le(){let e=process.cwd(),n=p(e)?.domain??"",r=`${k.PRICING}?ref=cli${n?`&domain=${encodeURIComponent(n)}`:""}`;c.info("Opening SEOAgent Cloud pricing..."),c.message(`URL: ${r}`);let o=process.platform==="darwin"?`open "${r}"`:process.platform==="win32"?`start "${r}"`:`xdg-open "${r}"`;dn(o,i=>{i&&c.warn("Could not open browser. Visit the URL above to upgrade.")})}import{exec as Sn}from"child_process";import{randomBytes as vn}from"crypto";import{existsSync as et,mkdirSync as Qe,readFileSync as fn,writeFileSync as mn,chmodSync as gn,unlinkSync as yn}from"fs";import{dirname as hn}from"path";function Y(){if(!et(g))return null;try{let e=JSON.parse(fn(g,"utf-8"));return!e.user_token||!e.website_token?null:{user_token:e.user_token,website_token:e.website_token,api_base:e.api_base||R}}catch{return null}}function tt(e){Qe(F,{recursive:!0}),Qe(hn(g),{recursive:!0}),mn(g,JSON.stringify(e,null,2)+`
18
+ `,"utf-8");try{gn(g,384)}catch{}}function nt(){if(!et(g))return!1;try{return yn(g),!0}catch{return!1}}function rt(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;return n.status==="ready"?typeof n.user_token=="string"&&typeof n.website_token=="string"?{status:"ready",user_token:n.user_token,website_token:n.website_token}:{status:"error",terminal:!0,message:"Malformed response: status=ready without user_token+website_token"}:n.status==="pending"?{status:"pending"}:n.status==="expired"?{status:"expired"}:{status:"error",terminal:!0,message:`Malformed response: unknown status=${JSON.stringify(n.status)}`}}var _n=1500,En=300*1e3;function xn(e){let t=process.platform==="darwin"?`open "${e}"`:process.platform==="win32"?`start "" "${e}"`:`xdg-open "${e}"`;Sn(t,()=>{})}function kn(){return vn(16).toString("hex")}async function In(e,t){try{let n=`${e}/api/cli/auth/poll?session=${encodeURIComponent(t)}`,r=await fetch(n,{headers:{Accept:"application/json"}}),o=null;try{o=await r.json()}catch{}return rt(r.status,o)}catch(n){return{status:"error",terminal:!1,message:n.message||"network error"}}}function wn(e){return new Promise(t=>setTimeout(t,e))}async function pe(e={}){let t=process.cwd(),n=p(t),r=(e.apiBase||k.BASE||R).replace(/\/$/,"");if(Y()){c.info("Already logged in. To switch accounts, run `seoagent logout` first.");return}let o=kn(),i=new URLSearchParams({session:o});n?.domain&&i.set("domain",n.domain);let s=`${r}/cli/auth?${i.toString()}`;c.message("Opening seoagent.com to connect this CLI to your account..."),c.info(`If the browser does not open, visit: ${s}`),xn(s);let a=Date.now()+En,u=!0;for(;Date.now()<a;){u||await wn(_n),u=!1;let l=await In(r,o);if(l.status==="ready"){tt({user_token:l.user_token,website_token:l.website_token,api_base:r}),c.success("Logged in. Future SEO work in this repo will sync to your dashboard.");return}if(l.status==="expired"){c.warn("Session expired. Run `seoagent login` again.");return}if(l.status==="error"){if(l.terminal){c.warn(`Login failed: ${l.message}. Run \`seoagent login\` again.`);return}continue}}c.warn("Login timed out. Run `seoagent login` again to retry.")}function de(){nt()?c.success("Logged out."):c.info("You were not logged in.")}import{existsSync as fe,mkdirSync as bn,readFileSync as ot,readdirSync as Pn,statSync as Cn,writeFileSync as An}from"fs";import{join as me,relative as Tn,sep as Rn}from"path";var On=new Set([".md"]);function $n(e){let t=[];function n(r){if(fe(r))for(let o of Pn(r,{withFileTypes:!0})){if(o.name.startsWith("."))continue;let i=me(r,o.name);if(o.isDirectory())n(i);else if(o.isFile()){let s=o.name.lastIndexOf("."),a=s===-1?"":o.name.slice(s);On.has(a)&&t.push(i)}}}return n(e),t}function it(e){let t=e.replace(/[^a-zA-Z0-9._-]/g,"_");return me(Q,`${t}.json`)}function jn(e){let t=it(e);if(!fe(t))return{files:{},last_synced_at:null};try{return JSON.parse(ot(t,"utf-8"))}catch{return{files:{},last_synced_at:null}}}function Nn(e,t){bn(Q,{recursive:!0}),An(it(e),JSON.stringify(t,null,2)+`
19
+ `,"utf-8")}function Ln(e,t){return Tn(e,t).split(Rn).join("/")}async function Fn(e,t,n){try{let r=await fetch(`${e}/api/cli/sync`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:n},body:JSON.stringify(t)});if(!r.ok){let o=await r.text().catch(()=>"");return{ok:!1,status:r.status,error:o.slice(0,200)}}return{ok:!0,status:r.status}}catch(r){return{ok:!1,status:0,error:r.message}}}async function st(e,t={}){let n=Y();if(!n)return{ok:!0,reason:"no-auth",synced:0,failed:0,errors:[]};let r=p(e);if(!r)return{ok:!0,reason:"no-project",synced:0,failed:0,errors:[]};let o=me(e,E);if(!fe(o))return{ok:!0,reason:"no-project",synced:0,failed:0,errors:[]};let i=jn(r.domain),s=$n(o),a=n.api_base||k.BASE,u=`Bearer ${n.user_token}:${n.website_token}`,l=0,d=0,v=[],A=0;for(let J of s){let T=Ln(o,J);if(t.pathFilter&&!T.endsWith(t.pathFilter))continue;let N=Cn(J),X=i.files[T];if(!(!X||X.mtime!==N.mtimeMs||X.size!==N.size)&&!t.force)continue;A++;let mt=ot(J,"utf-8"),q=await Fn(a,{path:T,contents:mt,domain:r.domain},u);q.ok?(i.files[T]={mtime:N.mtimeMs,size:N.size},l++):(d++,v.push(`${T}: ${q.status} ${q.error??""}`.trim()))}return A===0?{ok:!0,reason:"no-changes",synced:0,failed:0,errors:[]}:(i.last_synced_at=new Date().toISOString(),Nn(r.domain,i),{ok:d===0,reason:d===0?void 0:"error",synced:l,failed:d,errors:v})}async function ge(e={}){let t=await st(process.cwd(),{pathFilter:e.path,force:e.force});if(e.silent){t.ok||(process.exitCode=0);return}if(t.reason==="no-auth"){c.info("Not logged in. Run `seoagent login` to enable cloud sync.");return}if(t.reason==="no-project"){c.info("No SEOAgent project here. Run `seoagent init` first.");return}if(t.reason==="no-changes"){c.info("Already in sync.");return}if(t.synced>0&&c.success(`Synced ${t.synced} file${t.synced===1?"":"s"} to your dashboard.`),t.failed>0){c.warn(`${t.failed} file${t.failed===1?"":"s"} failed to sync. Will retry next run.`);for(let n of t.errors.slice(0,3))c.info(` \u2022 ${n}`)}}import{existsSync as Dn,readFileSync as Un}from"fs";import{join as Gn}from"path";var at=["openai","fal","replicate"],C={openai:["OPENAI_API_KEY"],fal:["FAL_KEY","FAL_API_KEY"],replicate:["REPLICATE_API_TOKEN","REPLICATE_API_KEY"]};function Mn(e){let t={};if(!Dn(e))return t;let n=Un(e,"utf-8");for(let r of n.split(/\r?\n/)){let o=r.trim();if(!o||o.startsWith("#"))continue;let i=o.indexOf("=");if(i===-1)continue;let s=o.slice(0,i).trim(),a=o.slice(i+1).trim();(a.startsWith('"')&&a.endsWith('"')||a.startsWith("'")&&a.endsWith("'"))&&(a=a.slice(1,-1)),t[s]=a}return t}function ct(e,t){let n={};for(let r of t){let o=process.env[r];o&&o.trim().length>0&&(n[r]={value:o,source:"process"})}for(let r of _){let o=Gn(e,r),i=Mn(o);for(let s of t){if(n[s])continue;let a=i[s];a&&a.length>0&&(n[s]={value:a,source:r})}}return{found:n}}function V(e){let t=[].concat(...at.map(a=>C[a])),n=ct(e,t),r=[];for(let a of at)C[a].some(u=>n.found[u])&&r.push(a);if(r.length===0)return{provider:"none",matched_key:null,source:null,available_providers:[]};let o=r[0],i=C[o].find(a=>n.found[a])??null,s=i?n.found[i].source:null;return{provider:o,matched_key:i,source:s,available_providers:r}}function ut(e,t){let n=ct(e,C[t]);for(let r of C[t])if(n.found[r])return{key:n.found[r].value,envName:r};return null}function lt(){return C}function ye(e={}){let t=process.cwd(),n=p(t),r=V(t);if(e.json){process.stdout.write(JSON.stringify(r,null,2)+`
20
+ `);return}if(c.message("SEOAgent environment check"),r.provider==="none"){c.info("Image provider: none detected."),c.info("To enable image generation in the free tier, set ONE of:");let o=lt();for(let i of Object.keys(o))c.info(` \u2022 ${i}: ${o[i].join(" or ")}`);c.info("Add to your shell, .env.local, or .env. Then re-run `seoagent env-check`.")}else c.success(`Image provider: ${r.provider} (${r.matched_key} from ${r.source}).`),r.available_providers.length>1&&c.info(`Other providers available: ${r.available_providers.filter(o=>o!==r.provider).join(", ")}.`);if(!n){c.warn("No SEOAgent project here. Run `seoagent init` first to persist the provider.");return}Ce(t,{image_provider:r.provider}),c.info(`Saved to .seoagent/project.md (image_provider: ${r.provider}).`)}import{existsSync as Hn,mkdirSync as Yn,writeFileSync as Vn}from"fs";import{dirname as ft,isAbsolute as Jn,resolve as Xn}from"path";import{Buffer as pt}from"buffer";async function he(e){let t=await fetch(e);if(!t.ok)throw new Error(`Image download failed: ${t.status} ${t.statusText}`);let n=await t.arrayBuffer();return pt.from(n)}async function Kn(e){let t=e.size??"1024x1024",n=await fetch("https://api.openai.com/v1/images/generations",{method:"POST",headers:{Authorization:`Bearer ${e.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify({model:"gpt-image-1",prompt:e.prompt,size:t,n:1,response_format:"b64_json"})});if(!n.ok){let i=await n.text().catch(()=>"");throw new Error(`OpenAI image API ${n.status}: ${i.slice(0,300)}`)}let o=(await n.json()).data?.[0];if(!o)throw new Error("OpenAI image API returned no data");if(o.b64_json)return{bytes:pt.from(o.b64_json,"base64"),contentType:"image/png"};if(o.url)return{bytes:await he(o.url),contentType:"image/png"};throw new Error("OpenAI image API returned neither b64_json nor url")}async function Bn(e){let t=process.env.FAL_MODEL||"fal-ai/flux/schnell",n=await fetch(`https://fal.run/${t}`,{method:"POST",headers:{Authorization:`Key ${e.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify({prompt:e.prompt,image_size:"landscape_16_9",num_images:1})});if(!n.ok){let s=await n.text().catch(()=>"");throw new Error(`fal.ai API ${n.status}: ${s.slice(0,300)}`)}let o=(await n.json()).images?.[0];if(!o?.url)throw new Error("fal.ai API returned no image url");return{bytes:await he(o.url),contentType:o.content_type??"image/png"}}async function Wn(e,t,n=9e4){let r=Date.now(),o=1e3;for(;Date.now()-r<n;){let i=await fetch(`https://api.replicate.com/v1/predictions/${e}`,{headers:{Authorization:`Token ${t}`}});if(!i.ok)throw new Error(`Replicate poll failed: ${i.status}`);let s=await i.json();if(s.status==="succeeded"||s.status==="failed"||s.status==="canceled")return s;await new Promise(a=>setTimeout(a,o)),o=Math.min(o*1.5,5e3)}throw new Error("Replicate prediction timed out")}async function zn(e){let t=process.env.REPLICATE_MODEL||"black-forest-labs/flux-schnell",n=await fetch(`https://api.replicate.com/v1/models/${t}/predictions`,{method:"POST",headers:{Authorization:`Token ${e.apiKey}`,"Content-Type":"application/json",Prefer:"wait"},body:JSON.stringify({input:{prompt:e.prompt,aspect_ratio:"16:9"}})});if(!n.ok){let s=await n.text().catch(()=>"");throw new Error(`Replicate API ${n.status}: ${s.slice(0,300)}`)}let r=await n.json();if(r.status!=="succeeded"&&r.status!=="failed"&&(r=await Wn(r.id,e.apiKey)),r.status!=="succeeded")throw new Error(`Replicate prediction ${r.status}: ${r.error??""}`);let o=Array.isArray(r.output)?r.output[0]:r.output;if(!o)throw new Error("Replicate prediction returned no output");return{bytes:await he(o),contentType:"image/png",providerImageId:r.id}}async function dt(e,t){switch(e){case"openai":return Kn(t);case"fal":return Bn(t);case"replicate":return zn(t);default:throw new Error(`Unknown image provider: ${e}`)}}function qn(e){return e==="openai"||e==="fal"||e==="replicate"}async function Se(e={}){let t=process.cwd();if(!e.prompt){c.warn('Missing --prompt. Example: seoagent generate-image --prompt "..." --out content/images/hero.png'),process.exitCode=1;return}if(!e.out){c.warn("Missing --out. Example: --out .seoagent/content/images/hero.png"),process.exitCode=1;return}let n=p(t),r=null;if(e.provider){if(!qn(e.provider)){c.warn(`Invalid --provider "${e.provider}". Use one of: openai, fal, replicate.`),process.exitCode=1;return}r=e.provider}else n?.image_provider&&n.image_provider!=="none"?r=n.image_provider:r=V(t).provider;if(!r||r==="none"){c.warn("No image generation provider available. Set OPENAI_API_KEY, FAL_KEY, or REPLICATE_API_TOKEN, then run `seoagent env-check`."),process.exitCode=1;return}let o=ut(t,r);if(!o){c.warn(`Provider "${r}" selected but no API key found. Set the env var, then re-run.`),process.exitCode=1;return}let i=Jn(e.out)?e.out:Xn(t,e.out);e.silent||c.info(`Generating image with ${r} (${o.envName})...`);try{let s=await dt(r,{prompt:e.prompt,apiKey:o.key,size:e.size});Hn(ft(i))||Yn(ft(i),{recursive:!0}),Vn(i,s.bytes),e.silent||c.success(`Wrote ${i} (${s.bytes.length} bytes, ${s.contentType}).`)}catch(s){c.warn(`Image generation failed: ${s.message}`),process.exitCode=1}}var f=new Zn;f.name("seoagent").description("AI SEO agent for Claude Code").version(D);f.command("init").description("Initialize SEOAgent project \u2014 creates .seoagent/ and installs the skill file").option("-y, --yes","Non-interactive: use inferred/env/flag values only (requires domain if not inferable)").option("--domain <domain>","Website domain (non-interactive or override)").option("--site-type <type>","Site type: saas, service, product, content, etc.").action(e=>{se({yes:e.yes,domain:e.domain,siteType:e.siteType})});f.command("status").description("Show current SEO project state").action(ue);f.command("login").description("Connect this CLI to your seoagent.com account (browser flow)").option("--api-base <url>","Override API base URL (for testing)").action(e=>{pe({apiBase:e.apiBase})});f.command("logout").description("Remove stored credentials for seoagent.com").action(de);f.command("sync").description("Push .seoagent/ artifacts to your dashboard (no-op when not logged in)").option("--silent","Suppress output (used by the Claude Code hook)").option("--force","Re-sync every artifact regardless of local state cache").option("--path <relpath>","Sync only files matching this path suffix").action(e=>{ge({silent:e.silent,force:e.force,path:e.path})});f.command("env-check").description("Detect which image generation provider is available (OPENAI / FAL / REPLICATE)").option("--json","Output detection result as JSON").action(e=>{ye({json:e.json})});f.command("generate-image").description("Generate an image via the detected (or explicit) provider").requiredOption("--prompt <text>","Image prompt").requiredOption("--out <path>","Output file path (relative to cwd or absolute)").option("--provider <name>","Force provider: openai | fal | replicate").option("--size <wxh>","Pixel size hint, e.g. 1024x1024").option("--silent","Suppress progress output").action(e=>{Se({prompt:e.prompt,out:e.out,provider:e.provider,size:e.size,silent:e.silent})});f.command("upgrade").description("Open SEOAgent Cloud pricing page").action(le);f.parse();
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@seoagent-official/seoagent",
3
+ "version": "1.7.0",
4
+ "description": "AI SEO agent for Claude Code — persistent audits, keyword strategy, content planning, and optimized writing",
5
+ "type": "module",
6
+ "bin": {
7
+ "seoagent": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "skills",
12
+ "postinstall-hint.cjs"
13
+ ],
14
+ "dependencies": {
15
+ "@clack/prompts": "^0.9.0",
16
+ "commander": "^12.0.0"
17
+ },
18
+ "keywords": [
19
+ "seo",
20
+ "claude-code",
21
+ "ai-agent",
22
+ "seo-audit",
23
+ "content-strategy",
24
+ "keyword-research"
25
+ ],
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/Baxter-Inc/seoagent-npm"
30
+ },
31
+ "scripts": {
32
+ "postinstall": "node ./postinstall-hint.cjs"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ if (process.env.CI === 'true' || process.env.CI === '1') {
4
+ process.exit(0);
5
+ }
6
+
7
+ process.stdout.write(
8
+ '\n' +
9
+ ' @seoagent-official/seoagent installed.\n' +
10
+ '\n' +
11
+ ' Best path: paste this prompt into Claude Code from your project root:\n' +
12
+ '\n' +
13
+ ' Set up SEOAgent in this project. Do these steps in order, without\n' +
14
+ ' asking for confirmation:\n' +
15
+ ' 1. Infer my site domain from package.json ("homepage") or .env files\n' +
16
+ ' (NEXT_PUBLIC_SITE_URL, SITE_URL, NEXT_PUBLIC_URL, NEXTAUTH_URL).\n' +
17
+ ' If you cannot infer it, ask me once.\n' +
18
+ ' 2. Run: npm install --legacy-peer-deps @seoagent-official/seoagent && \\\n' +
19
+ ' npx @seoagent-official/seoagent init --yes --domain <inferred>\n' +
20
+ ' 3. Read .claude/skills/seoagent/SKILL.md and follow its Session\n' +
21
+ ' Initialization protocol. Use Edit (not Write) on existing files.\n' +
22
+ ' 4. Confirm site_type from the live homepage; edit project.md if init\n' +
23
+ ' wrote `site_type: unknown` but the homepage shows otherwise.\n' +
24
+ ' 5. Run a first audit (Phase 1). Read references/audit-checks.md\n' +
25
+ ' FIRST, before any WebFetch. Use the operator output template.\n' +
26
+ ' 6. End by asking me whether to continue to keyword strategy.\n' +
27
+ '\n' +
28
+ ' Or terminal-only:\n' +
29
+ ' npx @seoagent-official/seoagent init\n' +
30
+ '\n'
31
+ );
@@ -0,0 +1,228 @@
1
+ # Full Audit Check List
2
+
3
+ Loaded by Phase 1 (Technical SEO Audit) when running a full audit. The main `SKILL.md` summarizes the categories; this reference holds the complete check list with the exact pattern to match and the recommendation text.
4
+
5
+ ## Crawlability & Indexation
6
+
7
+ ### `robots_txt_exists`
8
+ Check: WebFetch `{domain}/robots.txt`. Pass = 200 OK.
9
+ Severity if fail: `high`
10
+ Recommendation: "Create a robots.txt file at {domain}/robots.txt. At minimum: `User-agent: *\nAllow: /\nSitemap: {domain}/sitemap.xml`."
11
+
12
+ ### `robots_txt_blocks_important_paths`
13
+ Check: parse robots.txt for `Disallow:` directives. Match against the URL list in `pages.md`.
14
+ Severity: `critical` if blocks > 5 known important pages; `high` if blocks any important page.
15
+ Recommendation: List the blocking rules and which pages they affect.
16
+
17
+ ### `sitemap_exists`
18
+ Check: WebFetch `{domain}/sitemap.xml`. Pass = 200 OK and parseable XML.
19
+ Severity: `high`
20
+ Recommendation: "Create a sitemap.xml. Include all canonical URLs. Submit to Google Search Console."
21
+
22
+ ### `sitemap_in_robots`
23
+ Check: robots.txt contains a `Sitemap:` line.
24
+ Severity: `medium`
25
+ Recommendation: "Add `Sitemap: {domain}/sitemap.xml` to robots.txt so crawlers find it without GSC submission."
26
+
27
+ ### `noindex_on_important_page`
28
+ Check: page HTML has `<meta name="robots" content="noindex">` or `X-Robots-Tag: noindex` header.
29
+ Severity: `critical` if homepage; `high` otherwise.
30
+ Recommendation: "Remove the `noindex` directive from {file/URL}."
31
+
32
+ ### `canonical_tag_present`
33
+ Check: `<link rel="canonical" href="...">` exists in `<head>`.
34
+ Severity: `medium` if absent; `high` if points to a different domain.
35
+ Recommendation: "Add a self-referencing canonical tag: `<link rel=\"canonical\" href=\"{full URL}\">`."
36
+
37
+ ### `redirect_chain`
38
+ Check: follow redirects, count hops.
39
+ Severity: `medium` if 2 hops; `high` if 3+; `critical` if loop.
40
+ Recommendation: "Resolve {URL} directly to {final URL} with a single 301 redirect."
41
+
42
+ ### `http_to_https_redirect`
43
+ Check: WebFetch `http://{domain}` and confirm 301 to `https://`.
44
+ Severity: `high`
45
+ Recommendation: "Configure a 301 redirect from http:// to https:// at the server / CDN level."
46
+
47
+ ## Render & Upstream Health
48
+
49
+ These checks catch the failure mode where a page returns `200 OK` but the body is empty or broken — Google sees a soft 404, you see a green status code, the audit silently passes. Run these on every page audited.
50
+
51
+ ### `page_renders_empty`
52
+ Check: WebFetch the page, strip `<nav>`, `<footer>`, `<script>`, `<style>`, `<noscript>`, and visually-hidden elements. Count visible body words. Pass = body word count >= 30 AND a `<h1>` or main heading is present.
53
+ Severity: `critical` if the homepage or any sitemap-listed page; `high` for nav-linked pages; `medium` otherwise.
54
+ Recommendation: "Page returns 200 OK but renders empty (likely a client-side data fetch is failing — most often a CMS, API, or proxy the page calls is down). Open the network tab in DevTools or grep the codebase for the URL the page fetches. Until fixed, Google treats this as a soft 404 and will deindex the page. See `upstream_dependency_unreachable` below — they almost always go together."
55
+
56
+ ### `upstream_dependency_unreachable`
57
+ Check: before auditing pages, use `Grep` to find external URLs the site depends on for content. Specifically search for:
58
+ - Cross-subdomain fetch URLs: `https://(blog|api|cms|content|admin|preview)\.{domain}` in `src/`, `app/`, `pages/`, `lib/`, `libs/`, `services/`
59
+ - `rewrites:` and `redirects:` blocks in `next.config.js`, `next.config.mjs`, `next.config.ts`, `vercel.json`
60
+ - Hardcoded API base URLs in `.env*` and `package.json`
61
+
62
+ Then WebFetch each unique base URL (and one representative API path if obvious). Pass = 2xx with non-empty body. Fail = 5xx, timeout, HTML error page, or "no healthy upstream"-style response.
63
+
64
+ Severity: `critical` if the dependency powers indexable pages (blog, docs, listings, product pages); `high` if it powers logged-in flows only; `medium` for purely internal admin tools.
65
+
66
+ Recommendation: "{dependency_url} returned {status}. The pages it powers ({list}) cannot render content — they will surface as `page_renders_empty`. Either restore the dependency, or move the affected pages to a different publishing target (see Publishing Target Decision in SKILL.md). Don't generate any new content for the affected URL pattern until this is resolved — the briefs and articles will all point at a dead address."
67
+
68
+ > **Why this check matters:** the most common SEO disaster on a live site is a CMS that's been quietly down for weeks — every page returns 200, the audit looks clean, but Google has been deindexing the blog the whole time. This check catches it on the first audit.
69
+
70
+ ## On-Page SEO
71
+
72
+ ### `title_missing`
73
+ Check: `<title>` element is empty or absent.
74
+ Severity: `critical`
75
+ Recommendation: "Add a `<title>` tag. Target 50-60 characters with primary keyword near the start."
76
+
77
+ ### `title_too_long`
78
+ Check: title > 60 characters.
79
+ Severity: `high`
80
+ Recommendation: "Shorten title from {N} to 50-60 characters: `{suggested title}`"
81
+
82
+ ### `title_too_short`
83
+ Check: title < 30 characters.
84
+ Severity: `medium`
85
+ Recommendation: "Title is too short to compete. Expand to 50-60 characters with primary keyword."
86
+
87
+ ### `title_duplicate`
88
+ Check: same title across multiple pages in the audit.
89
+ Severity: `high`
90
+ Recommendation: "Pages {list} all use the same title. Make each unique."
91
+
92
+ ### `meta_description_missing`
93
+ Check: `<meta name="description">` absent or empty.
94
+ Severity: `medium`
95
+ Recommendation: "Add a meta description, 150-160 chars, includes primary keyword and a soft CTA."
96
+
97
+ ### `meta_description_too_long`
98
+ Check: meta description > 160 characters.
99
+ Severity: `low`
100
+ Recommendation: "Trim meta description from {N} to 150-160 chars: `{suggested}`."
101
+
102
+ ### `h1_missing`
103
+ Check: no `<h1>` in the page body.
104
+ Severity: `high`
105
+ Recommendation: "Add exactly one `<h1>` containing the primary keyword."
106
+
107
+ ### `h1_multiple`
108
+ Check: more than one `<h1>` in body.
109
+ Severity: `medium`
110
+ Recommendation: "Reduce to one `<h1>`. Demote others to `<h2>` or `<h3>`."
111
+
112
+ ### `heading_hierarchy_skipped`
113
+ Check: H1 → H3 with no H2 between, or H2 → H4 with no H3.
114
+ Severity: `low`
115
+ Recommendation: "Don't skip heading levels. Demote {heading} to {correct level}."
116
+
117
+ ### `url_uppercase`
118
+ Check: URL contains uppercase letters.
119
+ Severity: `low`
120
+ Recommendation: "Use lowercase URLs. Add a lowercase rewrite rule and 301 redirect existing pages."
121
+
122
+ ### `url_underscores`
123
+ Check: URL contains underscores.
124
+ Severity: `low`
125
+ Recommendation: "Replace underscores with hyphens in URL: `{old}` → `{new}` with 301."
126
+
127
+ ## Content Quality
128
+
129
+ ### `word_count_thin`
130
+ Check: page word count < 300 (excluding nav, footer, cookie banners).
131
+ Severity: `medium` for content pages; skip for utility pages (login, 404, error).
132
+ Recommendation: "Add ~{target - current} words of substantive content. Target 800+ for blog posts."
133
+
134
+ ### `keyword_not_in_first_100_words`
135
+ Check: primary keyword (from brief if available, inferred from title otherwise) appears in body before word 100.
136
+ Severity: `medium`
137
+ Recommendation: "Move the primary keyword `{keyword}` into the first paragraph for AI-extractability."
138
+
139
+ ### `images_without_alt`
140
+ Check: `<img>` tags without `alt=""` attribute (decorative images should have empty alt).
141
+ Severity: `medium` if > 3 images; `low` otherwise.
142
+ Recommendation: "Add descriptive alt text to {N} images. SEOAgent can write these — `seoagent.js` does it automatically on production."
143
+
144
+ ### `internal_links_count`
145
+ Check: number of `<a>` tags pointing to same-domain URLs in body.
146
+ Severity: `medium` if 0 (orphan); `low` if < 3.
147
+ Recommendation: "Add internal links to related content. Pillar / sub_pillar pages should link to their cluster siblings."
148
+
149
+ ## Technical Foundations
150
+
151
+ ### `non_https`
152
+ Check: any URL on the site uses `http://`.
153
+ Severity: `critical` if homepage; `high` otherwise.
154
+ Recommendation: "Migrate to HTTPS. Configure a 301 redirect from http:// at the server level."
155
+
156
+ ### `mobile_viewport_missing`
157
+ Check: `<meta name="viewport">` absent.
158
+ Severity: `high`
159
+ Recommendation: "Add `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">` to `<head>`."
160
+
161
+ ### `mixed_content`
162
+ Check: HTTP resources (img, script, css) on an HTTPS page.
163
+ Severity: `medium`
164
+ Recommendation: "Update {N} resource URLs from http:// to https:// or use protocol-relative paths."
165
+
166
+ ### `core_web_vitals_lcp_poor`
167
+ Check: LCP > 2.5s on mobile (via PageSpeed Insights API).
168
+ Severity: `high` if > 4s; `medium` if 2.5-4s.
169
+ Recommendation: "Largest Contentful Paint is {N}s (target <2.5s). Optimize hero image, defer non-critical CSS, server-side render the above-fold content."
170
+
171
+ ### `core_web_vitals_cls_poor`
172
+ Check: CLS > 0.1.
173
+ Severity: `high` if > 0.25; `medium` if 0.1-0.25.
174
+ Recommendation: "Cumulative Layout Shift is {N} (target <0.1). Reserve space for images and ads, avoid injecting content above existing content."
175
+
176
+ ### `core_web_vitals_inp_poor`
177
+ Check: INP > 200ms.
178
+ Severity: `medium`
179
+ Recommendation: "Interaction to Next Paint is {N}ms (target <200ms). Defer JavaScript, avoid long-running event handlers."
180
+
181
+ ## AI Search Readiness
182
+
183
+ ### `ai_bots_blocked`
184
+ Check: robots.txt has `Disallow: /` or `User-agent: GPTBot`/`PerplexityBot`/`ClaudeBot`/`Google-Extended` with `Disallow:`.
185
+ Severity: `medium` (intentional for some sites)
186
+ Recommendation: "Robots.txt blocks AI crawlers ({list}). If you want to be cited by AI search, allow them. If you want to opt out of AI training, the current rules are correct."
187
+
188
+ ### `no_extractable_answer_block`
189
+ Check: page has primary keyword in title/H1 but no paragraph in the first 200 words that directly answers the keyword's intent.
190
+ Severity: `medium`
191
+ Recommendation: "Lead with a definition or direct answer in the first paragraph (40-80 words) so AI search engines can extract it."
192
+
193
+ ### `faq_section_missing_on_pillar`
194
+ Check: page is page_type=pillar but has no `<h2>` containing "FAQ" or "Frequently Asked Questions".
195
+ Severity: `low`
196
+ Recommendation: "Add an FAQ section with 5-7 H3 questions. Powers FAQ schema and AI Overview citations."
197
+
198
+ ## Schema Markup
199
+
200
+ ### `no_json_ld_on_article`
201
+ Check: page is a blog article (URL pattern `/blog/*`) but page HTML has no `<script type="application/ld+json">` Article schema.
202
+ Severity: `medium`
203
+ Recommendation: "Add `Article` JSON-LD with headline, author, datePublished, dateModified, image, publisher.logo. See `references/schema-markup.md`."
204
+
205
+ ### `no_organization_schema_on_homepage`
206
+ Check: homepage has no `Organization` schema.
207
+ Severity: `medium`
208
+ Recommendation: "Add `Organization` JSON-LD on homepage with name, url, logo, sameAs links to social profiles."
209
+
210
+ ### `note_schema_detection_limitation`
211
+ WebFetch strips `<script>` tags including JSON-LD. After auditing, always tell the user: "Test your pages at https://search.google.com/test/rich-results for accurate schema validation — WebFetch can't see your JSON-LD blocks directly."
212
+
213
+ ## Site Architecture
214
+
215
+ ### `orphan_page`
216
+ Check: page in sitemap.xml but no internal link points to it from any other audited page.
217
+ Severity: `high`
218
+ Recommendation: "{N} pages are orphans (no internal links pointing to them). Add internal links from related pages or remove from sitemap."
219
+
220
+ ### `deep_page`
221
+ Check: page > 4 clicks from homepage.
222
+ Severity: `low`
223
+ Recommendation: "Page is {N} clicks deep. Consider promoting it via an internal link from a high-authority page."
224
+
225
+ ### `breadcrumbs_missing`
226
+ Check: deep page (> 2 clicks from home) without `BreadcrumbList` schema or visible breadcrumb nav.
227
+ Severity: `low`
228
+ Recommendation: "Add breadcrumb navigation + BreadcrumbList JSON-LD on deep pages."