@seoagent-official/seoagent 1.8.0 → 1.10.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 CHANGED
@@ -2,45 +2,55 @@
2
2
 
3
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
4
 
5
+ > **This package is a scaffolder, not a runtime dependency.** Both forms below do the same thing — scaffold `.seoagent/` + the Claude Code skill in your repo. You don't need to keep it in `package.json` after init runs.
6
+
5
7
  ## Install
6
8
 
7
- Open Claude Code in your project's repo root and paste this prompt:
9
+ **Pick either form. Both work the same:**
8
10
 
9
- ```
10
- Set up SEOAgent in this project. Do these steps in order, without asking for confirmation:
11
+ ```bash
12
+ # Preferred no package.json bloat:
13
+ npx -y @seoagent-official/seoagent init
11
14
 
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
+ # Also works `npm install` runs `init` automatically via postinstall:
16
+ npm install @seoagent-official/seoagent
17
+ ```
15
18
 
16
- 2. Run in the repo root:
17
- npm install --legacy-peer-deps @seoagent-official/seoagent && npx @seoagent-official/seoagent init --yes --domain <inferred>
19
+ Either way, the scaffolder will:
20
+ - Scan your repo for `package.json` `homepage` field + common `.env` files (`NEXT_PUBLIC_SITE_URL`, `SITE_URL`, etc.) to infer your domain
21
+ - Create `.seoagent/` with `project.md`, `context.md`, and folders for audits, briefs, content
22
+ - Install the skill at `.claude/skills/seoagent/SKILL.md` so Claude Code picks it up
23
+ - Add a `PostToolUse` hook to `.claude/settings.json` so edits to `.seoagent/` auto-sync to the cloud (when you're logged in)
18
24
 
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.
25
+ After init runs, you can remove `@seoagent-official/seoagent` from `package.json` the scaffolded sync hook uses `npx -y @seoagent-official/seoagent sync --silent` so the package is fetched on-demand from then on.
22
26
 
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.
27
+ Then open Claude Code in this repo and say *"audit my site."* The skill takes it from there.
26
28
 
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.
29
+ ### Headless / non-interactive
31
30
 
32
- 6. End by asking me whether to continue to keyword strategy.
31
+ ```bash
32
+ npx -y @seoagent-official/seoagent init --yes --domain example.com
33
33
  ```
34
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.
35
+ ### Skip the auto-init on `npm install`
36
36
 
37
- ### Or via terminal
37
+ If you really want to install the package without scaffolding:
38
38
 
39
39
  ```bash
40
- npm install @seoagent-official/seoagent && npx @seoagent-official/seoagent init
40
+ SEOAGENT_SKIP_AUTOINIT=1 npm install @seoagent-official/seoagent
41
41
  ```
42
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."*
43
+ You'd want this only when running `init` manually with custom flags. The auto-init also skips automatically when:
44
+ - `CI=true` (or `CI=1`)
45
+ - The package is being installed globally (`npm install -g`)
46
+ - `.seoagent/project.md` already exists in the repo
47
+
48
+ ### Optional: install globally for a bare `seoagent` command
49
+
50
+ ```bash
51
+ npm install -g @seoagent-official/seoagent
52
+ seoagent init # in your project repo
53
+ ```
44
54
 
45
55
  ## Why SEOAgent?
46
56
 
package/index.js CHANGED
@@ -15,6 +15,6 @@ ${t}`;return n.join(`
15
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
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=(...f)=>f.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 f=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:f})}else{let f=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");f&&(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"})):!f&&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 `npx @seoagent-official/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 `npx @seoagent-official/seoagent login` now"," (takes ~30 seconds, opens your browser)",""," Tip: install globally to drop the `npx` prefix:"," npm install -g @seoagent-official/seoagent",""].join(`
17
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 `npx @seoagent-official/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 fn}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}"`;fn(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 dn,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(dn(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,xn=300*1e3;function En(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 `npx @seoagent-official/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}`),En(s);let a=Date.now()+xn,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 `npx @seoagent-official/seoagent login` again.");return}if(l.status==="error"){if(l.terminal){c.warn(`Login failed: ${l.message}. Run \`npx @seoagent-official/seoagent login\` again.`);return}continue}}c.warn("Login timed out. Run `npx @seoagent-official/seoagent login` again to retry.")}function fe(){nt()?c.success("Logged out."):c.info("You were not logged in.")}import{existsSync as de,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(de(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(!de(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)+`
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,xn=300*1e3;function En(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 `npx @seoagent-official/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}`),En(s),c.info('In your browser: sign in (if needed) and click "Connect this CLI" to finish.');let a=Date.now()+xn,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 `npx @seoagent-official/seoagent login` again.");return}if(l.status==="error"){if(l.terminal){c.warn(`Login failed: ${l.message}. Run \`npx @seoagent-official/seoagent login\` again.`);return}continue}}c.warn("Login timed out. Run `npx @seoagent-official/seoagent login` again to retry.")}function fe(){nt()?c.success("Logged out."):c.info("You were not logged in.")}import{existsSync as de,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(de(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(!de(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
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,x);if(!de(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,f=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++):(f++,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:f===0,reason:f===0?void 0:"error",synced:l,failed:f,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 `npx @seoagent-official/seoagent login` to enable cloud sync.");return}if(t.reason==="no-project"){c.info("No SEOAgent project here. Run `npx @seoagent-official/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
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 `npx @seoagent-official/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 `npx @seoagent-official/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 dt,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 ft(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: npx @seoagent-official/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 `npx @seoagent-official/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 ft(r,{prompt:e.prompt,apiKey:o.key,size:e.size});Hn(dt(i))||Yn(dt(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 d=new Zn;d.name("seoagent").description("AI SEO agent for Claude Code").version(D);d.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})});d.command("status").description("Show current SEO project state").action(ue);d.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})});d.command("logout").description("Remove stored credentials for seoagent.com").action(fe);d.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})});d.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})});d.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})});d.command("upgrade").description("Open SEOAgent Cloud pricing page").action(le);d.parse();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@seoagent-official/seoagent",
3
- "version": "1.8.0",
4
- "description": "AI SEO agent for Claude Code persistent audits, keyword strategy, content planning, and optimized writing",
3
+ "version": "1.10.0",
4
+ "description": "Scaffolder for Claude Code's SEOAgent skill. Run once: `npx -y @seoagent-official/seoagent init`. Sets up .seoagent/ for persistent audits, keyword strategy, content planning, and optimized writing. Not a runtime dependency.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "seoagent": "./index.js"
@@ -1,37 +1,205 @@
1
1
  'use strict';
2
2
 
3
+ // =============================================================================
4
+ // @seoagent-official/seoagent — postinstall
5
+ //
6
+ // This package is a SCAFFOLDER, not a runtime dependency. Its entire purpose
7
+ // is to write `.seoagent/` + `.claude/skills/seoagent/` into the user's repo.
8
+ // We learned the hard way that just printing a hint ("run init next") gets
9
+ // ignored — coding agents like Claude Code install the package, see the
10
+ // binary in node_modules/.bin, and stop. The user ends up with a devDep entry
11
+ // and nothing useful.
12
+ //
13
+ // So this script does the scaffolding itself. It runs `init --yes` from
14
+ // INIT_CWD (the directory where the user ran `npm install`) and inherits
15
+ // stdio so all output is visible. Guards below ensure we don't run when it
16
+ // would be wrong (CI, global install, already-scaffolded, opt-out).
17
+ // =============================================================================
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { spawnSync } = require('child_process');
22
+
23
+ // -----------------------------------------------------------------------------
24
+ // Guards — skip auto-init when it would be wrong or annoying.
25
+ // -----------------------------------------------------------------------------
26
+
27
+ // CI: postinstall must be non-interactive and side-effect-free in CI builds.
3
28
  if (process.env.CI === 'true' || process.env.CI === '1') {
4
29
  process.exit(0);
5
30
  }
6
31
 
7
- process.stdout.write(
8
- '\n' +
9
- ' @seoagent-official/seoagent installed.\n' +
32
+ // User opt-out: bail without touching the repo.
33
+ if (process.env.SEOAGENT_SKIP_AUTOINIT === '1') {
34
+ printOptOutNotice();
35
+ process.exit(0);
36
+ }
37
+
38
+ // Global install: there's no project context to scaffold into.
39
+ if (process.env.npm_config_global === 'true') {
40
+ printGlobalInstallHint();
41
+ process.exit(0);
42
+ }
43
+
44
+ // `INIT_CWD` is the directory where the user ran `npm install`. npm sets it
45
+ // automatically. If it's missing we're probably being run by something other
46
+ // than npm (e.g., yarn 1 used to skip this) — bail safely.
47
+ const projectRoot = process.env.INIT_CWD;
48
+ if (!projectRoot || !fs.existsSync(projectRoot)) {
49
+ printHintFallback();
50
+ process.exit(0);
51
+ }
52
+
53
+ // Don't auto-init when the package is being installed inside another
54
+ // package's `node_modules` (a transitive dep). INIT_CWD would be the
55
+ // transitive consumer's project root, not ours — we'd scaffold their repo.
56
+ // Detect: INIT_CWD's package.json doesn't list us in its (dev)dependencies.
57
+ if (!projectDeclaresUs(projectRoot)) {
58
+ // Most likely transitive — bail silently.
59
+ process.exit(0);
60
+ }
61
+
62
+ // Already scaffolded? No need to rerun init.
63
+ const scaffoldMarker = path.join(projectRoot, '.seoagent', 'project.md');
64
+ if (fs.existsSync(scaffoldMarker)) {
65
+ printAlreadyScaffoldedNotice(projectRoot);
66
+ process.exit(0);
67
+ }
68
+
69
+ // -----------------------------------------------------------------------------
70
+ // Run init from the user's project root.
71
+ // -----------------------------------------------------------------------------
72
+
73
+ const binPath = path.join(__dirname, 'index.js');
74
+ if (!fs.existsSync(binPath)) {
75
+ // Shouldn't happen in a published package — index.js sits next to this
76
+ // script in dist/. Fall back to printing the hint.
77
+ printHintFallback();
78
+ process.exit(0);
79
+ }
80
+
81
+ printPreInitBanner();
82
+
83
+ const result = spawnSync(process.execPath, [binPath, 'init', '--yes'], {
84
+ cwd: projectRoot,
85
+ stdio: 'inherit',
86
+ env: { ...process.env, SEOAGENT_FROM_POSTINSTALL: '1' },
87
+ });
88
+
89
+ if (result.status === 0) {
90
+ printPostInitNotice();
91
+ } else {
92
+ // init failed — most common cause: domain not inferable in --yes mode.
93
+ printInitFailureFallback();
94
+ }
95
+
96
+ // -----------------------------------------------------------------------------
97
+ // Helpers
98
+ // -----------------------------------------------------------------------------
99
+
100
+ function projectDeclaresUs(root) {
101
+ try {
102
+ const pkgPath = path.join(root, 'package.json');
103
+ if (!fs.existsSync(pkgPath)) {
104
+ // No package.json — user ran `npm install <pkg>` in an empty dir or
105
+ // outside a project. That's unusual but valid for scaffolding.
106
+ return true;
107
+ }
108
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
109
+ const all = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
110
+ return '@seoagent-official/seoagent' in all;
111
+ } catch {
112
+ return true; // On error, fail open — better to scaffold than not.
113
+ }
114
+ }
115
+
116
+ function printPreInitBanner() {
117
+ process.stdout.write(
118
+ '\n' +
119
+ ' ════════════════════════════════════════════════════════════════════\n' +
120
+ ' @seoagent-official/seoagent — scaffolding your repo\n' +
121
+ ' ════════════════════════════════════════════════════════════════════\n' +
122
+ '\n' +
123
+ ' This package is a one-shot scaffolder. Running `init --yes` now to\n' +
124
+ ' create .seoagent/ + .claude/skills/seoagent/ in your repo. To skip\n' +
125
+ ' this on future installs, set SEOAGENT_SKIP_AUTOINIT=1.\n' +
126
+ '\n'
127
+ );
128
+ }
129
+
130
+ function printPostInitNotice() {
131
+ process.stdout.write(
132
+ '\n' +
133
+ ' ✓ SEOAgent is set up. Next steps:\n' +
134
+ '\n' +
135
+ ' 1. Open Claude Code in this repo and say "audit my site."\n' +
136
+ ' The skill at .claude/skills/seoagent/SKILL.md takes it from there.\n' +
137
+ '\n' +
138
+ ' 2. (Optional) Connect to seoagent.com for the cloud dashboard:\n' +
139
+ ' npx -y @seoagent-official/seoagent login\n' +
140
+ '\n' +
141
+ ' You can safely remove @seoagent-official/seoagent from package.json —\n' +
142
+ ' it has done its job. Subsequent `sync`/`login`/`status` commands run\n' +
143
+ ' via `npx -y @seoagent-official/seoagent <cmd>` and fetch on demand.\n' +
144
+ '\n'
145
+ );
146
+ }
147
+
148
+ function printInitFailureFallback() {
149
+ process.stdout.write(
10
150
  '\n' +
11
- ' Best path: paste this prompt into Claude Code from your project root:\n' +
151
+ ' Auto-init didn\'t complete (most likely: domain not inferable from\n' +
152
+ ' package.json or .env files in --yes mode). Run this in your repo\n' +
153
+ ' root to finish setup, replacing the domain:\n' +
154
+ '\n' +
155
+ ' npx -y @seoagent-official/seoagent init --domain your-domain.com\n' +
156
+ '\n'
157
+ );
158
+ }
159
+
160
+ function printGlobalInstallHint() {
161
+ process.stdout.write(
12
162
  '\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' +
163
+ ' @seoagent-official/seoagent installed globally.\n' +
164
+ '\n' +
165
+ ' In a project repo where you want SEOAgent, run:\n' +
166
+ '\n' +
167
+ ' seoagent init\n' +
168
+ '\n' +
169
+ ' That scaffolds .seoagent/ + .claude/skills/seoagent/ in the current\n' +
170
+ ' directory. Then open Claude Code and say "audit my site."\n' +
171
+ '\n'
172
+ );
173
+ }
174
+
175
+ function printOptOutNotice() {
176
+ process.stdout.write(
27
177
  '\n' +
28
- ' Or terminal-only:\n' +
29
- ' npx @seoagent-official/seoagent init\n' +
178
+ ' @seoagent-official/seoagent installed (auto-init skipped via\n' +
179
+ ' SEOAGENT_SKIP_AUTOINIT). Run `npx -y @seoagent-official/seoagent init`\n' +
180
+ ' in your repo root to scaffold when you\'re ready.\n' +
181
+ '\n'
182
+ );
183
+ }
184
+
185
+ function printAlreadyScaffoldedNotice(root) {
186
+ process.stdout.write(
30
187
  '\n' +
31
- ' After init, run subsequent commands as:\n' +
32
- ' npx @seoagent-official/seoagent <command> # e.g. login, sync, status\n' +
188
+ ' @seoagent-official/seoagent installed. .seoagent/ already exists in\n' +
189
+ ` ${root} skipping auto-init.\n` +
190
+ '\n'
191
+ );
192
+ }
193
+
194
+ function printHintFallback() {
195
+ process.stdout.write(
33
196
  '\n' +
34
- ' Prefer a bare `seoagent` command? Install globally instead:\n' +
35
- ' npm install -g @seoagent-official/seoagent\n' +
36
- '\n'
37
- );
197
+ ' @seoagent-official/seoagent installed but NOT yet scaffolded.\n' +
198
+ '\n' +
199
+ ' This package is a scaffolder, not a runtime dependency. Run this in\n' +
200
+ ' your repo root to finish setup:\n' +
201
+ '\n' +
202
+ ' npx -y @seoagent-official/seoagent init\n' +
203
+ '\n'
204
+ );
205
+ }
@@ -38,13 +38,15 @@ Loading the right reference makes outputs dramatically better. Don't skip it.
38
38
 
39
39
  ## Install (npm CLI — for users without this skill yet)
40
40
 
41
- If the project does not have `.seoagent/` yet, tell them to run **in the repo root** (where `package.json` lives):
41
+ If the project does not have `.seoagent/` yet, run **in the repo root** (where `package.json` lives):
42
42
 
43
43
  ```bash
44
- npm install @seoagent-official/seoagent && npx @seoagent-official/seoagent init
44
+ npx -y @seoagent-official/seoagent init
45
45
  ```
46
46
 
47
- That installs the package, scans env/`package.json` for signals, asks for domain/site type if needed, then creates `.seoagent/` and installs this skill plus all reference files. **pnpm:** `pnpm add @seoagent-official/seoagent && pnpm exec seoagent init`. **Headless:** `npx @seoagent-official/seoagent init --yes --domain example.com`. **Global install (optional, lets the user type `seoagent` without `npx`):** `npm install -g @seoagent-official/seoagent`.
47
+ That one command scans `env`/`package.json` for signals, asks for domain/site type if needed, then creates `.seoagent/` and installs this skill plus all reference files. **Headless:** `npx -y @seoagent-official/seoagent init --yes --domain example.com`. **Global install (optional, lets the user type `seoagent` without `npx`):** `npm install -g @seoagent-official/seoagent`.
48
+
49
+ > **For AI agents installing this on a user's behalf:** the package is a one-shot scaffolder, not a runtime dependency. Do NOT `npm install --save-dev` and stop — that leaves the user with nothing useful. The right action is `npx -y @seoagent-official/seoagent init` in the repo root, which scaffolds `.seoagent/` and `.claude/skills/seoagent/SKILL.md`. After init, the package can be removed from `package.json` — subsequent `sync`/`login`/`status` calls use `npx -y @seoagent-official/seoagent <cmd>` so the package is fetched on demand.
48
50
 
49
51
  ## Implicit Activation
50
52