@@ -15,12 +15,13 @@ const program = new Command();
1515program
1616 . name ( "hodor" )
1717 . description (
18- "AI-powered code review agent for GitHub PRs and GitLab MRs.\n\n" +
18+ "AI-powered code review agent for GitHub PRs, GitLab MRs, and local diffs .\n\n" +
1919 "Hodor uses an AI agent that clones the repository, checks out the PR branch,\n" +
20- "and analyzes the code using tools (gh, git, glab) for metadata fetching and comment posting." ,
20+ "and analyzes the code using tools (gh, git, glab) for metadata fetching and comment posting.\n\n" +
21+ "For local reviews, use --local with --diff-against to review changes in your current git repository." ,
2122 )
22- . version ( "0.3.4 " )
23- . argument ( "< pr-url> " , "URL of the GitHub PR or GitLab MR to review" )
23+ . version ( "0.4.1 " )
24+ . argument ( "[ pr-url] " , "URL of the GitHub PR or GitLab MR to review (optional with --local) " )
2425 . option (
2526 "--model <model>" ,
2627 "LLM model to use (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-5)" ,
@@ -58,7 +59,17 @@ program
5859 "--prometheus-push <url>" ,
5960 "Push review metrics to a Prometheus Pushgateway URL" ,
6061 )
61- . action ( async ( prUrl : string , cmdOpts : Record < string , unknown > ) => {
62+ . option (
63+ "--local" ,
64+ "Review local changes in the current directory (no PR URL required)" ,
65+ false ,
66+ )
67+ . option (
68+ "--diff-against <ref>" ,
69+ "Git ref to diff against in local mode (e.g., origin/main, HEAD~1)" ,
70+ "origin/main" ,
71+ )
72+ . action ( async ( prUrl : string | undefined , cmdOpts : Record < string , unknown > ) => {
6273 const verbose = cmdOpts . verbose as boolean ;
6374 const post = cmdOpts . post as boolean ;
6475 const model = cmdOpts . model as string ;
@@ -69,6 +80,17 @@ program
6980 const ultrathink = cmdOpts . ultrathink as boolean ;
7081 const bedrockTagsRaw = cmdOpts . bedrockTags as string | undefined ;
7182 const prometheusPush = cmdOpts . prometheusPush as string | undefined ;
83+ const localMode = cmdOpts . local as boolean ;
84+ const diffAgainst = cmdOpts . diffAgainst as string ;
85+
86+ if ( ! localMode && ! prUrl ) {
87+ console . error ( chalk . red ( "Error: pr-url is required unless --local is specified" ) ) ;
88+ process . exit ( 1 ) ;
89+ }
90+ if ( localMode && post ) {
91+ console . error ( chalk . red ( "Error: --post is not supported in --local mode (no remote to post to)" ) ) ;
92+ process . exit ( 1 ) ;
93+ }
7294
7395 // Auto-detect CI environment
7496 const isCI = ! ! ( process . env . CI || process . env . GITLAB_CI || process . env . GITHUB_ACTIONS ) ;
@@ -168,41 +190,34 @@ program
168190 }
169191
170192 try {
171- // Validate URL and detect platform (inside try so errors are caught)
172- const platform = detectPlatform ( prUrl ) ;
173- const githubToken = process . env . GITHUB_TOKEN ;
174- const gitlabToken =
175- process . env . GITLAB_TOKEN ??
176- process . env . GITLAB_PRIVATE_TOKEN ??
177- process . env . CI_JOB_TOKEN ;
193+ // Detect platform and warn about missing tokens
194+ let platform : string = "local" ;
195+ if ( ! localMode && prUrl ) {
196+ platform = detectPlatform ( prUrl ) ;
197+ const githubToken = process . env . GITHUB_TOKEN ;
198+ const gitlabToken =
199+ process . env . GITLAB_TOKEN ??
200+ process . env . GITLAB_PRIVATE_TOKEN ??
201+ process . env . CI_JOB_TOKEN ;
178202
179- if ( platform === "github" && ! githubToken ) {
180- console . error (
181- chalk . yellow (
182- "Warning: GITHUB_TOKEN not set. You may encounter rate limits or authentication issues." ,
183- ) ,
184- ) ;
185- console . error (
186- chalk . dim ( " Set GITHUB_TOKEN environment variable or run: gh auth login\n" ) ,
187- ) ;
188- } else if ( platform === "gitlab" && ! gitlabToken ) {
189- console . error (
190- chalk . yellow (
191- "Warning: No GitLab token detected. Set GITLAB_TOKEN (api scope) for authentication." ,
192- ) ,
193- ) ;
194- console . error (
195- chalk . dim (
196- " Export GITLAB_TOKEN and optionally GITLAB_HOST for self-hosted instances.\n" ,
197- ) ,
198- ) ;
203+ if ( platform === "github" && ! githubToken ) {
204+ console . error ( chalk . yellow ( "Warning: GITHUB_TOKEN not set. You may encounter rate limits." ) ) ;
205+ console . error ( chalk . dim ( " Set GITHUB_TOKEN or run: gh auth login\n" ) ) ;
206+ } else if ( platform === "gitlab" && ! gitlabToken ) {
207+ console . error ( chalk . yellow ( "Warning: No GitLab token detected. Set GITLAB_TOKEN (api scope)." ) ) ;
208+ console . error ( chalk . dim ( " Export GITLAB_TOKEN and optionally GITLAB_HOST.\n" ) ) ;
209+ }
199210 }
200211
201- log (
202- `\n${ chalk . bold . cyan ( "Hodor - AI Code Review Agent" ) } ` ,
203- ) ;
204- log ( chalk . dim ( `Platform: ${ platform . toUpperCase ( ) } ` ) ) ;
205- log ( chalk . dim ( `PR URL: ${ prUrl } ` ) ) ;
212+ log ( `\n${ chalk . bold . cyan ( "Hodor - AI Code Review Agent" ) } ` ) ;
213+ if ( localMode ) {
214+ log ( chalk . dim ( `Mode: Local diff review` ) ) ;
215+ log ( chalk . dim ( `Diff against: ${ diffAgainst } ` ) ) ;
216+ log ( chalk . dim ( `Workspace: ${ workspace ?? process . cwd ( ) } ` ) ) ;
217+ } else {
218+ log ( chalk . dim ( `Platform: ${ platform . toUpperCase ( ) } ` ) ) ;
219+ log ( chalk . dim ( `PR URL: ${ prUrl } ` ) ) ;
220+ }
206221 log ( chalk . dim ( `Model: ${ model } ` ) ) ;
207222 if ( reasoningEffort ) {
208223 log ( chalk . dim ( `Reasoning Effort: ${ reasoningEffort } ` ) ) ;
@@ -211,22 +226,24 @@ program
211226
212227 streamLog ( chalk . dim ( "▶ Setting up workspace..." ) ) ;
213228 const { review, metricsFooter, headSha, metrics } = await reviewPr ( {
214- prUrl,
229+ prUrl : localMode ? undefined : prUrl ,
215230 model,
216231 reasoningEffort,
217232 customPrompt : prompt ,
218233 promptFile,
219234 cleanup : ! workspace ,
220235 workspaceDir : workspace ,
221- includeMetricsFooter : post ,
236+ includeMetricsFooter : post && ! localMode ,
222237 onEvent : handleEvent ,
223238 bedrockTags,
239+ localMode,
240+ diffAgainst,
224241 } ) ;
225242 const reviewText = renderMarkdown ( review ) ;
226243
227244 streamLog ( chalk . green ( "✔ Review complete!" ) ) ;
228245
229- if ( post ) {
246+ if ( post && prUrl ) {
230247 log ( chalk . cyan ( "\nPosting review to PR/MR..." ) ) ;
231248
232249 const result = await postReviewComment ( {
@@ -241,20 +258,16 @@ program
241258 log ( chalk . bold . green ( "Review posted successfully!" ) ) ;
242259 log ( chalk . dim ( ` ${ platform === "github" ? "PR" : "MR" } : ${ prUrl } ` ) ) ;
243260 } else {
244- log (
245- chalk . bold . red ( `Failed to post review: ${ result . error } ` ) ,
246- ) ;
261+ log ( chalk . bold . red ( `Failed to post review: ${ result . error } ` ) ) ;
247262 log ( chalk . yellow ( "\nReview output:\n" ) ) ;
248263 console . log ( reviewText ) ;
249264 }
250265 } else {
251266 log ( chalk . bold . green ( "Review Complete\n" ) ) ;
252267 console . log ( reviewText ) ;
253- log (
254- chalk . dim (
255- "\nTip: Use --post to automatically post this review to the PR/MR" ,
256- ) ,
257- ) ;
268+ if ( ! localMode ) {
269+ log ( chalk . dim ( "\nTip: Use --post to automatically post this review to the PR/MR" ) ) ;
270+ }
258271 }
259272
260273 // Push metrics to Prometheus Pushgateway (best-effort, never fails the run)
0 commit comments