UNPKG

code-review-gpt

Version:

an extensible code review agent

104 lines (84 loc) 33 kB
#!/usr/bin/env node var kc=Object.create;var{getPrototypeOf:qc,defineProperty:X,getOwnPropertyNames:Cc}=Object;var uc=Object.prototype.hasOwnProperty;var S=(s,c,E)=>{E=s!=null?kc(qc(s)):{};let n=c||!s||!s.__esModule?X(E,"default",{value:s,enumerable:!0}):E;for(let A of Cc(s))if(!uc.call(n,A))X(n,A,{get:()=>s[A],enumerable:!0});return n};var ns=(s,c)=>{for(var E in c)X(s,E,{get:c[E],enumerable:!0,configurable:!0,set:(n)=>c[E]=()=>n})};var h=(s,c)=>()=>(s&&(c=s(s=0)),c);var fs,v;var y=h(()=>{fs=require("tslog"),v=new fs.Logger({prettyLogTemplate:"{{logLevelName}}\t"})});var Q=()=>{};var ps,J=async(s)=>{let c=await ps.glob(s,{onlyFiles:!0});if(c.length===0)throw new Error(`No template file found for pattern: ${s}`);return c[0]};var ws=h(()=>{ps=require("tinyglobby")});var hs={};ns(hs,{configure:()=>Mc});var V,T,k,bs,Mc=async(s)=>{if(s.setupTarget==="github")await Qc();if(s.setupTarget==="gitlab")await Jc();if(s.setupTarget==="azdev")await jc()},Os=async()=>{return await bs.password({message:"Please input your OpenAI API key:"})},Qc=async()=>{let s=await J("**/templates/github-pr.yml"),c=k.default.join(process.cwd(),".github","workflows");T.default.mkdirSync(c,{recursive:!0});let E=k.default.join(c,"code-review-gpt.yml");T.default.writeFileSync(E,T.default.readFileSync(s,"utf8"),"utf8"),v.info(`Created GitHub Actions workflow at: ${E}`);let n=await Os();if(!n){v.error("No API key provided. Please manually add the OPENAI_API_KEY secret to your GitHub repository.");return}try{V.execSync("gh auth status || gh auth login",{stdio:"inherit"}),V.execSync(`gh secret set OPENAI_API_KEY --body=${String(n)}`),v.info("Successfully added the OPENAI_API_KEY secret to your GitHub repository.")}catch(A){v.error("It seems that the GitHub CLI is not installed or there was an error during authentication. Don't forget to add the OPENAI_API_KEY to the repo settings/Environment/Actions/Repository Secrets manually.")}},Jc=async()=>{let s=await J("**/templates/gitlab-pr.yml"),c=process.cwd(),E=k.default.join(c,".gitlab-ci.yml");T.default.writeFileSync(E,T.default.readFileSync(s,"utf8"),"utf8"),v.info(`Created GitLab CI at: ${E}`);let n=await Os();if(!n){v.error("No API key provided. Please manually add the OPENAI_API_KEY secret to your GitLab CI/CD environment variables for your repository.");return}try{V.execSync("glab auth login",{stdio:"inherit"}),V.execSync(`glab variable set OPENAI_API_KEY ${String(n)}`),v.info(`Successfully added the OPENAI_API_KEY secret to your GitLab repository. Please make sure you have set up your Gitlab access token before using this tool. Refer to the README (Gitlab CI section) for information on how to do this.`)}catch(A){v.error("It seems that the GitLab CLI is not installed or there was an error during authentication. Don't forget to add the OPENAI_API_KEY and the GITLAB_TOKEN to the repo's CI/CD Variables manually. Refer to the README (Gitlab CI section)for information on how to set up your access token.")}},jc=async()=>{let s=await J("**/templates/azdev-pr.yml"),c=process.cwd(),E=k.default.join(c,"code-review-gpt.yaml");T.default.writeFileSync(E,T.default.readFileSync(s,"utf8"),"utf8"),v.info(`Created Azure DevOps Pipeline at: ${E}`),v.info("Please manually add the OPENAI_API_KEY and API_TOKEN secrets as encrypted variables in the UI.")};var is=h(()=>{V=require("child_process"),T=S(require("fs")),k=S(require("path")),bs=require("@inquirer/prompts");Q();y();ws()});var j=()=>{let s=["GITHUB_SHA","BASE_SHA","GITHUB_TOKEN"],c=[];for(let E of s)if(!process.env[E])c.push(E);if(c.length>0)throw v.error(`Missing environment variables: ${c.join(", ")}`),new Error("One or more GitHub environment variables are not set");return{githubSha:process.env.GITHUB_SHA??"",baseSha:process.env.BASE_SHA??"",githubToken:process.env.GITHUB_TOKEN??""}},Gs=()=>{let s=["CI_MERGE_REQUEST_DIFF_BASE_SHA","CI_PROJECT_ID","CI_MERGE_REQUEST_IID","CI_COMMIT_SHA","GITLAB_TOKEN","GITLAB_HOST"].filter((c)=>!process.env[c]);if(s.length>0)throw v.error(`Missing environment variables: ${s.join(", ")}`),new Error("One or more GitLab environment variables are not set. Did you set up your Gitlab access token? Refer to the README (Gitlab CI section) on how to set it up.");return{mergeRequestBaseSha:process.env.CI_MERGE_REQUEST_DIFF_BASE_SHA??"",gitlabSha:process.env.CI_COMMIT_SHA??"",gitlabToken:process.env.GITLAB_TOKEN??"",projectId:process.env.CI_PROJECT_ID??"",mergeRequestIIdString:process.env.CI_MERGE_REQUEST_IID??"",gitlabHost:process.env.GITLAB_HOST??"https://gitlab.com"}},Ss=()=>{let s=["SYSTEM_PULLREQUEST_SOURCECOMMITID","BASE_SHA","API_TOKEN"],c=[];for(let E of s)if(!process.env[E])c.push(E);if(c.length>0)throw v.error(`Missing environment variables: ${c.join(", ")}`),new Error("One or more Azure DevOps environment variables are not set");return{azdevSha:process.env.SYSTEM_PULLREQUEST_SOURCECOMMITID??"",baseSha:process.env.BASE_SHA??"",azdevToken:process.env.API_TOKEN??""}};var D=h(()=>{y()});var ys,Us=(s)=>{if(s==="github"){let{githubSha:E,baseSha:n}=j();return`git diff --diff-filter=AMRT -U0 ${n} ${E}`}if(s==="gitlab"){let{gitlabSha:E,mergeRequestBaseSha:n}=Gs();return`git diff --diff-filter=AMRT -U0 ${n} ${E}`}if(s==="azdev"){let{azdevSha:E,baseSha:n}=Ss();return`git diff --diff-filter=AMRT -U0 ${n} ${E}`}if(s==="local")return"git diff --diff-filter=AMRT -U0 --cached";throw new Error("Invalid CI platform")},$=()=>{return new Promise((s,c)=>{ys.exec("git rev-parse --show-toplevel",(E,n)=>{if(E)c(new Error(`Failed to find git root. Error: ${E.message}`));else s(n.trim())})})};var Z=h(()=>{ys=require("child_process");D();Q();y()});var as,Ts,Bs,R,Zc=(s,c)=>{let E=s.split(` `),n=new Map,A=null,I=/^diff --git a\/(.+) b\/(.+)$/,_=/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;for(let o of E){let f=o.match(I);if(f){if(A=Ts.join(c,f[2]),!n.has(A))n.set(A,[]);continue}if(!A)continue;let p=o.match(_);if(p){let w=Number.parseInt(p[1],10),O=p[2]?Number.parseInt(p[2],10):1;if(O>0){let i=w+O-1,b=n.get(A);if(b)b.push({start:w,end:i})}}}return n},Ks=async(s)=>{try{let c=await $(),E=Us(s);v.debug("Running combined diff command:",E);let n=await new Promise((p,w)=>{as.exec(E,{cwd:c,maxBuffer:10485760},(O,i,b)=>{if(O)return w(new Error(`Failed to execute git diff. Error: ${O.message}`));if(b?.toLowerCase().includes("error"))v.error("Git diff command stderr error:",b);else if(b)v.debug("Git diff command stderr info:",b);p(i)})});if(!n.trim())v.warn("No changes found between refs. Ensure changes are staged (if local) or refs are correct (if CI)."),R.exit(0);let A=Zc(n,c);v.debug("Parsed changed lines map:",A);let I=Array.from(A.keys());if(I.length===0)v.warn("Parsed diff but found no file changes. Check diff command output."),R.exit(0);let _=I.map(async(p)=>{try{let w=await Bs.readFile(p,"utf8"),O=A.get(p)||[];return{fileName:p,fileContent:w,changedLines:O}}catch(w){return v.error(`Failed to read file content for ${p}: ${w}`),null}}),f=(await Promise.all(_)).filter((p)=>p!==null);if(f.length===0&&I.length>0)v.warn("All changed files failed to be read. Check file paths and permissions.");return v.debug("Final ReviewFile objects:",f),f}catch(c){throw v.error(`Error in getFilesWithChanges: ${c.message}`),new Error(`Failed to get files with changes: ${c.message}`)}};var $s=h(()=>{as=require("child_process"),Ts=require("path"),Bs=require("fs/promises"),R=require("process");y();Z()});var Ys,Hs,Vs,Nc,Wc=(s,c)=>{let E=Nc[s];if(!E)throw new Error(`Unsupported provider: ${s}`);if(c?.baseURL)return E({baseURL:c.baseURL});return E()},ks=(s,c)=>{let E=s.split(":");if(E.length!==2)throw new Error('Invalid model string format. Expected "provider:modelName", e.g., "openai:gpt-4o"');let[n,A]=E;return Wc(n,c)(A)};var qs=h(()=>{Ys=require("@ai-sdk/anthropic"),Hs=require("@ai-sdk/google"),Vs=require("@ai-sdk/openai"),Nc={openai:Vs.createOpenAI,google:Hs.createGoogleGenerativeAI,anthropic:Ys.createAnthropic}});var F="#### Powered by [Code Review GPT](https://github.com/mattzcarey/code-review-gpt)",L,Cs,us;var N=h(()=>{L={".js":"JavaScript",".ts":"TypeScript",".py":"Python",".sh":"Shell",".go":"Go",".rs":"Rust",".tsx":"TypeScript",".jsx":"JavaScript",".dart":"Dart",".php":"PHP",".cpp":"C++",".h":"C++",".c":"C",".cxx":"C++",".hpp":"C++",".hxx":"C++",".cs":"C#",".rb":"Ruby",".kt":"Kotlin",".kts":"Kotlin",".java":"Java",".vue":"Vue",".tf":"Terraform",".hcl":"Terraform",".swift":"Swift"},Cs=new Set(Object.keys(L)),us=new Set([".d.ts","dist","node_modules","package-lock.json",".lock"])});var Y,Ms=()=>{let{githubToken:s}=j();if(!s)throw new Error("GITHUB_TOKEN is not set");return s},Xc=()=>{try{let s=Ms(),{payload:c,issue:E}=Y.context;if(!c.pull_request){v.warn("Not a pull request context. Cannot get Octokit details.");return}let n=Y.getOctokit(s),{owner:A,repo:I,number:_}=E;return{octokit:n,owner:A,repo:I,pull_number:_}}catch(s){v.error(`Failed to get Octokit details: ${s}`);return}},Dc=()=>{try{let s=Ms();if(!s){v.error("GitHub token not found. Cannot initialize Octokit.");return}return Y.getOctokit(s)}catch(s){v.error(`Failed to get Octokit instance: ${s}`);return}},Qs=async()=>{return{postReviewComment:async(c)=>{let{filePath:E,comment:n,startLine:A,endLine:I}=c,_=Xc();if(!_){v.error("Could not get Octokit repository details for posting review comment.");return}let{octokit:o,owner:f,repo:p,pull_number:w}=_;try{let i=(await o.rest.pulls.get({owner:f,repo:p,pull_number:w})).data.head.sha;if(A===I||!A){let{data:G}=await o.rest.pulls.createReviewComment({owner:f,repo:p,pull_number:w,commit_id:i,body:n,path:E,line:I});return G.html_url}let{data:b}=await o.rest.pulls.createReviewComment({owner:f,repo:p,pull_number:w,commit_id:i,body:n,path:E,line:I,start_line:A,start_side:"RIGHT",side:"RIGHT"});return b.html_url}catch(O){v.error(`Failed to post review comment on GitHub: ${JSON.stringify(O)}`);return}},postThreadComment:async(c)=>{let{comment:E}=c,n=Dc();if(!n)return;let{payload:A,issue:I}=Y.context;if(!A.pull_request){v.warn("Not a pull request. Skipping commenting on PR thread...");return}let{owner:_,repo:o,number:f}=I,p=`${E} --- ${F}`;try{let{data:w}=await n.rest.issues.listComments({owner:_,repo:o,issue_number:f}),O=w.find((b)=>b.body?.includes(F));if(O){let{data:b}=await n.rest.issues.updateComment({owner:_,repo:o,comment_id:O.id,body:p});return b.html_url}let{data:i}=await n.rest.issues.createComment({owner:_,repo:o,issue_number:f,body:p});return i.html_url}catch(w){v.error(`Failed to post thread comment on GitHub: ${JSON.stringify(w)}`);return}}}};var Js=h(()=>{Y=require("@actions/github");D();N();y()});var js,q,U,Rc=".shippie",Fc=()=>{let s=new Date,c=s.getFullYear(),E=String(s.getMonth()+1).padStart(2,"0"),n=String(s.getDate()).padStart(2,"0"),A=String(s.getHours()).padStart(2,"0"),I=String(s.getMinutes()).padStart(2,"0"),_=String(s.getSeconds()).padStart(2,"0");return`${c}${E}${n}_${A}${I}${_}`},Lc=(s)=>{v.debug("reviewcommentDetails",s);let{filePath:c,comment:E,startLine:n,endLine:A}=s,I=`### Suggestion for \`${c}\``;if(n!==void 0)I+=` (Line ${n}${A&&A!==n?`-${A}`:""})`;return` ${I} \`\`\`suggestion ${E} \`\`\` --- `},xc=(s)=>{v.debug("thread commentDetails",s);let{comment:c}=s;return` ### General Comment / Summary ${c} --- `},Zs=async()=>{let s=await $(),E=`local_review_${Fc()}.md`,n=q.join(s,Rc,E),A=q.dirname(n),I=q.join(A,".gitignore"),_=async()=>{try{await U.mkdir(A,{recursive:!0});try{await U.access(I)}catch{await U.writeFile(I,"*"),v.debug(`Created .gitignore file at ${I}`)}}catch(f){if(f?.code!=="EEXIST")throw v.error(`Failed to create .shippie directory at ${A}: ${f}`),f;try{await U.access(I)}catch{await U.writeFile(I,"*"),v.debug(`Created .gitignore file at ${I}`)}}},o=async(f)=>{v.debug("appendToFile",f),await _();try{await U.appendFile(n,f+js.EOL)}catch(p){throw v.error(`Failed to append to local review file ${n}: ${p}`),p}};return{postReviewComment:async(f)=>{v.info(`LocalProvider: Adding review comment for ${f.filePath} to ${n}`);let p=Lc(f);return await o(p),`Suggestion added to local review file: ${n}`},postThreadComment:async(f)=>{v.info(`LocalProvider: Adding general thread comment to ${n}.`);let p=xc(f);return await o(p),`General comment added to local review file: ${n}`}}};var Ns=h(()=>{js=S(require("os")),q=S(require("path")),U=S(require("fs/promises"));Z();y()});var Ws=async(s)=>{switch(s){case"github":return v.info("Using GitHub platform provider."),await Qs();case"gitlab":throw v.info("Using GitLab platform provider."),new Error("GitLab CI provider is not implemented yet.");case"azdev":throw v.info("Using Azure DevOps platform provider."),new Error("Azure DevOps platform provider is not implemented yet.");case"local":return v.info("Using Local platform provider."),await Zs();default:throw new Error(`Unsupported CI environment specified: ${s}`)}};var Xs=h(()=>{Q();y();Js();Ns()});var Ds,Rs,Fs,C,Pc,x;var Ls=h(()=>{Ds=require("node:child_process"),Rs=require("node:util"),Fs=require("ai"),C=require("zod"),Pc=Rs.promisify(Ds.exec),x=Fs.tool({description:"Execute a bash command and get its output. Use with caution and only when necessary.",parameters:C.z.object({command:C.z.string().describe("The bash command to execute"),cwd:C.z.string().describe("The working directory for the command. Default is the current directory.").default("."),timeout:C.z.number().describe("Timeout in milliseconds before the command is killed").default(1e4)}),execute:async({command:s,cwd:c,timeout:E})=>{try{let n=["rm -rf","mkfs","dd",":(){","wget","curl"];for(let o of n)if(s.includes(o))return`Error: Potentially dangerous command detected: ${o}`;let{stdout:A,stderr:I}=await Pc(s,{cwd:c,timeout:E,maxBuffer:1048576}),_="";if(A)_+=`STDOUT: ${A} `;if(I)_+=`STDERR: ${I} `;if(!_)_="Command executed successfully with no output.";return _}catch(n){if(n instanceof Error){if("code"in n&&"killed"in n&&n.killed)return`Command timed out after ${E}ms`;return`Error executing command: ${n.message}`}return"Unknown error executing command"}}})});var xs,B,rc=1e4,P;var Ps=h(()=>{xs=require("ai"),B=require("zod"),P=xs.tool({description:"Make HTTP requests to external APIs and websites. Returns the response body as text.",parameters:B.z.object({url:B.z.string().url().describe("The URL to fetch data from"),method:B.z.enum(["GET","POST","PUT","DELETE","PATCH","HEAD"]).default("GET").describe("HTTP method to use"),headers:B.z.record(B.z.string()).optional().describe("HTTP headers to include in the request"),body:B.z.string().optional().describe("Request body (for POST, PUT, PATCH)"),timeout:B.z.number().default(rc).describe("Request timeout in milliseconds")}),execute:async({url:s,method:c,headers:E,body:n,timeout:A})=>{try{let _=new URL(s).hostname;if(_==="localhost"||_==="127.0.0.1"||_.startsWith("192.168.")||_.startsWith("10.")||_.startsWith("172.16."))return"Error: Requests to internal networks are not allowed for security reasons";let o=new AbortController,f=setTimeout(()=>o.abort(),A),p={method:c,headers:E,signal:o.signal};if(n&&["POST","PUT","PATCH"].includes(c))p.body=n;let w=await fetch(s,p);return clearTimeout(f),await w.text()}catch(I){if(I instanceof Error){if(I.name==="AbortError")return`Request timed out after ${A}ms`;return`Error: ${I.message}`}return"Unknown error during fetch"}}})});var r,rs,zs,a,z;var ts=h(()=>{r=S(require("node:path")),rs=require("ai"),zs=require("tinyglobby"),a=require("zod"),z=rs.tool({description:"Find files matching glob patterns. Useful for locating files with specific extensions or naming patterns.",parameters:a.z.object({patterns:a.z.array(a.z.string()).describe("One or more glob patterns to match files"),cwd:a.z.string().describe("The directory to search in. Default is the current directory.").default("."),excludePatterns:a.z.array(a.z.string()).describe("Glob patterns to exclude from results").optional(),includeHidden:a.z.boolean().describe("Whether to include hidden files (starting with .)").default(!1),onlyFiles:a.z.boolean().describe("Whether to only include files (not directories)").default(!0),maxResults:a.z.number().describe("Maximum number of results to return").default(100)}),execute:async({patterns:s,cwd:c,excludePatterns:E,includeHidden:n,onlyFiles:A,maxResults:I})=>{try{let _={cwd:c,dot:n,onlyFiles:A,ignore:E||[]},o=[];for(let b of s){let G=await zs.glob(b,_);o.push(...G)}let f=[...new Set(o)],p=f.slice(0,I),w=f.length>I;if(p.length===0)return`No files found matching the patterns: ${s.join(", ")}`;let O={};for(let b of p){let G=r.default.dirname(b);if(!O[G])O[G]=[];O[G].push(r.default.basename(b))}let i=[];for(let[b,G]of Object.entries(O)){if(b===".")i.push(`\uD83D\uDCC1 ./ (${G.length} files):`);else i.push(`\uD83D\uDCC1 ${b}/ (${G.length} files):`);for(let Vc of G)i.push(` \uD83D\uDCC4 ${Vc}`);i.push("")}if(w)i.push(`... and ${f.length-I} more files (limited to ${I} results)`);return`Found ${f.length} files matching the patterns: ${s.join(", ")} ${i.join(` `)}`}catch(_){return`Error finding files: ${_ instanceof Error?_.message:String(_)}`}}})});var gs,ds,es,ms,K,t;var ls=h(()=>{gs=S(require("node:fs/promises")),ds=S(require("node:path")),es=require("ai"),ms=require("tinyglobby"),K=require("zod"),t=es.tool({description:"Search for a pattern in files. Returns matching lines with line numbers and file paths.",parameters:K.z.object({pattern:K.z.string().describe("The pattern to search for (supports JavaScript regex)"),path:K.z.string().describe("Directory or file path to search in. Default is the current directory.").default("."),glob:K.z.string().describe('Glob pattern for filtering files (e.g., "**/*.ts" for TypeScript files)').default("**/*.*"),ignoreCase:K.z.boolean().describe("Whether to ignore case when matching").default(!1),maxResults:K.z.number().describe("Maximum number of results to return").default(30)}),execute:async({pattern:s,path:c,glob:E,ignoreCase:n,maxResults:A})=>{try{let I=await ms.glob(E,{cwd:c,onlyFiles:!0,dot:!1}),_=new RegExp(s,n?"i":""),o=[],f=0;for(let p of I){if(f>=A)break;try{let w=ds.default.resolve(c,p),i=(await gs.default.readFile(w,"utf-8")).split(` `);for(let b=0;b<i.length;b++){if(f>=A)break;let G=i[b];if(_.test(G))o.push(`${p}:${b+1}: ${G.trim()}`),f++}}catch(w){}}if(o.length===0)return`No matches found for pattern "${s}" in ${I.length} files.`;return`Found ${o.length} matches for pattern "${s}": ${o.join(` `)}`}catch(I){return`Error searching files: ${I instanceof Error?I.message:String(I)}`}}})});var g,d,sc,u,e,cc=async(s,c,E)=>{try{let n=await g.default.readdir(s,{withFileTypes:!0}),A=E?n:n.filter((_)=>!_.name.startsWith(".")),I=[];for(let _ of A){let o=d.default.join(s,_.name);if(_.isDirectory()){if(I.push(`\uD83D\uDCC1 ${_.name}/`),c){let p=(await cc(o,c,E)).split(` `).filter((w)=>w.trim()).map((w)=>` ${w}`).join(` `);if(p)I.push(p)}}else I.push(`\uD83D\uDCC4 ${_.name}`)}return I.join(` `)}catch(n){return`Error listing directory: ${n instanceof Error?n.message:String(n)}`}};var Ec=h(()=>{g=S(require("node:fs/promises")),d=S(require("node:path")),sc=require("ai"),u=require("zod"),e=sc.tool({description:"List files and directories at a specified path. Helpful for exploring the repository structure.",parameters:u.z.object({path:u.z.string().describe("The absolute path to list contents from.").default("."),recursive:u.z.boolean().describe("Whether to list contents recursively").default(!1),includeHidden:u.z.boolean().describe("Whether to include hidden files (starting with .)").default(!1)}),execute:async({path:s,recursive:c,includeHidden:E})=>{try{let n=await g.default.readdir(s,{withFileTypes:!0}),A=E?n:n.filter((_)=>!_.name.startsWith(".")),I=[];for(let _ of A){let o=d.default.join(s,_.name);if(_.isDirectory()){if(I.push(`\uD83D\uDCC1 ${_.name}/`),c){let p=(await cc(o,c,E)).split(` `).filter((w)=>w.trim()).map((w)=>` ${w}`).join(` `);if(p)I.push(p)}}else I.push(`\uD83D\uDCC4 ${_.name}`)}return I.join(` `)}catch(n){return`Error listing directory: ${n instanceof Error?n.message:String(n)}`}}})});var nc,W=(s,c="Unknown Language")=>{let E=nc.extname(s);return L[E]||c};var m=h(()=>{nc=require("path");N()});var vc,M,l;var Ic=h(()=>{vc=require("ai"),M=require("zod");m();l=vc.tool({description:"Read a file or part of a file. You should use this to gather context about the changes in the PR. You should read several lines before and after the changes. You may need to go back and read more lines.",parameters:M.z.object({path:M.z.string().describe("The absolute path to the file to read"),startLine:M.z.number().optional().describe("The line number to start reading from."),endLine:M.z.number().optional().describe("The line number to end reading at.")}),execute:async({path:s,startLine:c,endLine:E})=>{let A=(await Bun.file(s).text()).split(` `),I=200,_=c?c-1:0,o=E?E-1:_+200,p=A.slice(_,o+1).join(` `),w=`Here is the file excerpt you requested. NOTE that unless an EOF is shown, the file is not complete. File path: ${s} Lines Selected: ${_+1} to ${o+1}: `,O=W(s,"");return`${w}\`\`\`${O.toLowerCase()} ${p}\`\`\``}})});var Ac,ss,cs=(s)=>Ac.tool({description:"Posts the final review report as a general comment on the PR. Call this tool when the review is complete.",parameters:ss.z.object({report:ss.z.string().describe("The final review report formatted in markdown. It should contain a brief but specific overview of the changes, and potential edge cases or issues that the reviewer should be aware of.")}),execute:async({report:c})=>{try{let E={comment:c},n=await s.postThreadComment(E);return v.info(`Report posted via tool. Result: ${n??"No URL"}`),n?`Report posted successfully: ${n}`:"Report posted, but no URL returned."}catch(E){return v.error(`Failed to post report via tool: ${E}`),`Error posting report: ${E}`}}});var _c=h(()=>{Ac=require("ai"),ss=require("zod");y()});var fc,H,Es=(s)=>fc.tool({description:"Posts a specific suggestion or comment on a particular file line or range. You should ONLY do this on files with actionable problems. If a file is fine you CANNOT use this tool otherwise the user will not trust you again.",parameters:H.z.object({filePath:H.z.string().describe("The absolute path to the file you are suggesting changes to."),comment:H.z.string().describe("The change you are suggesting. It must be actionable and specific."),startLine:H.z.number().optional().describe("The line number to start the comment at."),endLine:H.z.number().optional().describe("The line number to end the comment at.")}),execute:async({filePath:c,comment:E,startLine:n,endLine:A})=>{try{let I=await s.postReviewComment({filePath:c,comment:E,startLine:n,endLine:A});return v.info(`Suggestion for ${c} posted via tool. Result: ${I??"No URL"}`),I?`Suggestion posted successfully: ${I}`:"Suggestion posted, but no URL returned."}catch(I){return v.error(`Failed to post suggestion for ${c} via tool: ${I}`),`Error posting suggestion: ${I instanceof Error?I.message:String(I)}`}}});var pc=h(()=>{fc=require("ai"),H=require("zod");y()});var wc=h(()=>{Ls();Ps();ts();ls();Ec();Ic();_c();pc()});var oc,zc=async(s,c,E,n,A)=>{let I=cs(E),_=Es(E);return await oc.generateText({model:c,prompt:s,tools:{read_file:l,suggest_change:_,submit_summary:I,fetch:P,glob:z,grep:t,ls:e,bash:x},maxSteps:n,onStepFinish:(f)=>{v.debug("Step finished:",f);let p=f.toolCalls?.some((O)=>O.toolName==="submit_summary"),w=f.toolResults?.some((O)=>O.toolName==="submit_summary");if((p||w)&&A)v.debug("Detected submit_summary tool usage in step, triggering callback."),A()}})},bc=async(s,c,E,n,A=3)=>{v.info(`Running agentic review (max retries: ${A})...`);let I=null,_=s,o="",f=!1;for(let w=1;w<=A;w++){if(v.info(`Attempt ${w}/${A}...`),f=!1,I=await zc(_,c,E,n,()=>{f=!0}),f){v.info(`Agent submitted summary on attempt ${w} (detected via callback).`);break}if(v.warn(`Agent did not submit summary on attempt ${w}.`),w<A){let O=I.toolResults.map((b)=>`Tool Result (${b.toolName}): ${JSON.stringify(b.result)}`).join(` `),i=I.text?` Final Text: ${I.text}`:"";o+=` --- Attempt ${w} Context --- ${O}${i} --- End Attempt ${w} Context ---`,_=`${s}${o} Please continue the task based on previous attempts and ensure you call submit_summary.`,v.info(`Preparing for attempt ${w+1}.`)}}if(!I)throw new Error("Agent did not produce any result.");let p=f;if(!p)v.error(`Agent failed to submit summary after ${A} attempts. Proceeding to parse final text anyway.`);try{let w=JSON.parse(I.text);if(typeof w.success==="boolean"&&typeof w.message==="string")return v.info(`Agent returned valid JSON. Success flag: ${w.success}. Summary submitted (detected via callback): ${p}`),w;return v.error("Parsed final text but structure is invalid."),{success:p,message:`Agent returned unexpected final text structure: ${I.text}`}}catch(w){return v.error("Failed to parse final agent text:",w),{success:p,message:`Agent finished with non-JSON text: ${I.text}`}}};var Oc=h(()=>{oc=require("ai");wc();y()});var hc,tc=(s,c)=>{let E={name:"root",children:{}};for(let n of s){let I=hc.relative(c,n.fileName).split("/"),_=E;for(let[o,f]of I.entries()){if(!f)continue;if(!_.children[f])_.children[f]={name:f,children:{}};if(_=_.children[f],o===I.length-1)_.isEndOfPath=!0,_.changedLines=n.changedLines,_.fullPath=n.fileName}}return E},gc=(s)=>{if(!s||s.length===0)return"";return s.sort((c,E)=>c.start-E.start),s.map((c)=>c.start===c.end?`${c.start}`:`${c.start}-${c.end}`).join(", ")},ic=(s,c="",E=!0)=>{let n="";if(s.name!=="root"){let I=`${c}${E?"└── ":"├── "}${s.name}`;if(s.isEndOfPath){let _=gc(s.changedLines);if(_)I+=`: ${_}`}n+=`${I} `}let A=Object.keys(s.children).sort();for(let[I,_]of A.entries()){let o=s.children[_],f=I===A.length-1,p=s.name==="root"?"":`${c}${E?" ":"│ "}`;n+=ic(o,p,f)}return n},Gc=(s,c,E)=>{let n=tc(s,c||""),A=ic(n,"",!0).trim();return`${E?`Review Goal: ${E} `:""}Files changed for this review (paths relative to root, includes line ranges): ${A} --- `};var Sc=h(()=>{hc=require("path")});var yc=`You are an expert {ProgrammingLanguage} developer agent. Your task is to review a pull request. Keep going until the user's query is completely resolved before ending your turn. Only terminate when you are sure the review is complete. Use tools to investigate the file content, codebase structure, or the impact of changes and to gather information. DO NOT guess or make up an answer. You MUST plan extensively before each action or tool call, and reflect on the outcomes of previous steps. // Goal Your primary goal is to review the changed code in the provided files and produce a concise summary describing the intent of the overall changes in the pull request. You MUST use the tools provided to you to complete your task. // Workflow 1. **Understand Changes:** Analyze the provided diffs (lines prefixed with '+' or '-'). You are given a list of filenames and their partial contents, but note that you might not have the full context of the code. 2. **Gather Context:** Use the \`read_file\` tool if more context is needed around the changed lines to understand their impact or intent. Pay attention to surrounding functions, classes, and imports. 3. **Assess Impact & Intent:** Determine what the changes aim to achieve and evaluate potential side effects. Use the \`shell\` tool to run tests or linters if necessary to verify correctness and style. 4. **Identify Issues:** Based on the rules below, identify specific problems or areas for improvement in the changed code. 5. **Formulate Feedback:** Use the appropriate tools (\`suggest_change\`, \`new_file\`, \`ask_question\`) to provide feedback or request clarification. 6. **Summarize Intent:** Synthesize your understanding into a brief summary of the pull request's purpose. 7. **Deliver Feedback via Tools:** Use the appropriate tools (\`suggest_change\`, \`new_file\`, \`ask_question\`) throughout the review process to provide feedback or ask questions. 8. **Final Output:** Finish your task by calling \`submit_summary\` with the summary text described in step 7. // Rules for Code Review - **Functionality:** Ensure changes do not break existing functionality. Use tools to investigate if needed. - **Testing:** Verify that changes are adequately tested. Suggest new tests using \`new_file\` if coverage is lacking. - **Best Practices:** Ensure changes follow clean code principles, are DRY (Don't Repeat Yourself), and are concise. Follow SOLID principles where applicable. - **Risk Assessment:** Evaluate changed code using a risk score from 1 (low risk) to 5 (high risk). Flag API keys or secrets present in plain text immediately as highest risk (5). - **Readability & Performance:** Comment on improving readability and performance where applicable. - **Focus:** Only review lines of code which have been changed (added '+' or removed '-'). Ignore context lines. Do not praise or complement anything. Only focus on the negative aspects. - **Brevity:** Keep feedback brief, concise, and accurate. If multiple similar issues exist, comment only on the most critical. Feedback should be in {ReviewLanguage}. - **Confidence:** Be aware of unfamiliar libraries/techniques. Only comment if confident there's a problem. Do not comment on breaking functions down unless it's a huge problem. - **Examples:** Include brief, correct code snippets for suggested changes using \`suggest_change\`. Use ordered lists for multiple suggestions. Use the same programming language as the file under review. // Output Format - Respond ONLY with a success or failure message. Return a success message if the review is complete. Return a failure message if the review is not complete or if there was an error which prevented the review from being completed. Success message: { "success": true, "message": "Review completed successfully." } Failure message: { "success": false, "message": "<include the error message here>" } `;var Uc=async(s,c)=>{let E=await $(),n=s.length>0?W(s[0].fileName):"default",A=yc.replace("{ProgrammingLanguage}",n).replace("{ReviewLanguage}",c),I=Gc(s,E);return`${A} ${I}`};var ac=h(()=>{Z();Sc();m()});var Tc,Bc=(s)=>{return s.filter((E)=>{let n=Tc.extname(E.fileName);return Cs.has(n)&&![...us].some((A)=>E.fileName.includes(A))&&E.fileName.trim()!==""})};var Kc=h(()=>{Tc=require("path");N()});var $c={};ns($c,{review:()=>ec});var ec=async(s)=>{v.debug("Review started."),v.debug(`Model used: ${s.modelString}`),v.debug(`Review language: ${s.reviewLanguage}`),v.debug(`Platform: ${s.platform}`),v.debug(`Max steps: ${s.maxSteps}`);let c=await Ks(s.platform);v.debug(`Found ${c.length} changed files.`);let E=Bc(c);if(v.debug(`Filtered ${E.length} files to review.`),E.length===0){v.info("No file to review, finishing review now.");return}v.debug(`Files to review after filtering: ${E.map((A)=>A.fileName)}`);let n=await Ws(s.platform);v.debug("Platform provider:",n);try{let A=ks(s.modelString),I=await Uc(E,s.reviewLanguage);v.debug("Prompt:",I);let{success:_,message:o}=await bc(I,A,n,s.maxSteps);if(_)v.info("Review completed successfully.");else v.error("Review failed with message:",o),process.exit(1)}catch(A){if(v.error("An error occurred during the review process:",JSON.stringify(A,null,6)),A instanceof Error&&A.stack)v.debug(`Stack trace: ${A.stack}`);else v.debug("No stack trace available for the error.");process.exit(1)}};var Yc=h(()=>{$s();qs();Xs();y();Oc();ac();Kc()});var Hc=S(require("dotenv"));var vs=S(require("dotenv")),Is=S(require("yargs")),As=require("yargs/helpers");vs.default.config();var _s=async()=>{return Is.default(As.hideBin(process.argv)).command("configure","Configure the tool",(s)=>{return s.option("setupTarget",{description:"Specifies for which platform ('github', 'gitlab' or 'azdev') the project should be configured for. Defaults to 'github'.",choices:["github","gitlab","azdev"],type:"string",default:"github"})}).command("review","Review code changes",(s)=>{return s.option("modelString",{description:'The model to use for generating the review. Defaults to "openai:gpt-4o-mini".',type:"string",default:"openai:gpt-4.1-mini"}).option("reviewLanguage",{description:"Specifies the target natural language for translation",type:"string",default:"English"}).option("platform",{description:"Platform type",choices:["github","gitlab","azdev","local"],type:"string",default:"local"}).option("maxSteps",{description:"Maximum number of agentic steps to take",type:"number",default:25})}).demandCommand(1,"Please specify a command: configure or review").option("debug",{description:"Enables debug logging.",type:"boolean",default:!1}).help().parse()};y();Hc.default.config();var mc=async()=>{let s=await _s();v.settings.minLevel=s.debug?2:s.ci?4:3,v.debug(`Args: ${JSON.stringify(s)}`);try{switch(s._?.[0]){case"configure":{let{configure:c}=await Promise.resolve().then(() => (is(),hs));await c(s);break}case"review":{let{review:c}=await Promise.resolve().then(() => (Yc(),$c));await c(s);break}default:v.error("Unknown command"),process.exit(1)}}catch(c){v.error(`Error: ${c}`),process.exit(1)}};mc().catch((s)=>{let c=s instanceof Error?s.message:"An unknown error occurred",E=s instanceof Error?s.stack:"No stack trace available";if(v.error(`Error: ${c}`),E)v.debug(`Stack trace: ${E}`);process.exit(1)});