@serpel/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/THIRD_PARTY_LICENSES.txt +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,13 +5,13 @@ Track Google rankings, audit your site, read Search Console and Bing data and ch
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install -g serpel
|
|
8
|
+
npm install -g @serpel/cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Requires Node.js 20.9 or newer. Run it once without installing:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx serpel --help
|
|
14
|
+
npx @serpel/cli --help
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Sign in
|
package/dist/index.js
CHANGED
|
@@ -98,7 +98,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
98
98
|
`)}heading(t){this.line(this.c.bold(t))}fields(t){let r=t.filter(([,n])=>n!=null&&n!==""),i=Math.max(0,...r.map(([n])=>n.length));for(let[n,o]of r)this.line(`${this.c.dim(`${n}:`.padEnd(i+1))} ${o}`)}table(t,r){let i=r.map(s=>t.map(c=>vR(yR(c.value(s)),c.max??60))),n=t.map((s,c)=>Math.max(s.header.length,...i.map(l=>(l[c]??"").length))),o=(s,c)=>t[c]?.align==="right"?s.padStart(n[c]??0):s.padEnd(n[c]??0),a=t.map((s,c)=>o(s.header,c)).join(" ");this.line(this.c.bold(a.trimEnd())),r.forEach((s,c)=>{let l=t.map((u,d)=>{let m=o(i[c]?.[d]??"",d);return u.color?u.color(m,s):m}).join(" ");this.line(l.trimEnd())})}paginationHint(t,r,i){let{offset:n,total:o,limit:a}=t;if(o===0||r===0||n===0&&r>=o)return;let s=n+1,c=n+r,l=`Showing ${s} to ${c} of ${o} entries.`;if(c<o){let u=i!==void 0?i+1:Math.floor(n/Math.max(1,a))+2;l+=` Use --page ${u} or --offset ${c} for more entries.`}this.hint(l)}};function yR(e){return e.replace(/[\r\n\t]+/g," ")}function vR(e,t){return e.length<=t?e:`${e.slice(0,Math.max(0,t-3))}...`}import Ff from"node:fs";import bR from"node:path";var Jf="serpel",nt="SERPEL_TOKEN";function hc(e){let t=(e??"").trim().toLowerCase();if(t===""||t==="auto")return"auto";if(t==="keyring"||t==="file")return t;throw new z("usage_error",`Invalid value for SERPEL_TOKEN_STORAGE: "${e}". Allowed values are auto, keyring and file.`,P.usage)}function wR(e){return e==="win32"?"Windows Credential Manager":e==="darwin"?"macOS Keychain":"System keyring (Secret Service)"}var fc=class{constructor(t){this.options=t;this.credentialsFile=bR.join(t.configDir,"credentials.json")}options;credentialsFile;keyringPromise=null;describe(t,r){return t==="keyring"?`${wR(this.options.platform)} (service ${Jf}, account ${r})`:this.credentialsFile}entry(t,r){return this.options.platform==="linux"?new t.Entry(Jf,r,{linux:{store:"secret-service"}}):new t.Entry(Jf,r)}keyring(){return this.options.mode==="file"?Promise.resolve(null):(this.keyringPromise??=this.options.loadKeyring().catch(()=>null),this.keyringPromise)}async save(t,r){let i="the @napi-rs/keyring package is not installed or not available for this platform",n=await this.keyring();if(n)try{let o=this.entry(n,t);if(o.setPassword(r),o.getPassword()!==r)throw new Error("read-back failed");return this.removeFromFile(t),{storage:"keyring",location:this.describe("keyring",t)}}catch(o){i=`access failed: ${kR(o)}`}if(this.options.mode==="keyring")throw new z("keyring_unavailable",`The keyring cannot be used (${i}).`,P.unexpected,{hint:"SERPEL_TOKEN_STORAGE=file allows storing the token in a file."});return this.options.mode==="auto"&&this.options.warn(`The system keyring is not available (${i}). The token is stored in ${this.credentialsFile} instead. The file is readable only by your user account; anyone who can read it anyway can access the API on your behalf.`),this.writeToFile(t,r),{storage:"file",location:this.credentialsFile}}async load(t,r){let i=r==="file"?["file","keyring"]:["keyring","file"];for(let n of i)if(n==="keyring"){let o=await this.keyring();if(!o)continue;try{let a=this.entry(o,t).getPassword();if(a)return{token:a,source:"keyring",location:this.describe("keyring",t)}}catch{}}else{let o=this.readFile()?.tokens[t];if(o)return{token:o,source:"file",location:this.credentialsFile}}return null}async remove(t){let r=!1,i=await this.keyring();if(i)try{r=this.entry(i,t).deletePassword()}catch{r=!1}let n=this.removeFromFile(t);return{keyring:r,file:n}}readFile(){let t;try{t=Ff.readFileSync(this.credentialsFile,"utf8")}catch{return null}if(this.options.platform!=="win32")try{let r=Ff.statSync(this.credentialsFile).mode&511;r&63&&this.options.warn(`${this.credentialsFile} is readable by other users (permissions ${r.toString(8)}). Please restrict it with "chmod 600".`)}catch{}try{let r=JSON.parse(t),i={};for(let[n,o]of Object.entries(r.tokens??{}))typeof o=="string"&&o&&(i[n]=o);return{version:1,tokens:i}}catch{return null}}writeToFile(t,r){let i=this.readFile()??{version:1,tokens:{}};i.tokens[t]=r,dc(this.credentialsFile,`${JSON.stringify(i,null,2)}
|
|
99
99
|
`,384)}removeFromFile(t){let r=this.readFile();return!r||!(t in r.tokens)?!1:(delete r.tokens[t],Object.keys(r.tokens).length===0?Ff.rmSync(this.credentialsFile,{force:!0}):dc(this.credentialsFile,`${JSON.stringify(r,null,2)}
|
|
100
100
|
`,384),!0)}};async function ak(e,t,r,i){let n=e[nt]?.trim();return n?{token:n,source:"env",location:`Environment variable ${nt}`}:t.load(r,i)}function kR(e){return(e instanceof Error?e.message:String(e)).split(`
|
|
101
|
-
`)[0]?.slice(0,200)??"unknown error"}var Qr="0.1.0";var sk=null;function Zf(e){sk=e}function ck(){return sk}var _R=new Set(["succeeded","failed","cancelled"]),dk=2e3,xR=3;async function pk(e,t,r){let i=r.now(),n=0;Zf(`Waiting cancelled. The job keeps running, check its status with: serpel jobs status ${t}`);try{for(;;){let o;try{o=await e.get(`/jobs/${encodeURIComponent(t)}`),n=0}catch(c){if(c instanceof z&&c.exitCode===P.unreachable&&n<xR){n+=1,await r.sleep(r.intervalMs*n);continue}throw c}let a=r.now()-i;if(r.onUpdate?.(o,a),_R.has(o.status))return o;if(r.timeoutMs!==void 0&&a>=r.timeoutMs)throw new z("wait_timeout",`Time limit of ${Math.round(r.timeoutMs/1e3)} s reached. Job ${t} has the status "${We[o.status]??o.status}" and keeps running.`,P.waitTimeout,{hint:`Check the status later: serpel jobs status ${t}`,details:{jobId:t,status:o.status}});let s=r.timeoutMs!==void 0?Math.max(0,r.timeoutMs-a):r.intervalMs;await r.sleep(Math.min(r.intervalMs,Math.max(s,10)))}}finally{Zf(null)}}function rt(e){if(e.status==="succeeded")return;let t=_t[e.type]??e.type;if(e.status==="cancelled")throw new z("job_cancelled",`${t} was cancelled (job ${e.id}).`,P.jobFailed,{details:{jobId:e.id,status:e.status}});let r=e.lastError?`: ${e.lastError}`:".";throw new z("job_failed",`${t} failed (job ${e.id})${r}`,P.jobFailed,{hint:`Details: serpel jobs status ${e.id}`,details:{jobId:e.id,status:e.status,attempts:e.attempts}})}function mk(e){let t=Math.floor(e/1e3),r=Math.floor(t/60),i=t%60;return`${String(r).padStart(2,"0")}:${String(i).padStart(2,"0")}`}function fk(e){let t=_t[e.type]??e.type,r=We[e.status]??e.status,i=`${t}: ${r}`;if(e.progress){let n=e.progress.total!==null?`/${e.progress.total}`:"";i+=` ${e.progress.current}${n}`,e.progress.message&&(i+=` (${e.progress.message})`)}return i}var lk="\x1B",uk=["-","\\","|","/"],gc=class{constructor(t){this.stream=t}stream;frame=0;active=!1;get enabled(){return!!this.stream.isTTY}update(t){if(!this.enabled)return;let r=uk[this.frame++%uk.length],i=Math.max(20,(this.stream.columns??80)-1),n=`${r} ${t}`.slice(0,i);this.stream.write(`\r${lk}[2K${n}`),this.active=!0}clear(){this.active&&(this.stream.write(`\r${lk}[2K`),this.active=!1)}};function hk(e,t,r,i){let n=e[t]?.trim();if(!n)return r;let o=Number(n);if(!Number.isFinite(o)||o<i)throw E(`Invalid value for ${t}: "${n}".`);return o}var yc=class{constructor(t,r,i=new Set){this.runtime=t;this.options=r;this.out=new Yr(t.stdout,t.stderr,t.env,{json:!!r.json,color:r.color!==!1,verbose:!!r.verbose},i);let n=tk(t.env,t.platform,t.homedir);this.config=new uc(n),this.tokens=new fc({configDir:n,mode:hc(t.env.SERPEL_TOKEN_STORAGE),platform:t.platform,loadKeyring:t.loadKeyring,warn:o=>this.out.warn(o)})}runtime;options;out;config;tokens;apiUrlCache=null;get json(){return this.out.json}apiUrl(){return this.apiUrlCache??=ek(this.options.apiUrl,this.runtime.env,this.config.load().apiUrl),this.apiUrlCache}async token(){let t=this.apiUrl().url,r=this.runtime.env[nt]?.trim()?null:this.config.account(t),i=await ak(this.runtime.env,this.tokens,t,r?.tokenStorage);return this.out.addSecret(i?.token),i}httpTimeoutMs(){return hk(this.runtime.env,"SERPEL_HTTP_TIMEOUT",Mf/1e3,1)*1e3}pollIntervalMs(){return hk(this.runtime.env,"SERPEL_POLL_INTERVAL_MS",dk,50)}createClient(t,r){return this.out.addSecret(t),new cc({baseUrl:this.apiUrl().url,token:t,timeoutMs:this.httpTimeoutMs(),fetch:this.runtime.fetch,userAgent:`serpel/${Qr} (node ${process.version}; ${this.runtime.platform})`,log:i=>this.out.debug(i),unauthorizedHint:r})}clientFor(t){let r=t.source==="env"?`The token from ${nt} is invalid, expired or revoked.`:"The stored token is invalid, expired or revoked. Please sign in again: serpel auth login";return this.createClient(t.token,r)}async client(){let t=await this.token();if(!t)throw new z("not_logged_in",`Not signed in to ${this.apiUrl().url}.`,P.unauthorized,{hint:`Sign in with "serpel auth login" or set a token through ${nt}.`});return this.clientFor(t)}async waitForJob(t,r,i){let n=new gc(this.runtime.stderr);try{return await pk(t,r,{intervalMs:this.pollIntervalMs(),timeoutMs:i!==void 0?i*1e3:void 0,sleep:this.runtime.sleep,now:this.runtime.now,onUpdate:(o,a)=>n.update(`${fk(o)} ${mk(a)}`)})}finally{n.clear()}}};function X(e,t=Number.MAX_SAFE_INTEGER){return r=>{let i=r.trim(),n=Number(i);if(!/^-?\d+$/.test(i)||!Number.isSafeInteger(n))throw new ct(`"${r}" is not an integer.`);if(n<e||n>t)throw new ct(t===Number.MAX_SAFE_INTEGER?`Allowed values start at ${e}.`:`Allowed values range from ${e} to ${t}.`);return n}}function it(e,t){return[...t??[],e]}function L(e,t,r){return new Xe(e,t).choices(r)}function ae(e){return e.option("--limit <n>","entries per page (1 to 200)",X(1,200),50).option("--page <n>","page number, starting at 1",X(1)).option("--offset <n>","number of entries to skip (instead of --page)",X(0))}function se(e){if(e.page!==void 0&&e.offset!==void 0)throw E("Please specify either --page or --offset, not both.");let t=e.page!==void 0?(e.page-1)*e.limit:e.offset??0;return{limit:e.limit,offset:t}}function ot(e,t){return e.option("--wait",`wait for ${t} to finish (progress on stderr)`).option("--timeout <seconds>","maximum wait time with --wait",X(1))}function at(e){if(e.timeout!==void 0&&!e.wait)throw E("--timeout can only be used together with --wait.")}function v(e,t){return async(...r)=>{let i=r[r.length-1],n=new yc(e.runtime,i.optsWithGlobals(),e.secrets),o=r.slice(0,Math.max(0,r.length-2)).map(a=>a===void 0?"":String(a));await t(n,o,i.opts())}}function B(e,t){let r=e.safeParse(t);if(r.success)return r.data;let i=r.error.issues.map(n=>`${n.path.length?`${n.path.map(String).join(".")}: `:""}${n.message}`);throw E(i.join(" "))}function Q(e,t,r,i,n){if(e.json){e.out.printJson(t);return}if(t.data.length===0){e.out.info(t.pagination.total>0?`No entries on this page (${t.pagination.total} in total).`:i);return}e.out.table(r,t.data),e.out.paginationHint(t.pagination,t.data.length,n?.page)}function yn(e,t){let r=e?.trim();return r?t.noMatch(r):t.empty}function k(e){return encodeURIComponent(e)}function $e(e){return{data:e,pagination:{limit:e.length,offset:0,total:e.length}}}function ue(e,t,r){if(!e.yes)throw E(t,`To confirm: ${r} --yes`)}function gk(e,t,r){return new Xe(e,t).choices(r).argParser((i,n)=>{if(!r.includes(i))throw new ct(`Allowed: ${r.join(", ")}.`);return[...n??[],i]})}function At(e){return[...new Set((e??[]).map(t=>t.trim()).filter(Boolean))]}var yk="/billing/agent-spending";function vk(e,t){if(e.json)return e.out.printJson(t);e.out.line(`Agents may spend credits: ${t.allowed?"yes":"no"}`),e.out.line(`Daily limit for agents: ${b(t.dailyCapCredits)} credits${t.customDailyCapCredits===null?" (default)":""}`),e.out.line(`Spent today: ${b(t.spentTodayCredits)} credits`),t.planAllowsAgentSpending?t.allowed||e.out.hint("Allow with: serpel agents allow"):e.out.hint("Paid agent tools require the Starter or Pro plan.")}async function SR(e){vk(e,await(await e.client()).get(yk))}async function Wf(e,t){let r=B(Hw,t);vk(e,await(await e.client()).patch(yk,r))}async function $R(e,t,r){await Wf(e,r.dailyLimit===void 0?{allowed:!0}:{allowed:!0,dailyCapCredits:r.dailyLimit})}async function IR(e){await Wf(e,{allowed:!1})}async function CR(e,t,r){let i=t[0]?Number(t[0]):void 0;if(r.reset&&i!==void 0)throw E("The limit and --reset are mutually exclusive.");if(!r.reset&&i===void 0)throw E("Pass the daily limit in credits or use --reset for the default.");await Wf(e,{dailyCapCredits:r.reset?null:i})}function bk(e,t){let r=e.command("agents").description("AI apps and agents: allow and limit Credit spending through the MCP server");r.command("status").description("Show whether agents may spend credits, the daily limit, and today's usage").action(v(t,i=>SR(i))),r.command("allow").description("Allow agents to spend credits in the Workspace (Starter or Pro plan required)").option("--daily-limit <credits>","set the daily limit in credits",X(1,Xs)).action(v(t,$R)),r.command("deny").description("Stop agents from spending credits").action(v(t,i=>IR(i))),r.command("daily-limit [credits]").description("Set the daily limit for agents in credits").option("--reset","reset to the default").action(v(t,CR))}var Hf="Cited: The AI answer links to a page on your domain as a source. Mentioned: The answer names your domain or your project name, cited answers count as well. AI Overview: Result of the Google search for the same prompt, \u201Ccited\u201D means the AI Overview links to your domain.";function AR(e){let t=e.aiOverviewPrompts;return t?t.checked===0?t.tracking?"not checked yet":"tracking is turned off in the project":`${b(t.cited)} of ${b(t.checked)} prompts cited${t.tracking?"":" (tracking has since been turned off)"}`:R}function Kf(e,t){e.out.fields([["Tracked prompts",b(t.prompts)],["Checked",`${b(t.checked)} of ${b(t.prompts)}`],["Cited",`${b(t.cited)} of ${b(t.checked)} answers`],["Mentioned",`${b(t.mentioned)} of ${b(t.checked)} answers`],["Google AI Overview",AR(t)],["Last check",I(t.lastCheckedAt)]])}function DR(e,t){if(e.out.line(),e.out.heading("Most frequent sources in AI answers"),t.topSources.length===0){e.out.line("No sources recorded yet.");return}e.out.table([{header:"Domain",value:r=>r.domain,max:50},{header:"Mentions",value:r=>b(r.count),align:"right"},{header:"Own domain",value:r=>r.isProjectDomain?"yes":"",color:r=>e.out.c.green(r)}],t.topSources)}function RR(e,t){let r=t.aiOverviewPrompts;if(r){if(e.out.line(),e.out.heading("Google AI Overview for your prompts"),r.checked===0){e.out.line(r.tracking?"No check yet. The Google search for each prompt runs with the next check.":"Tracking is turned off in the project (setting \u201CTrack AI Overview\u201D).");return}e.out.fields([["Tracking",r.tracking?"active":"turned off"],["Checked prompts",`${b(r.checked)} of ${b(t.prompts)}`],["With AI Overview",`${b(r.present)} of ${b(r.checked)}`],["Own domain cited",`${b(r.cited)} of ${b(r.present)}`],["Last check",I(r.lastCheckedAt)]])}}function TR(e,t){e.out.line(),e.out.heading("Google AI Overview for tracked keywords");let r=t.aiOverview;if(r.keywords===0){e.out.line("No ranking check with AI Overview data yet.");return}e.out.fields([["Keywords with data",b(r.keywords)],["With AI Overview",`${b(r.present)} of ${b(r.keywords)}`],["Own domain cited",`${b(r.cited)} of ${b(r.present)}`],["Last check",I(r.lastCheckedAt)]])}function ER(e,t){if(e.out.line(),e.out.heading("AI crawler access (robots.txt)"),!t.crawlerAccess)e.out.line("No data yet. The check runs with every crawl.");else{let{c:r}=e.out;e.out.table([{header:"Crawler",value:i=>i.userAgent},{header:"Provider",value:i=>i.vendor},{header:"Purpose",value:i=>Wh[i.purpose]??i.purpose},{header:"Access",value:i=>i.allowed?"allowed":"blocked",color:(i,n)=>n.allowed?r.green(i):r.yellow(i)}],t.crawlerAccess),e.out.line()}e.out.fields([["llms.txt",ac(t.llmsTxtStatus)],["Checked on",t.accessCheckedAt?I(t.accessCheckedAt):null]])}async function OR(e,t){let i=await(await e.client()).get(`/projects/${k(t.project)}/ai-visibility`);if(e.json){e.out.printJson(i);return}e.out.heading("AI visibility"),Kf(e,i),DR(e,i),TR(e,i),RR(e,i),ER(e,i),e.out.hint(Hf)}function wk(e,t){e.command("status").description("Summary: citations, mentions, most frequent sources, AI Overview per keyword and per prompt, AI crawlers and llms.txt").requiredOption("--project <id>","project ID").addHelpText("after",`
|
|
101
|
+
`)[0]?.slice(0,200)??"unknown error"}var Qr="0.1.1";var sk=null;function Zf(e){sk=e}function ck(){return sk}var _R=new Set(["succeeded","failed","cancelled"]),dk=2e3,xR=3;async function pk(e,t,r){let i=r.now(),n=0;Zf(`Waiting cancelled. The job keeps running, check its status with: serpel jobs status ${t}`);try{for(;;){let o;try{o=await e.get(`/jobs/${encodeURIComponent(t)}`),n=0}catch(c){if(c instanceof z&&c.exitCode===P.unreachable&&n<xR){n+=1,await r.sleep(r.intervalMs*n);continue}throw c}let a=r.now()-i;if(r.onUpdate?.(o,a),_R.has(o.status))return o;if(r.timeoutMs!==void 0&&a>=r.timeoutMs)throw new z("wait_timeout",`Time limit of ${Math.round(r.timeoutMs/1e3)} s reached. Job ${t} has the status "${We[o.status]??o.status}" and keeps running.`,P.waitTimeout,{hint:`Check the status later: serpel jobs status ${t}`,details:{jobId:t,status:o.status}});let s=r.timeoutMs!==void 0?Math.max(0,r.timeoutMs-a):r.intervalMs;await r.sleep(Math.min(r.intervalMs,Math.max(s,10)))}}finally{Zf(null)}}function rt(e){if(e.status==="succeeded")return;let t=_t[e.type]??e.type;if(e.status==="cancelled")throw new z("job_cancelled",`${t} was cancelled (job ${e.id}).`,P.jobFailed,{details:{jobId:e.id,status:e.status}});let r=e.lastError?`: ${e.lastError}`:".";throw new z("job_failed",`${t} failed (job ${e.id})${r}`,P.jobFailed,{hint:`Details: serpel jobs status ${e.id}`,details:{jobId:e.id,status:e.status,attempts:e.attempts}})}function mk(e){let t=Math.floor(e/1e3),r=Math.floor(t/60),i=t%60;return`${String(r).padStart(2,"0")}:${String(i).padStart(2,"0")}`}function fk(e){let t=_t[e.type]??e.type,r=We[e.status]??e.status,i=`${t}: ${r}`;if(e.progress){let n=e.progress.total!==null?`/${e.progress.total}`:"";i+=` ${e.progress.current}${n}`,e.progress.message&&(i+=` (${e.progress.message})`)}return i}var lk="\x1B",uk=["-","\\","|","/"],gc=class{constructor(t){this.stream=t}stream;frame=0;active=!1;get enabled(){return!!this.stream.isTTY}update(t){if(!this.enabled)return;let r=uk[this.frame++%uk.length],i=Math.max(20,(this.stream.columns??80)-1),n=`${r} ${t}`.slice(0,i);this.stream.write(`\r${lk}[2K${n}`),this.active=!0}clear(){this.active&&(this.stream.write(`\r${lk}[2K`),this.active=!1)}};function hk(e,t,r,i){let n=e[t]?.trim();if(!n)return r;let o=Number(n);if(!Number.isFinite(o)||o<i)throw E(`Invalid value for ${t}: "${n}".`);return o}var yc=class{constructor(t,r,i=new Set){this.runtime=t;this.options=r;this.out=new Yr(t.stdout,t.stderr,t.env,{json:!!r.json,color:r.color!==!1,verbose:!!r.verbose},i);let n=tk(t.env,t.platform,t.homedir);this.config=new uc(n),this.tokens=new fc({configDir:n,mode:hc(t.env.SERPEL_TOKEN_STORAGE),platform:t.platform,loadKeyring:t.loadKeyring,warn:o=>this.out.warn(o)})}runtime;options;out;config;tokens;apiUrlCache=null;get json(){return this.out.json}apiUrl(){return this.apiUrlCache??=ek(this.options.apiUrl,this.runtime.env,this.config.load().apiUrl),this.apiUrlCache}async token(){let t=this.apiUrl().url,r=this.runtime.env[nt]?.trim()?null:this.config.account(t),i=await ak(this.runtime.env,this.tokens,t,r?.tokenStorage);return this.out.addSecret(i?.token),i}httpTimeoutMs(){return hk(this.runtime.env,"SERPEL_HTTP_TIMEOUT",Mf/1e3,1)*1e3}pollIntervalMs(){return hk(this.runtime.env,"SERPEL_POLL_INTERVAL_MS",dk,50)}createClient(t,r){return this.out.addSecret(t),new cc({baseUrl:this.apiUrl().url,token:t,timeoutMs:this.httpTimeoutMs(),fetch:this.runtime.fetch,userAgent:`serpel/${Qr} (node ${process.version}; ${this.runtime.platform})`,log:i=>this.out.debug(i),unauthorizedHint:r})}clientFor(t){let r=t.source==="env"?`The token from ${nt} is invalid, expired or revoked.`:"The stored token is invalid, expired or revoked. Please sign in again: serpel auth login";return this.createClient(t.token,r)}async client(){let t=await this.token();if(!t)throw new z("not_logged_in",`Not signed in to ${this.apiUrl().url}.`,P.unauthorized,{hint:`Sign in with "serpel auth login" or set a token through ${nt}.`});return this.clientFor(t)}async waitForJob(t,r,i){let n=new gc(this.runtime.stderr);try{return await pk(t,r,{intervalMs:this.pollIntervalMs(),timeoutMs:i!==void 0?i*1e3:void 0,sleep:this.runtime.sleep,now:this.runtime.now,onUpdate:(o,a)=>n.update(`${fk(o)} ${mk(a)}`)})}finally{n.clear()}}};function X(e,t=Number.MAX_SAFE_INTEGER){return r=>{let i=r.trim(),n=Number(i);if(!/^-?\d+$/.test(i)||!Number.isSafeInteger(n))throw new ct(`"${r}" is not an integer.`);if(n<e||n>t)throw new ct(t===Number.MAX_SAFE_INTEGER?`Allowed values start at ${e}.`:`Allowed values range from ${e} to ${t}.`);return n}}function it(e,t){return[...t??[],e]}function L(e,t,r){return new Xe(e,t).choices(r)}function ae(e){return e.option("--limit <n>","entries per page (1 to 200)",X(1,200),50).option("--page <n>","page number, starting at 1",X(1)).option("--offset <n>","number of entries to skip (instead of --page)",X(0))}function se(e){if(e.page!==void 0&&e.offset!==void 0)throw E("Please specify either --page or --offset, not both.");let t=e.page!==void 0?(e.page-1)*e.limit:e.offset??0;return{limit:e.limit,offset:t}}function ot(e,t){return e.option("--wait",`wait for ${t} to finish (progress on stderr)`).option("--timeout <seconds>","maximum wait time with --wait",X(1))}function at(e){if(e.timeout!==void 0&&!e.wait)throw E("--timeout can only be used together with --wait.")}function v(e,t){return async(...r)=>{let i=r[r.length-1],n=new yc(e.runtime,i.optsWithGlobals(),e.secrets),o=r.slice(0,Math.max(0,r.length-2)).map(a=>a===void 0?"":String(a));await t(n,o,i.opts())}}function B(e,t){let r=e.safeParse(t);if(r.success)return r.data;let i=r.error.issues.map(n=>`${n.path.length?`${n.path.map(String).join(".")}: `:""}${n.message}`);throw E(i.join(" "))}function Q(e,t,r,i,n){if(e.json){e.out.printJson(t);return}if(t.data.length===0){e.out.info(t.pagination.total>0?`No entries on this page (${t.pagination.total} in total).`:i);return}e.out.table(r,t.data),e.out.paginationHint(t.pagination,t.data.length,n?.page)}function yn(e,t){let r=e?.trim();return r?t.noMatch(r):t.empty}function k(e){return encodeURIComponent(e)}function $e(e){return{data:e,pagination:{limit:e.length,offset:0,total:e.length}}}function ue(e,t,r){if(!e.yes)throw E(t,`To confirm: ${r} --yes`)}function gk(e,t,r){return new Xe(e,t).choices(r).argParser((i,n)=>{if(!r.includes(i))throw new ct(`Allowed: ${r.join(", ")}.`);return[...n??[],i]})}function At(e){return[...new Set((e??[]).map(t=>t.trim()).filter(Boolean))]}var yk="/billing/agent-spending";function vk(e,t){if(e.json)return e.out.printJson(t);e.out.line(`Agents may spend credits: ${t.allowed?"yes":"no"}`),e.out.line(`Daily limit for agents: ${b(t.dailyCapCredits)} credits${t.customDailyCapCredits===null?" (default)":""}`),e.out.line(`Spent today: ${b(t.spentTodayCredits)} credits`),t.planAllowsAgentSpending?t.allowed||e.out.hint("Allow with: serpel agents allow"):e.out.hint("Paid agent tools require the Starter or Pro plan.")}async function SR(e){vk(e,await(await e.client()).get(yk))}async function Wf(e,t){let r=B(Hw,t);vk(e,await(await e.client()).patch(yk,r))}async function $R(e,t,r){await Wf(e,r.dailyLimit===void 0?{allowed:!0}:{allowed:!0,dailyCapCredits:r.dailyLimit})}async function IR(e){await Wf(e,{allowed:!1})}async function CR(e,t,r){let i=t[0]?Number(t[0]):void 0;if(r.reset&&i!==void 0)throw E("The limit and --reset are mutually exclusive.");if(!r.reset&&i===void 0)throw E("Pass the daily limit in credits or use --reset for the default.");await Wf(e,{dailyCapCredits:r.reset?null:i})}function bk(e,t){let r=e.command("agents").description("AI apps and agents: allow and limit Credit spending through the MCP server");r.command("status").description("Show whether agents may spend credits, the daily limit, and today's usage").action(v(t,i=>SR(i))),r.command("allow").description("Allow agents to spend credits in the Workspace (Starter or Pro plan required)").option("--daily-limit <credits>","set the daily limit in credits",X(1,Xs)).action(v(t,$R)),r.command("deny").description("Stop agents from spending credits").action(v(t,i=>IR(i))),r.command("daily-limit [credits]").description("Set the daily limit for agents in credits").option("--reset","reset to the default").action(v(t,CR))}var Hf="Cited: The AI answer links to a page on your domain as a source. Mentioned: The answer names your domain or your project name, cited answers count as well. AI Overview: Result of the Google search for the same prompt, \u201Ccited\u201D means the AI Overview links to your domain.";function AR(e){let t=e.aiOverviewPrompts;return t?t.checked===0?t.tracking?"not checked yet":"tracking is turned off in the project":`${b(t.cited)} of ${b(t.checked)} prompts cited${t.tracking?"":" (tracking has since been turned off)"}`:R}function Kf(e,t){e.out.fields([["Tracked prompts",b(t.prompts)],["Checked",`${b(t.checked)} of ${b(t.prompts)}`],["Cited",`${b(t.cited)} of ${b(t.checked)} answers`],["Mentioned",`${b(t.mentioned)} of ${b(t.checked)} answers`],["Google AI Overview",AR(t)],["Last check",I(t.lastCheckedAt)]])}function DR(e,t){if(e.out.line(),e.out.heading("Most frequent sources in AI answers"),t.topSources.length===0){e.out.line("No sources recorded yet.");return}e.out.table([{header:"Domain",value:r=>r.domain,max:50},{header:"Mentions",value:r=>b(r.count),align:"right"},{header:"Own domain",value:r=>r.isProjectDomain?"yes":"",color:r=>e.out.c.green(r)}],t.topSources)}function RR(e,t){let r=t.aiOverviewPrompts;if(r){if(e.out.line(),e.out.heading("Google AI Overview for your prompts"),r.checked===0){e.out.line(r.tracking?"No check yet. The Google search for each prompt runs with the next check.":"Tracking is turned off in the project (setting \u201CTrack AI Overview\u201D).");return}e.out.fields([["Tracking",r.tracking?"active":"turned off"],["Checked prompts",`${b(r.checked)} of ${b(t.prompts)}`],["With AI Overview",`${b(r.present)} of ${b(r.checked)}`],["Own domain cited",`${b(r.cited)} of ${b(r.present)}`],["Last check",I(r.lastCheckedAt)]])}}function TR(e,t){e.out.line(),e.out.heading("Google AI Overview for tracked keywords");let r=t.aiOverview;if(r.keywords===0){e.out.line("No ranking check with AI Overview data yet.");return}e.out.fields([["Keywords with data",b(r.keywords)],["With AI Overview",`${b(r.present)} of ${b(r.keywords)}`],["Own domain cited",`${b(r.cited)} of ${b(r.present)}`],["Last check",I(r.lastCheckedAt)]])}function ER(e,t){if(e.out.line(),e.out.heading("AI crawler access (robots.txt)"),!t.crawlerAccess)e.out.line("No data yet. The check runs with every crawl.");else{let{c:r}=e.out;e.out.table([{header:"Crawler",value:i=>i.userAgent},{header:"Provider",value:i=>i.vendor},{header:"Purpose",value:i=>Wh[i.purpose]??i.purpose},{header:"Access",value:i=>i.allowed?"allowed":"blocked",color:(i,n)=>n.allowed?r.green(i):r.yellow(i)}],t.crawlerAccess),e.out.line()}e.out.fields([["llms.txt",ac(t.llmsTxtStatus)],["Checked on",t.accessCheckedAt?I(t.accessCheckedAt):null]])}async function OR(e,t){let i=await(await e.client()).get(`/projects/${k(t.project)}/ai-visibility`);if(e.json){e.out.printJson(i);return}e.out.heading("AI visibility"),Kf(e,i),DR(e,i),TR(e,i),RR(e,i),ER(e,i),e.out.hint(Hf)}function wk(e,t){e.command("status").description("Summary: citations, mentions, most frequent sources, AI Overview per keyword and per prompt, AI crawlers and llms.txt").requiredOption("--project <id>","project ID").addHelpText("after",`
|
|
102
102
|
Examples:
|
|
103
103
|
serpel ai status --project <id>
|
|
104
104
|
serpel ai status --project <id> --json`).action(v(t,(r,i,n)=>OR(r,n)))}var PR=60,kk=365;function _k(e){return e?e.status==="error"?"check failed":Lf(e.cited):"not checked yet"}function xk(e){return!e||e.status==="error"?R:Lf(e.mentioned)}function Sk(e){return e?e.status==="error"?"check failed":e.present?e.cited?"cited":"not cited":"no AI Overview":R}function $k(e){return qc[e]??e}var Ik=[{header:"ID",value:e=>e.id,max:36},{header:"Prompt",value:e=>e.prompt,max:50},{header:"Platform",value:e=>$k(e.platform)},{header:"Market",value:e=>Le(e.country,e.language)}],jR=[...Ik,{header:"Cited",value:e=>_k(e.latest)},{header:"Mentioned",value:e=>xk(e.latest)},{header:"AI Overview",value:e=>Sk(e.aiOverview)},{header:"Checked",value:e=>I(e.latest?.checkedAt)}];function NR(e,t){if(e.out.line(),e.out.line(e.out.c.bold("Google AI Overview for the prompt")),t.length===0){e.out.line("No check has run for the Google AI Overview yet.");return}e.out.table([{header:"Checked",value:i=>I(i.checkedAt)},{header:"AI Overview",value:i=>Sk(i)},{header:"Cited URL",value:i=>i.status==="error"?i.error??R:i.citedUrl??R,max:60},{header:"Sources",value:i=>String(i.sources.length),align:"right"}],t);let r=t.at(-1);if(r?.answerExcerpt&&(e.out.line(),e.out.line(e.out.c.bold(`Excerpt of the latest AI Overview (${I(r.checkedAt)})`)),e.out.line(` ${r.answerExcerpt}`)),r&&r.sources.length>0){e.out.line(),e.out.line(e.out.c.bold("Sources of the latest AI Overview"));for(let i of r.sources)e.out.line(`- ${i.title?`${i.title}: `:""}${i.url}`)}}async function LR(e,t){let i=await(await e.client()).get(`/projects/${k(t.project)}/ai-prompts`);Q(e,$e(i),jR,`No tracked prompts yet. Add some with: serpel ai add --project ${t.project} --prompt "..."`),!e.json&&i.length>0&&e.out.hint(Hf)}async function UR(e,t){let r=At(t.prompt);if(r.length===0)throw E('Provide at least one prompt with --prompt "...".');let i={prompts:r};t.platform!==void 0&&(i.platform=t.platform),t.country!==void 0&&(i.country=t.country),t.language!==void 0&&(i.language=t.language),B(kw,i);let o=await(await e.client()).post(`/projects/${k(t.project)}/ai-prompts`,i);if(e.json){e.out.printJson(o);return}e.out.line(`${o.added.length} ${o.added.length===1?"prompt":"prompts"} added to tracking`+(o.skipped>0?`, ${o.skipped} skipped (already tracked or duplicate).`:".")),o.added.length>0&&e.out.table(Ik,o.added),e.out.hint(`Start a check: serpel ai run --project ${t.project} --wait`)}async function zR(e,t,r){ue(r,"The prompt is removed from tracking together with all earlier check results.",`serpel ai remove ${t}`);let i=await e.client(),n=await i.get(`/ai-prompts/${k(t)}/history`,{limit:1});await i.delete(`/ai-prompts/${k(t)}`);let o=n.prompt;if(e.json){e.out.printJson({removed:!0,id:o.id,prompt:o.prompt,projectId:o.projectId});return}e.out.line(`Prompt removed from tracking: ${o.prompt}`),e.out.fields([["ID",o.id]])}async function MR(e,t){try{return await e.get(`/projects/${k(t)}/ai-visibility`)}catch{return null}}async function VR(e,t){at(t);let r=At(t.promptId),i=r.length>0?{promptIds:r}:{};B(_w,i);let n=await e.client(),o=await n.post(`/projects/${k(t.project)}/ai-prompts/run`,i),a=`${o.promptCount} ${o.promptCount===1?"prompt":"prompts"}`;if(!t.wait){if(e.json){e.out.printJson(o);return}e.out.line(o.alreadyRunning?"An AI visibility check is already running for this project.":`AI visibility check started for ${a}.`),e.out.fields([["Job ID",o.job.id]]),e.out.hint(`Status: serpel jobs status ${o.job.id} --wait`);return}e.json||e.out.info(o.alreadyRunning?`An AI visibility check is already running for this project. Waiting for job ${o.job.id} ...`:`AI visibility check started (${a}). Job ID: ${o.job.id}. Waiting for it to finish ...`);let s=await e.waitForJob(n,o.job.id,t.timeout);rt(s);let c=await MR(n,t.project);if(e.json){e.out.printJson({job:s,promptCount:o.promptCount,alreadyRunning:o.alreadyRunning,summary:c});return}e.out.line("AI visibility check completed."),e.out.fields([["Job ID",s.id]]),c&&Kf(e,c),e.out.hint(`Results per prompt: serpel ai prompts --project ${t.project}`)}async function BR(e,t,r){let n=await(await e.client()).get(`/ai-prompts/${k(t)}/history`,{limit:r.limit});if(e.json){e.out.printJson(n);return}let o=n.prompt;e.out.heading(o.prompt),e.out.fields([["ID",o.id],["Project ID",o.projectId],["Platform",$k(o.platform)],["Market",Le(o.country,o.language)]]),e.out.line();let a=n.aiOverviewChecks??[];if(n.checks.length===0&&a.length===0){e.out.line("No check has run for this prompt yet.");return}FR(e,n.checks),NR(e,a)}function FR(e,t){if(e.out.line(e.out.c.bold("ChatGPT answers")),t.length===0){e.out.line("No check has run for ChatGPT yet.");return}e.out.table([{header:"Checked",value:i=>I(i.checkedAt)},{header:"Cited",value:i=>_k(i)},{header:"Mentioned",value:i=>xk(i)},{header:"Cited URL",value:i=>i.status==="error"?i.error??R:i.citedUrl??R,max:60},{header:"Sources",value:i=>String(i.sources.length),align:"right"}],t);let r=t.at(-1);if(r?.answerExcerpt&&(e.out.line(),e.out.line(e.out.c.bold(`Excerpt of the latest answer (${I(r.checkedAt)})`)),e.out.line(` ${r.answerExcerpt}`)),r&&r.sources.length>0){e.out.line(),e.out.line(e.out.c.bold("Sources of the latest answer"));for(let i of r.sources)e.out.line(`- ${i.title?`${i.title}: `:""}${i.url}`)}}function Ck(e,t){let r=e.command("ai").description("AI visibility: track prompts and check whether AI answers cite your domain");r.command("prompts").description("Tracked prompts with their latest result: cited, mentioned, Google AI Overview, time of the check").requiredOption("--project <id>","project ID").addHelpText("after",`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serpel/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Serpel on the command line: Google rank tracking, site audits, Search Console and Bing data, and AI visibility for ChatGPT and Google AI Overviews",
|
|
6
6
|
"keywords": [
|