@seoagent-official/seoagent 1.89.2 → 1.89.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +2 -1
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1002,7 +1002,8 @@ ${t}
1002
1002
  `,"utf-8");let{cited:$,total:_}=fn(f),U=$===0?N.red:$<_?N.yellow:N.green,R=[{content:N.bold("AI Citation Scorecard")},{sep:!0},{content:`Domain: ${N.cyan(f.domain)}`},{content:`Cited: ${U(`${$} / ${_}`)} ${N.dim("queries")}`}];if(h){let A=h.scoreDelta>0?N.green(`\u25B2 +${h.scoreDelta}`):h.scoreDelta<0?N.red(`\u25BC ${h.scoreDelta}`):N.dim("\u25AA 0");R.push({content:`Trend: ${A} ${N.dim(`vs ${h.prevCited}/${h.prevTotal} on ${h.prevDate}`)}`})}else R.push({content:N.dim("Trend: first run \u2014 re-run to start tracking")});if(R.push({content:`Engine: ${N.dim(kr)}`}),f.competitors.length){R.push({sep:!0}),R.push({content:N.dim("Share of voice:")});for(let A of lo(f)){let T=A.isYou?N.bold(`${A.brand} (you)`):A.brand;R.push({content:` ${T}: ${A.surfaced}/${A.total}`})}}R.push({sep:!0}),R.push({content:N.dim(`Saved \u2192 .seoagent/${so}/${ao}`)}),R.push({content:$<_?N.dim("Not surfacing everywhere \u2014 see the scorecard, then `seoagent okf`."):N.dim("Surfacing across the board. Re-run periodically to catch drift.")}),u.message(M(R,{title:N.magenta("citations")})),e.json&&process.stdout.write(JSON.stringify(f,null,2)+`
1003
1003
  `)}async function Cw(e){let{cwd:t,prompt:n,model:r}=e;try{let i=_w({prompt:n,options:{cwd:t,allowedTools:["WebSearch","WebFetch","Read","Grep","Glob"],permissionMode:"acceptEdits",...r?{model:r}:{}}}),o="";for await(let s of i)if(s.type==="assistant"){let a=s.message.content;if(Array.isArray(a))for(let l of a)Rw(l)}else if(s.type==="result"){if(process.stdout.write(`
1004
1004
  `),!(s.subtype==="success"&&!s.is_error))return u.error(" Agent did not complete the citation audit (auth, rate limit, or network)."),jc(),null;o=String(s.result??"")}return o}catch(i){return process.stdout.write(`
1005
- `),u.error(` Agent error: ${i.message}`),jc(),null}}function jc(){for(let e of bl)u.message(N.dim(` ${e}`))}function Rw(e){if(!e||typeof e!="object")return;let t=e.type;if(t==="text"){let n=e.text;typeof n=="string"&&n&&process.stdout.write(dr(n));return}if(t==="tool_use"){let n=String(e.name??"tool"),r=e.input,i=n==="WebSearch"?String(r?.query??""):n==="WebFetch"?String(r?.url??""):"";process.stdout.write(N.dim(` ${N.magenta("\u{1F50E}")} ${n}${i?` ${Ew(i,70)}`:""}
1005
+ `),u.error(` Agent error: ${i.message}`),jc(),null}}function jc(){u.message(bl.map(e=>N.dim(` ${e}`)).join(`
1006
+ `))}function Rw(e){if(!e||typeof e!="object")return;let t=e.type;if(t==="text"){let n=e.text;typeof n=="string"&&n&&process.stdout.write(dr(n));return}if(t==="tool_use"){let n=String(e.name??"tool"),r=e.input,i=n==="WebSearch"?String(r?.query??""):n==="WebFetch"?String(r?.url??""):"";process.stdout.write(N.dim(` ${N.magenta("\u{1F50E}")} ${n}${i?` ${Ew(i,70)}`:""}
1006
1007
  `))}}function Ew(e,t){return e.length<=t?e:e.slice(0,t-1)+"\u2026"}function Aw(e){return typeof e!="string"?[]:e.split(",").map(t=>t.trim()).filter(Boolean)}function Pw(e){try{if(!Sw(e))return null;let t=xw(e).filter(i=>i.endsWith(".json")).sort();if(t.length===0)return null;let n=t[t.length-1],r=JSON.parse($w(Dc(e,n),"utf-8"));return r&&typeof r=="object"&&Array.isArray(r.results)&&typeof r.generated_at=="string"?r:null}catch{return null}}import At from"picocolors";var Iw=30,Ow=/<(script|style|noscript|template|svg|nav|header|footer)\b[^>]*>[\s\S]*?<\/\1>/gi;function Tw(e){let t=e,n=/<body\b[^>]*>([\s\S]*?)<\/body>/i.exec(t);n&&(t=n[1]),t=t.replace(Ow," ").replace(/<!--[\s\S]*?-->/g," ").replace(/<[^>]+>/g," ").replace(/&nbsp;/gi," ").replace(/&[a-z]+;/gi," ").replace(/\s+/g," ").trim();let r=t===""?0:t.split(" ").filter(Boolean).length;return{wordCount:r,rendered:r<Iw?"empty":"yes"}}async function Lw(e,t){try{let n=await O(e,{headers:{"user-agent":"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) SEOAgent-CLI"},redirect:"follow"},{attempts:2,timeoutMs:1e4,fetchImpl:t});if(!n.ok)return{status:n.status,rendered:"error",wordCount:0};let r=await n.text(),{wordCount:i,rendered:o}=Tw(r);return{status:n.status,rendered:o,wordCount:i}}catch{return{status:null,rendered:"error",wordCount:0}}}async function Fc(e,t={}){let n=Math.max(1,t.concurrency??5),r=new Map,i=[...e],o=0;async function s(){for(;;){let a=i.shift();if(a===void 0)return;r.set(a,await Lw(a,t.fetchImpl)),o+=1,t.onProgress?.(o,e.length)}}return await Promise.all(Array.from({length:Math.min(n,e.length)},()=>s())),r}var co=300;async function gn(e={}){if(e.all){await Nw();return}let t=process.cwd();if(!L(t))return;Ve(t);let n=_i(t);Uc(t);let r=await Mc(t,{enrichSitemap:!0,crawl:e.crawl});if(n.reason==="refreshed"?(u.success(n.from&&n.from!==n.to?`Refreshed the SEOAgent skill ${n.from} \u2192 ${n.to}.`:`Refreshed the SEOAgent skill (now ${n.to}).`),u.info("Your `.seoagent/` knowledge was left untouched.")):(u.error(`Could not refresh the skill (${n.reason}).`),process.exitCode=1),r.added>0?u.success(`Re-scanned the codebase: +${r.added} new page${r.added===1?"":"s"} merged into .seoagent/pages.md (${r.total} total).`):r.total>0&&u.info(`Page inventory already current (${r.total} pages).`),r.crawl){let{crawled:i,empty:o,errors:s}=r.crawl;u.success(`Crawled ${i} page${i===1?"":"s"} \u2192 filled Status / Rendered / Word count`+(o>0?`, ${o} render empty`:"")+(s>0?`, ${s} unreachable`:"")+".")}}async function Mc(e,t={}){try{let n=S(e),r=n?.domain&&n.domain!=="unknown"?n.domain:null,{pages:i}=ge(e,r);t.enrichSitemap&&r&&await Cs(i,r);let o,s;if(t.crawl&&r){let l=i.map(d=>d.url).filter(d=>/^https?:\/\//.test(d)),c=l.slice(0,co);if(l.length>co&&u.info(`Crawling the first ${co} of ${l.length} pages (cap).`),c.length>0){u.info(`Crawling ${c.length} page${c.length===1?"":"s"}\u2026`),s=await Fc(c);let d=0,f=0;for(let p of s.values())p.rendered==="empty"?d++:p.rendered==="error"&&f++;o={crawled:s.size,empty:d,errors:f}}}let a=Bn(e,i,s);return{added:a.added,total:a.total,crawl:o}}catch{return{added:0,total:0}}}async function Nw(){let e=os();if(e.length===0){u.info("No SEOAgent projects are registered on this machine yet. They register themselves on `init` / `sync`.");return}u.message(`Refreshing ${e.length} project${e.length===1?"":"s"} to SEOAgent ${At.bold(I)}\u2026`);let t=0,n=0;for(let r of e){let i=_i(r);Uc(r);let o=await Mc(r),s=o.added>0?At.cyan(` +${o.added} page${o.added===1?"":"s"}`):"";if(i.reason==="refreshed"){t++;let a=i.from&&i.from!==i.to?At.dim(` (${i.from} \u2192 ${i.to})`):"";u.message(` ${At.green("\u2713")} ${r}${a}${s}`)}else n++,u.message(` ${At.yellow("!")} ${r} ${At.dim("\u2014 "+i.reason)}${s}`)}u.success(`Refreshed ${t}/${e.length} project${e.length===1?"":"s"} to ${I}.`),n>0&&u.info(`${n} could not be refreshed (see above).`)}function Uc(e){try{qe(e)}catch{}}import{spinner as po,confirm as jw,isCancel as Dw}from"@clack/prompts";import v from"picocolors";var Hc={easy_win:"easy win",striking_distance:"striking distance",competitor_gap:"competitor gap",defend:"defend",low_priority:"low priority"};async function Fw(e){let t=process.cwd(),n=Nn(t);if(!n){u.error(`No .seoagent/project.md found. Run \`${g("init")}\` first.`),process.exitCode=1;return}let r=S(t),i=r?.domain&&r.domain!=="unknown"?r.domain:void 0,o=(e.apiBase||j.BASE).replace(/\/$/,""),a=!!process.stdout.isTTY&&!e.json?po():null;a?.start(`Looking up ${v.bold(`"${e.keyword}"`)} ${v.dim("via DataForSEO (free peek)\u2026")}`);let l;try{l=await O(`${o}/api/cli/keywords/peek`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({install_id:n,keyword:e.keyword,...i?{domain:i}:{}})})}catch(d){x(a,"error",`Network error: ${d.message}`,v.red("Network error.")),process.exitCode=1;return}let c=null;try{c=await l.json()}catch{c=null}if(e.json){process.stdout.write(JSON.stringify({status:c?.status??"error",http_status:l.status,body:c??null})+`
1007
1008
  `);return}if(l.status===429||c?.skipped?.startsWith("rate-limited")){let d=c?.skipped==="rate-limited:ip"?"this network":"this install";x(a,"warn",`Daily peek quota reached for ${d}.`,v.yellow(`Daily peek quota reached for ${d}.`)),u.message(Bc()),process.exitCode=1;return}if(!l.ok||!c){x(a,"error",`Peek failed (${l.status})${c?.error?`: ${c.error}`:""}`,v.red(`Peek failed (${l.status}).`)),process.exitCode=1;return}if(c.skipped==="provider-quota"){x(a,"warn","Real keyword data is temporarily unavailable. Try again later.",v.yellow("Real keyword data is temporarily unavailable. Try again later.")),u.message(v.dim("No action needed on your side \u2014 the maintainer has been notified."));return}if(c.skipped==="no-creds"||c.skipped==="no-table"){x(a,"warn","Real keyword data isn\u2019t enabled on the server yet \u2014 try again later.",v.yellow("Real keyword data is not enabled on the server yet."));return}if(!c.opportunity){x(a,"warn",`No data returned for "${e.keyword}".`,v.yellow(`No data returned for "${e.keyword}".`));return}a?.stop(v.green("Got it.")),u.message(Mw(c.opportunity)),c.remaining&&u.message(`${v.dim("Peeks remaining today:")} ${v.bold(String(c.remaining.install))} ${v.dim("\u2014 "+Bc())}`)}function Mw(e){let n=c=>v.dim(c.padEnd(14)),r=e.volume!=null?v.bold(String(e.volume)):v.dim("n/a"),i=e.difficulty!=null?`${v.bold(String(e.difficulty))} ${v.dim(Uw(e.difficulty))}`:v.dim("n/a"),o=Hc[e.opportunity]??e.opportunity,s=e.opportunity==="easy_win"?v.bold(v.green(o)):e.opportunity==="striking_distance"?v.bold(v.cyan(o)):e.opportunity==="competitor_gap"?v.bold(v.yellow(o)):v.dim(o),a=[{content:`${v.green("\u25CF")} ${v.bold(e.keyword)}`},{sep:!0},{content:`${n("Volume:")}${r} ${v.dim("/mo")}`},{content:`${n("Difficulty:")}${i}`},{content:`${n("Opportunity:")}${s}`}],l=`${v.bold(v.cyan("Keyword peek"))}`;return M(a,{width:55,title:l})}function Uw(e){return e<=20?"easy":e<=40?"moderate":e<=60?"hard":"very hard"}function Bc(e=D()){return e?`You're logged in \u2014 run \`${g("keywords")}\` for full enrichment (no quota). Pro unlocks the full inventory beyond the ~25 cap plus \`${g("keywords --discover")}\` (new targets) + \`${g("keywords --competitors")}\` (competitor gaps): ${g("upgrade")}.`:`Log in for the full top-25 enrichment, no quota: ${g("login")}`}function uo(e){let t=e.filter(n=>n.opportunity!=="low_priority");if(t.length!==0){u.message("Top opportunities:");for(let n of t){let r=n.volume!=null?`vol ${n.volume}`:"vol n/a",i=n.difficulty!=null?`KD ${n.difficulty}`:"KD n/a",o=n.our_position!=null?`, you're #${n.our_position}`:", not ranking";u.message(` \u2022 [${Hc[n.opportunity]??n.opportunity}] ${n.keyword} \u2014 ${r}, ${i}${o}`)}}}function Bw(e){if(e.length!==0){u.message("Top competitor gaps:");for(let t of e){let n=t.volume!=null?`vol ${t.volume}`:"vol n/a",r=t.difficulty!=null?`KD ${t.difficulty}`:"KD n/a",i=t.their_position!=null?`#${t.their_position}`:"top 10";u.message(` \u2022 ${t.keyword} \u2014 ${t.competitor_domain} ranks ${i}, ${n}, ${r}`)}}}async function Hw(e){let t=e.all?"all":"suggested";if(t==="all"&&process.stdin.isTTY){let a=await jw({message:"Delete ALL keywords for this site from the cloud? This cannot be undone."});if(Dw(a)||!a){u.info("No changes.");return}}let n=e.apiBase||e.auth.api_base||j.BASE,r=process.stdout.isTTY?po():null;r?.start(t==="all"?"Purging all keywords\u2026":"Purging suggested keywords\u2026");let i;try{i=await O(`${n}/api/cli/keywords/purge`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.auth.user_token}:${e.auth.website_token}`},body:JSON.stringify({scope:t})})}catch(a){x(r,"error",`Network error: ${a.message}`,v.red("Network error.")),process.exitCode=1;return}let o=null;try{o=await i.json()}catch{o=null}if(o?.skipped==="no-table"||o?.skipped==="no-columns"){x(r,"warn","Keyword storage isn\u2019t enabled on the server yet.",v.yellow("Nothing to purge yet."));return}if(!i.ok||o?.status!=="ok"){x(r,"error",`Purge failed (${i.status})${o?.error?`: ${o.error}`:""}`,v.red(`Purge failed (${i.status}).`)),process.exitCode=1;return}let s=o.purged??0;x(r,"success",`Purged ${s} ${t==="all"?"":"suggested "}keyword${s===1?"":"s"}.`,v.green(`Purged ${s} keyword${s===1?"":"s"}.`)),u.message(v.dim(t==="all"?"Cleared the cloud inventory. Re-run `seoagent keywords --seed` / strategy to rebuild it.":"Cleared discovery noise. Your clustered keywords were kept."))}async function Ue(e={}){if(e.peek&&e.peek.trim().length>0){await Fw({keyword:e.peek.trim(),apiBase:e.apiBase,json:e.json});return}let t=S(process.cwd()),n=D(t?.domain);if(!n){u.error(`Not logged in. Run \`${g("login")}\` first.`),process.exitCode=1;return}if(e.purge){await Hw({all:e.all,apiBase:e.apiBase,auth:n});return}let r=e.apiBase||n.api_base||j.BASE,i=`Bearer ${n.user_token}:${n.website_token}`,o=e.competitors?"competitors":e.discover?"discover":e.seed?"seed":"enrich",s=o==="competitors"?"/api/cli/keywords/competitors":o==="discover"?"/api/cli/keywords/discover":o==="seed"?"/api/cli/keywords/seed":"/api/cli/keywords/enrich",a=o==="competitors"?"Finding keywords your competitors rank for\u2026":o==="discover"?"Discovering new keywords to target\u2026":o==="seed"?"Seeding keywords from your Search Console data\u2026":"Enriching keywords with real volume + difficulty\u2026",c=!!process.stdout.isTTY?po():null;c?.start(a);let d=o==="competitors"||o==="discover"||o==="seed",f;try{f=await O(`${r}${s}`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:i},body:"{}"},d?{timeoutMs:6e4,attempts:1}:{})}catch(y){x(c,"error",`Network error: ${y.message}`,v.red("Network error.")),process.exitCode=1;return}let p=null;try{p=await f.json()}catch{p=null}if(f.status===402||p?.code==="upgrade_required"){x(c,"warn",o==="competitors"?"Competitor gap analysis is a paid feature.":"Keyword discovery is a paid feature.",v.yellow("Paid feature \u2014 upgrade to continue.")),u.message(o==="competitors"?`Upgrade to see which keywords competitors rank for that you don't: ${v.cyan(fe("cli-keywords-competitors"))}`:`Upgrade to find new keywords to target: ${v.cyan(fe("cli-keywords-discover"))}`),process.exitCode=1;return}if(!f.ok||p?.status!=="ok"){x(c,"error",`Server rejected request (${f.status})${p?.error?`: ${p.error}`:""}`,v.red(`Server rejected request (${f.status}).`)),process.exitCode=1;return}if(p.skipped==="no-gsc-data"){x(c,"warn","No Search Console data to seed from yet. Connect Google Search Console (run `seoagent login` and link GSC) \u2014 or wait for impressions to accrue on a new site.",v.yellow("No Search Console data yet."));return}if(p.skipped==="no-keywords"||p.skipped==="no-seeds"){x(c,"warn","No keywords yet. Seed from Search Console with `seoagent keywords --seed`, or run the keyword strategy (Phase 2) first, then try again.",v.yellow("No keywords yet."));return}if(p.skipped==="no-competitors"){x(c,"warn","No competitors found yet. Add competitors or run strategy discovery first, then try again.",v.yellow("No competitors yet."));return}if(p.skipped==="no-creds"||p.skipped==="no-columns"||p.skipped==="no-table"){x(c,"warn","Real keyword data isn\u2019t enabled on the server yet \u2014 try again later.",v.yellow("Real keyword data is not enabled on the server yet."));return}if(p.skipped){x(c,"warn",`Skipped: ${p.skipped}`,v.yellow(`Skipped: ${p.skipped}`));return}if(o==="competitors"){let y=p.gaps??0;x(c,"success",`Found ${y} competitor gap${y===1?"":"s"} across ${p.competitors?.length??0} competitor${p.competitors?.length===1?"":"s"} (added as "suggested").`,v.green(`${y} competitor gap${y===1?"":"s"} found.`)),Bw(p.topGaps??[]),u.message(`Review them in ${v.cyan(".seoagent/keywords.md")} (a read-only projection \u2014 don't hand-edit it). Keep the on-topic ones for your clusters; clear off-topic noise with \`${g("keywords --purge")}\`. The analyzed competitors are saved to your dashboard's Competitors section.`);return}if(o==="discover"){let y=p.discovered??0;x(c,"success",`Discovered ${y} new keyword${y===1?"":"s"} to target (added as "suggested").`,v.green(`${y} new keyword${y===1?"":"s"} discovered.`)),uo(p.opportunities??[]),u.message(`Review them in ${v.cyan(".seoagent/keywords.md")} (a read-only projection \u2014 don't hand-edit it). Relevance-check before adopting; clear off-topic noise with \`${g("keywords --purge")}\`.`);return}if(o==="seed"){let y=p.seeded??0;x(c,"success",`Seeded ${y} keyword${y===1?"":"s"} from Search Console (real impressed queries, added as "suggested").`,v.green(`${y} keyword${y===1?"":"s"} seeded from Search Console.`)),uo(p.opportunities??[]),u.message(`These are queries your site already gets impressions for \u2014 striking-distance wins first. Review in ${v.cyan(".seoagent/keywords.md")} and build clusters from them.`);return}let m=p.enriched??0;x(c,"success",`Enriched ${m} keyword${m===1?"":"s"}.`,v.green(`Enriched ${m} keyword${m===1?"":"s"}.`)),uo(p.opportunities??[]),p.paid||u.message(`${v.dim("Free plan:")} enriched up to ${v.bold(String(p.cap??25))} keywords. ${v.dim("Upgrade for full enrichment + discovery + competitor gap:")} ${v.cyan(fe("cli-keywords-cap"))}`)}import B from"picocolors";function hn(e={}){let t=process.cwd(),n=S(t),r=Ne(t);if(e.json){process.stdout.write(JSON.stringify(r,null,2)+`
1008
1009
  `);return}if(u.message(Ww(r)),!n){u.warn(`No SEOAgent project here. Run \`${g("init")}\` first to persist the provider.`);return}V(t,{image_provider:r.provider}),u.info(`Saved to ${B.cyan(".seoagent/project.md")} ${B.dim(`(image_provider: ${r.provider})`)}`)}function Ww(e){let n=o=>B.dim(o.padEnd(12)),r=[];if(e.provider==="none"){r.push({content:`${B.dim("\u25CB")} ${B.bold(B.yellow("No image provider detected"))}`}),r.push({sep:!0}),r.push({content:B.dim("Set ONE of these to enable image generation:")});let o=ws();for(let s of Object.keys(o))r.push({content:`${n(s+":")}${B.cyan(o[s].join(" or "))}`});r.push({sep:!0}),r.push({content:B.dim("Add to your shell, .env.local, or .env, then re-run:")}),r.push({content:` ${B.cyan(g("env-check"))}`})}else{r.push({content:`${B.green("\u2713")} ${B.dim("Image provider:")} ${B.bold(B.green(e.provider))}`}),r.push({sep:!0}),e.matched_key&&r.push({content:`${n("Key:")}${B.bold(e.matched_key)}`}),e.source&&r.push({content:`${n("Source:")}${B.cyan(e.source)}`});let o=e.available_providers.filter(s=>s!==e.provider);o.length>0&&r.push({content:`${n("Available:")}${B.dim(o.join(", "))}`})}let i=`${B.bold(B.cyan("SEOAgent"))} ${B.dim("\xB7")} ${B.dim("env-check")}`;return M(r,{width:64,title:i})}import{existsSync as go,mkdirSync as Kc,writeFileSync as qc}from"fs";import{dirname as Sr,isAbsolute as Jw,join as xr,resolve as Xw}from"path";import{tmpdir as Qw}from"os";import{spinner as Zw}from"@clack/prompts";import q from"picocolors";import{Buffer as Wc}from"buffer";var Gw=6e4,fo=18e4,Kw=15e3;async function mo(e){let t=await fetch(e,{signal:AbortSignal.timeout(Gw)});if(!t.ok)throw new Error(`Image download failed: ${t.status} ${t.statusText}`);let n=await t.arrayBuffer();return Wc.from(n)}async function qw(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}),signal:AbortSignal.timeout(fo)});if(!n.ok){let o=await n.text().catch(()=>"");throw new Error(`OpenAI image API ${n.status}: ${o.slice(0,300)}`)}let i=(await n.json()).data?.[0];if(!i)throw new Error("OpenAI image API returned no data");if(i.b64_json)return{bytes:Wc.from(i.b64_json,"base64"),contentType:"image/png"};if(i.url)return{bytes:await mo(i.url),contentType:"image/png"};throw new Error("OpenAI image API returned neither b64_json nor url")}async function Vw(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}),signal:AbortSignal.timeout(fo)});if(!n.ok){let s=await n.text().catch(()=>"");throw new Error(`fal.ai API ${n.status}: ${s.slice(0,300)}`)}let i=(await n.json()).images?.[0];if(!i?.url)throw new Error("fal.ai API returned no image url");return{bytes:await mo(i.url),contentType:i.content_type??"image/png"}}async function Yw(e,t,n=9e4){let r=Date.now(),i=1e3;for(;Date.now()-r<n;){let o=await fetch(`https://api.replicate.com/v1/predictions/${e}`,{headers:{Authorization:`Token ${t}`},signal:AbortSignal.timeout(Kw)});if(!o.ok)throw new Error(`Replicate poll failed: ${o.status}`);let s=await o.json();if(s.status==="succeeded"||s.status==="failed"||s.status==="canceled")return s;await new Promise(a=>setTimeout(a,i)),i=Math.min(i*1.5,5e3)}throw new Error("Replicate prediction timed out")}async function zw(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"}}),signal:AbortSignal.timeout(fo)});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 Yw(r.id,e.apiKey)),r.status!=="succeeded")throw new Error(`Replicate prediction ${r.status}: ${r.error??""}`);let i=Array.isArray(r.output)?r.output[0]:r.output;if(!i)throw new Error("Replicate prediction returned no output");return{bytes:await mo(i),contentType:"image/png",providerImageId:r.id}}async function Gc(e,t){switch(e){case"openai":return qw(t);case"fal":return Vw(t);case"replicate":return zw(t);default:throw new Error(`Unknown image provider: ${e}`)}}function eb(e){return e==="openai"||e==="fal"||e==="replicate"}async function yn(e={}){let t=process.cwd();if(!e.prompt){u.warn(`Missing --prompt. Example: ${g('generate-image --prompt "..." --out content/images/hero.png')}`),process.exitCode=1;return}if(!e.out){u.warn("Missing --out. Example: --out .seoagent/content/images/hero.png"),process.exitCode=1;return}let n=S(t),r=null;if(e.provider){if(!eb(e.provider)){u.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=Ne(t).provider;if(!r||r==="none"){u.warn(`No image generation provider available. Set OPENAI_API_KEY, FAL_KEY, or REPLICATE_API_TOKEN, then run \`${g("env-check")}\`.`),process.exitCode=1;return}let i=ys(t,r);if(!i){u.warn(`Provider "${r}" selected but no API key found. Set the env var, then re-run.`),process.exitCode=1;return}let o=Jw(e.out)?e.out:Xw(t,e.out),a=!e.silent&&!!process.stdout.isTTY?Zw():null;a?.start(`Generating image with ${q.bold(r)} ${q.dim(`(${i.envName})`)}\u2026`);try{let l=await Gc(r,{prompt:e.prompt,apiKey:i.key,size:e.size});go(Sr(o))||Kc(Sr(o),{recursive:!0}),qc(o,l.bytes),e.silent||x(a,"success",`Wrote ${o} (${l.bytes.length} bytes, ${l.contentType}).`,q.green(`Wrote ${o} ${q.dim(`(${l.bytes.length} bytes, ${l.contentType})`)}`))}catch(l){a?.stop(q.red("Image generation failed."));let c=nb(t,{provider:r,envName:i.envName,prompt:e.prompt,size:e.size,outPath:o,error:l});u.message(tb({provider:r,envName:i.envName,prompt:e.prompt,size:e.size,message:l.message})),c&&u.message(`${q.dim("Full error log:")} ${q.cyan(c)}`),process.exitCode=1}}function tb(e){let n=d=>q.dim(d.padEnd(10)),i=e.prompt.length>54?`${e.prompt.slice(0,53)}\u2026`:e.prompt,o=[{content:`${q.red("\u2717")} ${q.bold(q.red("Image generation failed"))}`},{sep:!0},{content:`${n("Provider:")}${q.bold(e.provider)}`},{content:`${n("Key:")}${e.envName}`},{content:`${n("Prompt:")}${q.dim(`"${i}"`)}`}];e.size&&o.push({content:`${n("Size:")}${e.size}`}),o.push({sep:!0});let s=e.message.replace(/\s+/g," ").trim(),a=64,l=s.length>a?`${s.slice(0,a-1)}\u2026`:s;o.push({content:q.red(l)});let c=`${q.bold(q.cyan("SEOAgent"))} ${q.dim("\xB7")} ${q.dim("generate-image")}`;return M(o,{width:70,title:c})}function nb(e,t){let r=`seoagent-generate-image-${new Date().toISOString().replace(/[:.]/g,"-")}.log`,i=xr(e,b),o=xr(i,".errors"),s=go(i)?xr(o,r):xr(Qw(),r),a=["# SEOAgent generate-image error",`# ${new Date().toISOString()}`,"","## Request",`provider: ${t.provider}`,`env_var: ${t.envName}`,`prompt: ${JSON.stringify(t.prompt)}`,`size: ${t.size??"(default)"}`,`out_path: ${t.outPath}`,`cwd: ${e}`,"","## Error",`message: ${t.error.message}`,"","## Stack",t.error.stack??"(no stack)",""];t.error.cause&&a.push("## Cause",String(t.error.cause),"");try{return go(Sr(s))||Kc(Sr(s),{recursive:!0}),qc(s,a.join(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seoagent-official/seoagent",
3
- "version": "1.89.2",
3
+ "version": "1.89.3",
4
4
  "description": "The persistent AI SEO agent for Claude Code. Audits, keyword strategy, briefs, articles, real product screenshots from your repo, and the autopilot loop (cloud detects → CLI executes → ack closes) — other SEO tools write the prompt, SEOAgent runs it.",
5
5
  "type": "module",
6
6
  "bin": {