@mrkaran/hodor 0.5.0 → 0.6.2-rc.2

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/dist/cli.js CHANGED
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  detectPlatform,
4
+ parsePrUrl,
5
+ postGitlabReviewCommitStatus,
4
6
  postReviewComment,
7
+ postReviewStructured,
5
8
  pushMetrics,
9
+ reviewPr
10
+ } from "./chunk-MRMLGIXO.js";
11
+ import {
6
12
  renderMarkdown,
7
- reviewPr,
8
13
  setLogLevel
9
- } from "./chunk-QGUJENIG.js";
14
+ } from "./chunk-DVJVQTVW.js";
15
+ import "./chunk-AMUK6GDX.js";
10
16
 
11
17
  // src/cli.ts
12
18
  import { Command } from "commander";
@@ -14,14 +20,14 @@ import chalk from "chalk";
14
20
  import "dotenv/config";
15
21
  var program = new Command();
16
22
  program.name("hodor").description(
17
- "AI-powered code review agent for GitHub PRs, GitLab MRs, and local diffs.\n\nHodor uses an AI agent that clones the repository, checks out the PR branch,\nand analyzes the code using tools (gh, git, glab) for metadata fetching and comment posting.\n\nFor local reviews, use --local with --diff-against to review changes in your current git repository."
18
- ).version("0.4.1").argument("[pr-url]", "URL of the GitHub PR or GitLab MR to review (optional with --local)").option(
23
+ "AI-powered code review agent for GitHub PRs, GitLab MRs, Gitea/Forgejo PRs, and local diffs.\n\nHodor uses an AI agent that clones the repository, checks out the PR branch,\nand analyzes the code using tools (gh, git, glab) for metadata fetching and comment posting.\n\nFor local reviews, use --local with --diff-against to review changes in your current git repository."
24
+ ).version("0.6.1").argument("[pr-url]", "URL of the GitHub PR, GitLab MR, or Gitea/Forgejo PR to review (optional with --local)").option(
19
25
  "--model <model>",
20
- "LLM model to use (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-5)",
26
+ "LLM model to use as provider/model-id (e.g., anthropic/claude-sonnet-4-5-20250929, openrouter/moonshotai/kimi-k2.6)",
21
27
  "anthropic/claude-sonnet-4-5-20250929"
22
28
  ).option(
23
29
  "--reasoning-effort <level>",
24
- "Reasoning effort level: low, medium, high, xhigh"
30
+ "Reasoning effort level: minimal, low, medium, high, xhigh"
25
31
  ).option("-v, --verbose", "Enable verbose logging", false).option(
26
32
  "--post",
27
33
  "Post the review directly to the PR/MR as a comment",
@@ -32,6 +38,17 @@ program.name("hodor").description(
32
38
  ).option(
33
39
  "--workspace <dir>",
34
40
  "Workspace directory (creates temp dir if not specified)"
41
+ ).option(
42
+ "--review-style <style>",
43
+ "How to post reviews on GitLab: summary (single comment), inline (diff comments), hybrid (both). Default: hybrid.",
44
+ "hybrid"
45
+ ).option(
46
+ "--code-quality <path>",
47
+ "Write a gl-code-quality-report.json CodeClimate artifact to this path"
48
+ ).option(
49
+ "--commit-status",
50
+ "Post a pass/fail commit status to the MR head SHA",
51
+ false
35
52
  ).option(
36
53
  "--ultrathink",
37
54
  "Enable maximum reasoning effort with extended thinking budget",
@@ -58,6 +75,9 @@ program.name("hodor").description(
58
75
  const prompt = cmdOpts.prompt;
59
76
  const promptFile = cmdOpts.promptFile;
60
77
  const workspace = cmdOpts.workspace;
78
+ const reviewStyle = cmdOpts.reviewStyle;
79
+ const codeQuality = cmdOpts.codeQuality;
80
+ const commitStatus = cmdOpts.commitStatus;
61
81
  const ultrathink = cmdOpts.ultrathink;
62
82
  const bedrockTagsRaw = cmdOpts.bedrockTags;
63
83
  const prometheusPush = cmdOpts.prometheusPush;
@@ -71,11 +91,15 @@ program.name("hodor").description(
71
91
  console.error(chalk.red("Error: --post is not supported in --local mode (no remote to post to)"));
72
92
  process.exit(1);
73
93
  }
74
- const isCI = !!(process.env.CI || process.env.GITLAB_CI || process.env.GITHUB_ACTIONS);
94
+ if (!["summary", "inline", "hybrid"].includes(reviewStyle ?? "hybrid")) {
95
+ console.error(chalk.red("Error: --review-style must be one of: summary, inline, hybrid"));
96
+ process.exit(1);
97
+ }
98
+ const isCI = !!(process.env.CI || process.env.GITLAB_CI || process.env.GITHUB_ACTIONS || process.env.GITEA_ACTIONS || process.env.FORGEJO_ACTIONS);
75
99
  if (verbose) setLogLevel("debug");
76
100
  else if (isCI) setLogLevel("info");
77
101
  if (ultrathink) {
78
- reasoningEffort = "high";
102
+ reasoningEffort = "xhigh";
79
103
  }
80
104
  let bedrockTags = null;
81
105
  if (bedrockTagsRaw) {
@@ -157,8 +181,13 @@ program.name("hodor").description(
157
181
  }
158
182
  try {
159
183
  let platform = "local";
184
+ let metricsProject;
185
+ let metricsMrIid;
160
186
  if (!localMode && prUrl) {
161
187
  platform = detectPlatform(prUrl);
188
+ const parsedPr = parsePrUrl(prUrl);
189
+ metricsProject = `${parsedPr.owner}/${parsedPr.repo}`;
190
+ metricsMrIid = String(parsedPr.prNumber);
162
191
  const githubToken = process.env.GITHUB_TOKEN;
163
192
  const gitlabToken = process.env.GITLAB_TOKEN ?? process.env.GITLAB_PRIVATE_TOKEN ?? process.env.CI_JOB_TOKEN;
164
193
  if (platform === "github" && !githubToken) {
@@ -167,6 +196,12 @@ program.name("hodor").description(
167
196
  } else if (platform === "gitlab" && !gitlabToken) {
168
197
  console.error(chalk.yellow("Warning: No GitLab token detected. Set GITLAB_TOKEN (api scope)."));
169
198
  console.error(chalk.dim(" Export GITLAB_TOKEN and optionally GITLAB_HOST.\n"));
199
+ } else if (platform === "gitea") {
200
+ const giteaToken = process.env.GITEA_TOKEN ?? process.env.FORGEJO_TOKEN;
201
+ if (!giteaToken) {
202
+ console.error(chalk.yellow("Warning: No Gitea/Forgejo token detected. Set GITEA_TOKEN for authentication."));
203
+ console.error(chalk.dim(" Export GITEA_TOKEN (or FORGEJO_TOKEN) for API access.\n"));
204
+ }
170
205
  }
171
206
  }
172
207
  log(`
@@ -185,7 +220,7 @@ ${chalk.bold.cyan("Hodor - AI Code Review Agent")}`);
185
220
  }
186
221
  log();
187
222
  streamLog(chalk.dim("\u25B6 Setting up workspace..."));
188
- const { review, metricsFooter, headSha, metrics } = await reviewPr({
223
+ const { review, metricsFooter, headSha, metrics, workspacePath } = await reviewPr({
189
224
  prUrl: localMode ? void 0 : prUrl,
190
225
  model,
191
226
  reasoningEffort,
@@ -201,18 +236,68 @@ ${chalk.bold.cyan("Hodor - AI Code Review Agent")}`);
201
236
  });
202
237
  const reviewText = renderMarkdown(review);
203
238
  streamLog(chalk.green("\u2714 Review complete!"));
239
+ if (codeQuality) {
240
+ try {
241
+ const { formatCodeQualityReport } = await import("./codequality-DTJK2LGF.js");
242
+ const { writeFileSync } = await import("fs");
243
+ writeFileSync(
244
+ codeQuality,
245
+ formatCodeQualityReport(review, process.env.CI_PROJECT_DIR ?? workspacePath),
246
+ "utf-8"
247
+ );
248
+ log(chalk.dim(`Wrote code quality report to ${codeQuality}`));
249
+ } catch (err) {
250
+ log(chalk.yellow(`Failed to write code quality report: ${err}`));
251
+ }
252
+ }
204
253
  if (post && prUrl) {
205
254
  log(chalk.cyan("\nPosting review to PR/MR..."));
206
- const result = await postReviewComment({
207
- prUrl,
208
- reviewText,
209
- model,
210
- metricsFooter,
211
- headSha
212
- });
255
+ const platform2 = detectPlatform(prUrl);
256
+ const useStructured = platform2 === "gitlab" && reviewStyle !== "summary";
257
+ let result;
258
+ if (useStructured) {
259
+ result = await postReviewStructured({
260
+ prUrl,
261
+ review,
262
+ model,
263
+ metricsFooter,
264
+ reviewStyle: reviewStyle ?? "hybrid",
265
+ commitStatus,
266
+ headSha,
267
+ workspacePath
268
+ });
269
+ } else {
270
+ result = await postReviewComment({
271
+ prUrl,
272
+ reviewText,
273
+ model,
274
+ metricsFooter,
275
+ headSha
276
+ });
277
+ if (platform2 === "gitlab" && commitStatus) {
278
+ try {
279
+ const { getGitlabMrDiffRefs } = await import("./gitlab-5TWHYHEP.js");
280
+ const parsed = parsePrUrl(prUrl);
281
+ const diffRefs = await getGitlabMrDiffRefs(
282
+ parsed.owner,
283
+ parsed.repo,
284
+ parsed.prNumber,
285
+ parsed.host
286
+ );
287
+ await postGitlabReviewCommitStatus(parsed, review, diffRefs);
288
+ } catch (err) {
289
+ result = {
290
+ success: false,
291
+ platform: "gitlab",
292
+ mrNumber: parsePrUrl(prUrl).prNumber,
293
+ error: `Failed to post commit status: ${err instanceof Error ? err.message : err}`
294
+ };
295
+ }
296
+ }
297
+ }
213
298
  if (result.success) {
214
299
  log(chalk.bold.green("Review posted successfully!"));
215
- log(chalk.dim(` ${platform === "github" ? "PR" : "MR"}: ${prUrl}`));
300
+ log(chalk.dim(` ${platform2 === "gitlab" ? "MR" : "PR"}: ${prUrl}`));
216
301
  } else {
217
302
  log(chalk.bold.red(`Failed to post review: ${result.error}`));
218
303
  log(chalk.yellow("\nReview output:\n"));
@@ -226,14 +311,18 @@ ${chalk.bold.cyan("Hodor - AI Code Review Agent")}`);
226
311
  }
227
312
  }
228
313
  if (prometheusPush) {
314
+ const labels = {
315
+ platform,
316
+ model,
317
+ verdict: review.overall_correctness === "patch is correct" ? "correct" : "incorrect"
318
+ };
319
+ if (metricsProject) labels.project = metricsProject;
320
+ if (metricsMrIid) labels.mr_iid = metricsMrIid;
229
321
  await pushMetrics({
230
322
  pushgatewayUrl: prometheusPush,
231
323
  metrics,
232
- labels: {
233
- platform,
234
- model,
235
- verdict: review.overall_correctness === "patch is correct" ? "correct" : "incorrect"
236
- }
324
+ findings: review.findings,
325
+ labels
237
326
  });
238
327
  }
239
328
  } catch (err) {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport \"dotenv/config\";\n\nimport { detectPlatform, postReviewComment, reviewPr } from \"./agent.js\";\nimport type { AgentProgressEvent } from \"./agent.js\";\nimport { renderMarkdown } from \"./render.js\";\nimport { pushMetrics } from \"./metrics.js\";\nimport { setLogLevel } from \"./utils/logger.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"hodor\")\n .description(\n \"AI-powered code review agent for GitHub PRs, GitLab MRs, and local diffs.\\n\\n\" +\n \"Hodor uses an AI agent that clones the repository, checks out the PR branch,\\n\" +\n \"and analyzes the code using tools (gh, git, glab) for metadata fetching and comment posting.\\n\\n\" +\n \"For local reviews, use --local with --diff-against to review changes in your current git repository.\",\n )\n .version(\"0.4.1\")\n .argument(\"[pr-url]\", \"URL of the GitHub PR or GitLab MR to review (optional with --local)\")\n .option(\n \"--model <model>\",\n \"LLM model to use (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-5)\",\n \"anthropic/claude-sonnet-4-5-20250929\",\n )\n .option(\n \"--reasoning-effort <level>\",\n \"Reasoning effort level: low, medium, high, xhigh\",\n )\n .option(\"-v, --verbose\", \"Enable verbose logging\", false)\n .option(\n \"--post\",\n \"Post the review directly to the PR/MR as a comment\",\n false,\n )\n .option(\"--prompt <text>\", \"Custom inline prompt text\")\n .option(\n \"--prompt-file <path>\",\n \"Path to file containing custom prompt instructions\",\n )\n .option(\n \"--workspace <dir>\",\n \"Workspace directory (creates temp dir if not specified)\",\n )\n .option(\n \"--ultrathink\",\n \"Enable maximum reasoning effort with extended thinking budget\",\n false,\n )\n .option(\n \"--bedrock-tags <json>\",\n \"JSON object of cost allocation tags for Bedrock requests (e.g., '{\\\"team\\\":\\\"platform\\\"}')\",\n )\n .option(\n \"--prometheus-push <url>\",\n \"Push review metrics to a Prometheus Pushgateway URL\",\n )\n .option(\n \"--local\",\n \"Review local changes in the current directory (no PR URL required)\",\n false,\n )\n .option(\n \"--diff-against <ref>\",\n \"Git ref to diff against in local mode (e.g., origin/main, HEAD~1)\",\n \"origin/main\",\n )\n .action(async (prUrl: string | undefined, cmdOpts: Record<string, unknown>) => {\n const verbose = cmdOpts.verbose as boolean;\n const post = cmdOpts.post as boolean;\n const model = cmdOpts.model as string;\n let reasoningEffort = cmdOpts.reasoningEffort as string | undefined;\n const prompt = cmdOpts.prompt as string | undefined;\n const promptFile = cmdOpts.promptFile as string | undefined;\n const workspace = cmdOpts.workspace as string | undefined;\n const ultrathink = cmdOpts.ultrathink as boolean;\n const bedrockTagsRaw = cmdOpts.bedrockTags as string | undefined;\n const prometheusPush = cmdOpts.prometheusPush as string | undefined;\n const localMode = cmdOpts.local as boolean;\n const diffAgainst = cmdOpts.diffAgainst as string;\n\n if (!localMode && !prUrl) {\n console.error(chalk.red(\"Error: pr-url is required unless --local is specified\"));\n process.exit(1);\n }\n if (localMode && post) {\n console.error(chalk.red(\"Error: --post is not supported in --local mode (no remote to post to)\"));\n process.exit(1);\n }\n\n // Auto-detect CI environment\n const isCI = !!(process.env.CI || process.env.GITLAB_CI || process.env.GITHUB_ACTIONS);\n\n if (verbose) setLogLevel(\"debug\");\n else if (isCI) setLogLevel(\"info\");\n\n // Handle ultrathink\n if (ultrathink) {\n reasoningEffort = \"high\";\n }\n\n // Parse Bedrock cost allocation tags\n let bedrockTags: Record<string, string> | null = null;\n if (bedrockTagsRaw) {\n try {\n bedrockTags = JSON.parse(bedrockTagsRaw) as Record<string, string>;\n } catch {\n console.error(chalk.red(\"Error: --bedrock-tags must be valid JSON\"));\n process.exit(1);\n }\n }\n\n const log = console.log;\n const logStream = process.stdout;\n\n const toolIcons: Record<string, string> = {\n bash: \"$\",\n read: \"cat\",\n grep: \"grep\",\n find: \"find\",\n ls: \"ls\",\n };\n\n /** Write a line to the log stream */\n function streamLog(msg: string): void {\n logStream.write(`${msg}\\n`);\n }\n\n /** Write inline text (no newline) for streaming deltas */\n function streamWrite(text: string): void {\n process.stderr.write(text);\n }\n\n function handleEvent(event: AgentProgressEvent): void {\n switch (event.type) {\n case \"agent_start\":\n streamLog(chalk.dim(\"▶ Agent started\"));\n break;\n case \"turn_start\":\n streamLog(chalk.dim(`\\n── Turn ${event.turnIndex ?? \"?\"} ──`));\n break;\n case \"tool_start\": {\n const icon = toolIcons[event.toolName ?? \"\"] ?? event.toolName;\n const preview = event.toolArgs ? ` ${event.toolArgs}` : \"\";\n const maxLen = 160;\n const truncated = preview.length > maxLen ? preview.slice(0, maxLen) + \"…\" : preview;\n streamLog(chalk.green(` ${icon}${truncated}`));\n break;\n }\n case \"tool_end\": {\n if (event.isError) {\n streamLog(chalk.red(` ✗ error`));\n }\n if (event.result) {\n const lines = event.result.split(\"\\n\");\n const maxLines = verbose ? 15 : 6;\n const maxChars = verbose ? 400 : 200;\n let chars = 0;\n for (let i = 0; i < Math.min(lines.length, maxLines); i++) {\n const line = lines[i];\n if (chars + line.length > maxChars) {\n streamLog(chalk.dim(` …(${lines.length - i} more lines)`));\n break;\n }\n streamLog(chalk.dim(` ${line}`));\n chars += line.length;\n }\n }\n break;\n }\n case \"text_delta\":\n if (verbose && event.delta) {\n streamWrite(event.delta);\n }\n break;\n case \"thinking_delta\":\n // Only show reasoning in verbose mode\n if (verbose && event.delta) {\n streamWrite(chalk.dim(event.delta));\n }\n break;\n case \"agent_end\":\n streamLog(chalk.dim(\"\\n▶ Extracting review...\"));\n break;\n }\n }\n\n try {\n // Detect platform and warn about missing tokens\n let platform: string = \"local\";\n if (!localMode && prUrl) {\n platform = detectPlatform(prUrl);\n const githubToken = process.env.GITHUB_TOKEN;\n const gitlabToken =\n process.env.GITLAB_TOKEN ??\n process.env.GITLAB_PRIVATE_TOKEN ??\n process.env.CI_JOB_TOKEN;\n\n if (platform === \"github\" && !githubToken) {\n console.error(chalk.yellow(\"Warning: GITHUB_TOKEN not set. You may encounter rate limits.\"));\n console.error(chalk.dim(\" Set GITHUB_TOKEN or run: gh auth login\\n\"));\n } else if (platform === \"gitlab\" && !gitlabToken) {\n console.error(chalk.yellow(\"Warning: No GitLab token detected. Set GITLAB_TOKEN (api scope).\"));\n console.error(chalk.dim(\" Export GITLAB_TOKEN and optionally GITLAB_HOST.\\n\"));\n }\n }\n\n log(`\\n${chalk.bold.cyan(\"Hodor - AI Code Review Agent\")}`);\n if (localMode) {\n log(chalk.dim(`Mode: Local diff review`));\n log(chalk.dim(`Diff against: ${diffAgainst}`));\n log(chalk.dim(`Workspace: ${workspace ?? process.cwd()}`));\n } else {\n log(chalk.dim(`Platform: ${platform.toUpperCase()}`));\n log(chalk.dim(`PR URL: ${prUrl}`));\n }\n log(chalk.dim(`Model: ${model}`));\n if (reasoningEffort) {\n log(chalk.dim(`Reasoning Effort: ${reasoningEffort}`));\n }\n log();\n\n streamLog(chalk.dim(\"▶ Setting up workspace...\"));\n const { review, metricsFooter, headSha, metrics } = await reviewPr({\n prUrl: localMode ? undefined : prUrl,\n model,\n reasoningEffort,\n customPrompt: prompt,\n promptFile,\n cleanup: !workspace,\n workspaceDir: workspace,\n includeMetricsFooter: post && !localMode,\n onEvent: handleEvent,\n bedrockTags,\n localMode,\n diffAgainst,\n });\n const reviewText = renderMarkdown(review);\n\n streamLog(chalk.green(\"✔ Review complete!\"));\n\n if (post && prUrl) {\n log(chalk.cyan(\"\\nPosting review to PR/MR...\"));\n\n const result = await postReviewComment({\n prUrl,\n reviewText,\n model,\n metricsFooter,\n headSha,\n });\n\n if (result.success) {\n log(chalk.bold.green(\"Review posted successfully!\"));\n log(chalk.dim(` ${platform === \"github\" ? \"PR\" : \"MR\"}: ${prUrl}`));\n } else {\n log(chalk.bold.red(`Failed to post review: ${result.error}`));\n log(chalk.yellow(\"\\nReview output:\\n\"));\n console.log(reviewText);\n }\n } else {\n log(chalk.bold.green(\"Review Complete\\n\"));\n console.log(reviewText);\n if (!localMode) {\n log(chalk.dim(\"\\nTip: Use --post to automatically post this review to the PR/MR\"));\n }\n }\n\n // Push metrics to Prometheus Pushgateway (best-effort, never fails the run)\n if (prometheusPush) {\n await pushMetrics({\n pushgatewayUrl: prometheusPush,\n metrics,\n labels: {\n platform,\n model,\n verdict: review.overall_correctness === \"patch is correct\" ? \"correct\" : \"incorrect\",\n },\n });\n }\n } catch (err) {\n streamLog(chalk.red(\"✗ Review failed\"));\n console.error(\n chalk.bold.red(`\\nError: ${err instanceof Error ? err.message : err}`),\n );\n if (verbose && err instanceof Error && err.stack) {\n console.error(chalk.dim(err.stack));\n }\n process.exit(1);\n }\n });\n\nprogram.parse();\n"],"mappings":";;;;;;;;;;;AAEA,SAAS,eAAe;AACxB,OAAO,WAAW;AAClB,OAAO;AAQP,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,OAAO,EACZ;AAAA,EACC;AAIF,EACC,QAAQ,OAAO,EACf,SAAS,YAAY,qEAAqE,EAC1F;AAAA,EACC;AAAA,EACA;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AACF,EACC,OAAO,iBAAiB,0BAA0B,KAAK,EACvD;AAAA,EACC;AAAA,EACA;AAAA,EACA;AACF,EACC,OAAO,mBAAmB,2BAA2B,EACrD;AAAA,EACC;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AACF,EACC,OAAO,OAAO,OAA2B,YAAqC;AAC7E,QAAM,UAAU,QAAQ;AACxB,QAAM,OAAO,QAAQ;AACrB,QAAM,QAAQ,QAAQ;AACtB,MAAI,kBAAkB,QAAQ;AAC9B,QAAM,SAAS,QAAQ;AACvB,QAAM,aAAa,QAAQ;AAC3B,QAAM,YAAY,QAAQ;AAC1B,QAAM,aAAa,QAAQ;AAC3B,QAAM,iBAAiB,QAAQ;AAC/B,QAAM,iBAAiB,QAAQ;AAC/B,QAAM,YAAY,QAAQ;AAC1B,QAAM,cAAc,QAAQ;AAE5B,MAAI,CAAC,aAAa,CAAC,OAAO;AACxB,YAAQ,MAAM,MAAM,IAAI,uDAAuD,CAAC;AAChF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI,aAAa,MAAM;AACrB,YAAQ,MAAM,MAAM,IAAI,uEAAuE,CAAC;AAChG,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,OAAO,CAAC,EAAE,QAAQ,IAAI,MAAM,QAAQ,IAAI,aAAa,QAAQ,IAAI;AAEvE,MAAI,QAAS,aAAY,OAAO;AAAA,WACvB,KAAM,aAAY,MAAM;AAGjC,MAAI,YAAY;AACd,sBAAkB;AAAA,EACpB;AAGA,MAAI,cAA6C;AACjD,MAAI,gBAAgB;AAClB,QAAI;AACF,oBAAc,KAAK,MAAM,cAAc;AAAA,IACzC,QAAQ;AACN,cAAQ,MAAM,MAAM,IAAI,0CAA0C,CAAC;AACnE,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,MAAM,QAAQ;AACpB,QAAM,YAAY,QAAQ;AAE1B,QAAM,YAAoC;AAAA,IACxC,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,IAAI;AAAA,EACN;AAGA,WAAS,UAAU,KAAmB;AACpC,cAAU,MAAM,GAAG,GAAG;AAAA,CAAI;AAAA,EAC5B;AAGA,WAAS,YAAY,MAAoB;AACvC,YAAQ,OAAO,MAAM,IAAI;AAAA,EAC3B;AAEA,WAAS,YAAY,OAAiC;AACpD,YAAQ,MAAM,MAAM;AAAA,MAClB,KAAK;AACH,kBAAU,MAAM,IAAI,sBAAiB,CAAC;AACtC;AAAA,MACF,KAAK;AACH,kBAAU,MAAM,IAAI;AAAA,oBAAa,MAAM,aAAa,GAAG,eAAK,CAAC;AAC7D;AAAA,MACF,KAAK,cAAc;AACjB,cAAM,OAAO,UAAU,MAAM,YAAY,EAAE,KAAK,MAAM;AACtD,cAAM,UAAU,MAAM,WAAW,IAAI,MAAM,QAAQ,KAAK;AACxD,cAAM,SAAS;AACf,cAAM,YAAY,QAAQ,SAAS,SAAS,QAAQ,MAAM,GAAG,MAAM,IAAI,WAAM;AAC7E,kBAAU,MAAM,MAAM,KAAK,IAAI,GAAG,SAAS,EAAE,CAAC;AAC9C;AAAA,MACF;AAAA,MACA,KAAK,YAAY;AACf,YAAI,MAAM,SAAS;AACjB,oBAAU,MAAM,IAAI,gBAAW,CAAC;AAAA,QAClC;AACA,YAAI,MAAM,QAAQ;AAChB,gBAAM,QAAQ,MAAM,OAAO,MAAM,IAAI;AACrC,gBAAM,WAAW,UAAU,KAAK;AAChC,gBAAM,WAAW,UAAU,MAAM;AACjC,cAAI,QAAQ;AACZ,mBAAS,IAAI,GAAG,IAAI,KAAK,IAAI,MAAM,QAAQ,QAAQ,GAAG,KAAK;AACzD,kBAAM,OAAO,MAAM,CAAC;AACpB,gBAAI,QAAQ,KAAK,SAAS,UAAU;AAClC,wBAAU,MAAM,IAAI,cAAS,MAAM,SAAS,CAAC,cAAc,CAAC;AAC5D;AAAA,YACF;AACA,sBAAU,MAAM,IAAI,OAAO,IAAI,EAAE,CAAC;AAClC,qBAAS,KAAK;AAAA,UAChB;AAAA,QACF;AACA;AAAA,MACF;AAAA,MACA,KAAK;AACH,YAAI,WAAW,MAAM,OAAO;AAC1B,sBAAY,MAAM,KAAK;AAAA,QACzB;AACA;AAAA,MACF,KAAK;AAEH,YAAI,WAAW,MAAM,OAAO;AAC1B,sBAAY,MAAM,IAAI,MAAM,KAAK,CAAC;AAAA,QACpC;AACA;AAAA,MACF,KAAK;AACH,kBAAU,MAAM,IAAI,+BAA0B,CAAC;AAC/C;AAAA,IACJ;AAAA,EACF;AAEA,MAAI;AAEF,QAAI,WAAmB;AACvB,QAAI,CAAC,aAAa,OAAO;AACvB,iBAAW,eAAe,KAAK;AAC/B,YAAM,cAAc,QAAQ,IAAI;AAChC,YAAM,cACJ,QAAQ,IAAI,gBACZ,QAAQ,IAAI,wBACZ,QAAQ,IAAI;AAEd,UAAI,aAAa,YAAY,CAAC,aAAa;AACzC,gBAAQ,MAAM,MAAM,OAAO,+DAA+D,CAAC;AAC3F,gBAAQ,MAAM,MAAM,IAAI,4CAA4C,CAAC;AAAA,MACvE,WAAW,aAAa,YAAY,CAAC,aAAa;AAChD,gBAAQ,MAAM,MAAM,OAAO,kEAAkE,CAAC;AAC9F,gBAAQ,MAAM,MAAM,IAAI,qDAAqD,CAAC;AAAA,MAChF;AAAA,IACF;AAEA,QAAI;AAAA,EAAK,MAAM,KAAK,KAAK,8BAA8B,CAAC,EAAE;AAC1D,QAAI,WAAW;AACb,UAAI,MAAM,IAAI,yBAAyB,CAAC;AACxC,UAAI,MAAM,IAAI,iBAAiB,WAAW,EAAE,CAAC;AAC7C,UAAI,MAAM,IAAI,cAAc,aAAa,QAAQ,IAAI,CAAC,EAAE,CAAC;AAAA,IAC3D,OAAO;AACL,UAAI,MAAM,IAAI,aAAa,SAAS,YAAY,CAAC,EAAE,CAAC;AACpD,UAAI,MAAM,IAAI,WAAW,KAAK,EAAE,CAAC;AAAA,IACnC;AACA,QAAI,MAAM,IAAI,UAAU,KAAK,EAAE,CAAC;AAChC,QAAI,iBAAiB;AACnB,UAAI,MAAM,IAAI,qBAAqB,eAAe,EAAE,CAAC;AAAA,IACvD;AACA,QAAI;AAEJ,cAAU,MAAM,IAAI,gCAA2B,CAAC;AAChD,UAAM,EAAE,QAAQ,eAAe,SAAS,QAAQ,IAAI,MAAM,SAAS;AAAA,MACjE,OAAO,YAAY,SAAY;AAAA,MAC/B;AAAA,MACA;AAAA,MACA,cAAc;AAAA,MACd;AAAA,MACA,SAAS,CAAC;AAAA,MACV,cAAc;AAAA,MACd,sBAAsB,QAAQ,CAAC;AAAA,MAC/B,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,UAAM,aAAa,eAAe,MAAM;AAExC,cAAU,MAAM,MAAM,yBAAoB,CAAC;AAE3C,QAAI,QAAQ,OAAO;AACjB,UAAI,MAAM,KAAK,8BAA8B,CAAC;AAE9C,YAAM,SAAS,MAAM,kBAAkB;AAAA,QACrC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,UAAI,OAAO,SAAS;AAClB,YAAI,MAAM,KAAK,MAAM,6BAA6B,CAAC;AACnD,YAAI,MAAM,IAAI,KAAK,aAAa,WAAW,OAAO,IAAI,KAAK,KAAK,EAAE,CAAC;AAAA,MACrE,OAAO;AACL,YAAI,MAAM,KAAK,IAAI,0BAA0B,OAAO,KAAK,EAAE,CAAC;AAC5D,YAAI,MAAM,OAAO,oBAAoB,CAAC;AACtC,gBAAQ,IAAI,UAAU;AAAA,MACxB;AAAA,IACF,OAAO;AACL,UAAI,MAAM,KAAK,MAAM,mBAAmB,CAAC;AACzC,cAAQ,IAAI,UAAU;AACtB,UAAI,CAAC,WAAW;AACd,YAAI,MAAM,IAAI,kEAAkE,CAAC;AAAA,MACnF;AAAA,IACF;AAGA,QAAI,gBAAgB;AAClB,YAAM,YAAY;AAAA,QAChB,gBAAgB;AAAA,QAChB;AAAA,QACA,QAAQ;AAAA,UACN;AAAA,UACA;AAAA,UACA,SAAS,OAAO,wBAAwB,qBAAqB,YAAY;AAAA,QAC3E;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,cAAU,MAAM,IAAI,sBAAiB,CAAC;AACtC,YAAQ;AAAA,MACN,MAAM,KAAK,IAAI;AAAA,SAAY,eAAe,QAAQ,IAAI,UAAU,GAAG,EAAE;AAAA,IACvE;AACA,QAAI,WAAW,eAAe,SAAS,IAAI,OAAO;AAChD,cAAQ,MAAM,MAAM,IAAI,IAAI,KAAK,CAAC;AAAA,IACpC;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AAEH,QAAQ,MAAM;","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport \"dotenv/config\";\n\nimport { detectPlatform, parsePrUrl, postGitlabReviewCommitStatus, postReviewComment, postReviewStructured, reviewPr } from \"./agent.js\";\nimport type { AgentProgressEvent } from \"./agent.js\";\nimport type { PostCommentResult } from \"./types.js\";\nimport { renderMarkdown } from \"./render.js\";\nimport { pushMetrics } from \"./metrics.js\";\nimport { setLogLevel } from \"./utils/logger.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"hodor\")\n .description(\n \"AI-powered code review agent for GitHub PRs, GitLab MRs, Gitea/Forgejo PRs, and local diffs.\\n\\n\" +\n \"Hodor uses an AI agent that clones the repository, checks out the PR branch,\\n\" +\n \"and analyzes the code using tools (gh, git, glab) for metadata fetching and comment posting.\\n\\n\" +\n \"For local reviews, use --local with --diff-against to review changes in your current git repository.\",\n )\n .version(\"0.6.1\")\n .argument(\"[pr-url]\", \"URL of the GitHub PR, GitLab MR, or Gitea/Forgejo PR to review (optional with --local)\")\n .option(\n \"--model <model>\",\n \"LLM model to use as provider/model-id (e.g., anthropic/claude-sonnet-4-5-20250929, openrouter/moonshotai/kimi-k2.6)\",\n \"anthropic/claude-sonnet-4-5-20250929\",\n )\n .option(\n \"--reasoning-effort <level>\",\n \"Reasoning effort level: minimal, low, medium, high, xhigh\",\n )\n .option(\"-v, --verbose\", \"Enable verbose logging\", false)\n .option(\n \"--post\",\n \"Post the review directly to the PR/MR as a comment\",\n false,\n )\n .option(\"--prompt <text>\", \"Custom inline prompt text\")\n .option(\n \"--prompt-file <path>\",\n \"Path to file containing custom prompt instructions\",\n )\n .option(\n \"--workspace <dir>\",\n \"Workspace directory (creates temp dir if not specified)\",\n )\n .option(\n \"--review-style <style>\",\n \"How to post reviews on GitLab: summary (single comment), inline (diff comments), hybrid (both). Default: hybrid.\",\n \"hybrid\",\n )\n .option(\n \"--code-quality <path>\",\n \"Write a gl-code-quality-report.json CodeClimate artifact to this path\",\n )\n .option(\n \"--commit-status\",\n \"Post a pass/fail commit status to the MR head SHA\",\n false,\n )\n .option(\n \"--ultrathink\",\n \"Enable maximum reasoning effort with extended thinking budget\",\n false,\n )\n .option(\n \"--bedrock-tags <json>\",\n \"JSON object of cost allocation tags for Bedrock requests (e.g., '{\\\"team\\\":\\\"platform\\\"}')\",\n )\n .option(\n \"--prometheus-push <url>\",\n \"Push review metrics to a Prometheus Pushgateway URL\",\n )\n .option(\n \"--local\",\n \"Review local changes in the current directory (no PR URL required)\",\n false,\n )\n .option(\n \"--diff-against <ref>\",\n \"Git ref to diff against in local mode (e.g., origin/main, HEAD~1)\",\n \"origin/main\",\n )\n .action(async (prUrl: string | undefined, cmdOpts: Record<string, unknown>) => {\n const verbose = cmdOpts.verbose as boolean;\n const post = cmdOpts.post as boolean;\n const model = cmdOpts.model as string;\n let reasoningEffort = cmdOpts.reasoningEffort as string | undefined;\n const prompt = cmdOpts.prompt as string | undefined;\n const promptFile = cmdOpts.promptFile as string | undefined;\n const workspace = cmdOpts.workspace as string | undefined;\n const reviewStyle = cmdOpts.reviewStyle as \"summary\" | \"inline\" | \"hybrid\" | undefined;\n const codeQuality = cmdOpts.codeQuality as string | undefined;\n const commitStatus = cmdOpts.commitStatus as boolean;\n const ultrathink = cmdOpts.ultrathink as boolean;\n const bedrockTagsRaw = cmdOpts.bedrockTags as string | undefined;\n const prometheusPush = cmdOpts.prometheusPush as string | undefined;\n const localMode = cmdOpts.local as boolean;\n const diffAgainst = cmdOpts.diffAgainst as string;\n\n if (!localMode && !prUrl) {\n console.error(chalk.red(\"Error: pr-url is required unless --local is specified\"));\n process.exit(1);\n }\n if (localMode && post) {\n console.error(chalk.red(\"Error: --post is not supported in --local mode (no remote to post to)\"));\n process.exit(1);\n }\n if (![\"summary\", \"inline\", \"hybrid\"].includes(reviewStyle ?? \"hybrid\")) {\n console.error(chalk.red(\"Error: --review-style must be one of: summary, inline, hybrid\"));\n process.exit(1);\n }\n\n // Auto-detect CI environment\n const isCI = !!(process.env.CI || process.env.GITLAB_CI || process.env.GITHUB_ACTIONS || process.env.GITEA_ACTIONS || process.env.FORGEJO_ACTIONS);\n\n if (verbose) setLogLevel(\"debug\");\n else if (isCI) setLogLevel(\"info\");\n\n // Handle ultrathink\n if (ultrathink) {\n reasoningEffort = \"xhigh\";\n }\n\n // Parse Bedrock cost allocation tags\n let bedrockTags: Record<string, string> | null = null;\n if (bedrockTagsRaw) {\n try {\n bedrockTags = JSON.parse(bedrockTagsRaw) as Record<string, string>;\n } catch {\n console.error(chalk.red(\"Error: --bedrock-tags must be valid JSON\"));\n process.exit(1);\n }\n }\n\n const log = console.log;\n const logStream = process.stdout;\n\n const toolIcons: Record<string, string> = {\n bash: \"$\",\n read: \"cat\",\n grep: \"grep\",\n find: \"find\",\n ls: \"ls\",\n };\n\n /** Write a line to the log stream */\n function streamLog(msg: string): void {\n logStream.write(`${msg}\\n`);\n }\n\n /** Write inline text (no newline) for streaming deltas */\n function streamWrite(text: string): void {\n process.stderr.write(text);\n }\n\n function handleEvent(event: AgentProgressEvent): void {\n switch (event.type) {\n case \"agent_start\":\n streamLog(chalk.dim(\"▶ Agent started\"));\n break;\n case \"turn_start\":\n streamLog(chalk.dim(`\\n── Turn ${event.turnIndex ?? \"?\"} ──`));\n break;\n case \"tool_start\": {\n const icon = toolIcons[event.toolName ?? \"\"] ?? event.toolName;\n const preview = event.toolArgs ? ` ${event.toolArgs}` : \"\";\n const maxLen = 160;\n const truncated = preview.length > maxLen ? preview.slice(0, maxLen) + \"…\" : preview;\n streamLog(chalk.green(` ${icon}${truncated}`));\n break;\n }\n case \"tool_end\": {\n if (event.isError) {\n streamLog(chalk.red(` ✗ error`));\n }\n if (event.result) {\n const lines = event.result.split(\"\\n\");\n const maxLines = verbose ? 15 : 6;\n const maxChars = verbose ? 400 : 200;\n let chars = 0;\n for (let i = 0; i < Math.min(lines.length, maxLines); i++) {\n const line = lines[i];\n if (chars + line.length > maxChars) {\n streamLog(chalk.dim(` …(${lines.length - i} more lines)`));\n break;\n }\n streamLog(chalk.dim(` ${line}`));\n chars += line.length;\n }\n }\n break;\n }\n case \"text_delta\":\n if (verbose && event.delta) {\n streamWrite(event.delta);\n }\n break;\n case \"thinking_delta\":\n // Only show reasoning in verbose mode\n if (verbose && event.delta) {\n streamWrite(chalk.dim(event.delta));\n }\n break;\n case \"agent_end\":\n streamLog(chalk.dim(\"\\n▶ Extracting review...\"));\n break;\n }\n }\n\n try {\n // Detect platform and warn about missing tokens\n let platform: string = \"local\";\n let metricsProject: string | undefined;\n let metricsMrIid: string | undefined;\n if (!localMode && prUrl) {\n platform = detectPlatform(prUrl);\n const parsedPr = parsePrUrl(prUrl);\n metricsProject = `${parsedPr.owner}/${parsedPr.repo}`;\n metricsMrIid = String(parsedPr.prNumber);\n const githubToken = process.env.GITHUB_TOKEN;\n const gitlabToken =\n process.env.GITLAB_TOKEN ??\n process.env.GITLAB_PRIVATE_TOKEN ??\n process.env.CI_JOB_TOKEN;\n\n if (platform === \"github\" && !githubToken) {\n console.error(chalk.yellow(\"Warning: GITHUB_TOKEN not set. You may encounter rate limits.\"));\n console.error(chalk.dim(\" Set GITHUB_TOKEN or run: gh auth login\\n\"));\n } else if (platform === \"gitlab\" && !gitlabToken) {\n console.error(chalk.yellow(\"Warning: No GitLab token detected. Set GITLAB_TOKEN (api scope).\"));\n console.error(chalk.dim(\" Export GITLAB_TOKEN and optionally GITLAB_HOST.\\n\"));\n } else if (platform === \"gitea\") {\n const giteaToken = process.env.GITEA_TOKEN ?? process.env.FORGEJO_TOKEN;\n if (!giteaToken) {\n console.error(chalk.yellow(\"Warning: No Gitea/Forgejo token detected. Set GITEA_TOKEN for authentication.\"));\n console.error(chalk.dim(\" Export GITEA_TOKEN (or FORGEJO_TOKEN) for API access.\\n\"));\n }\n }\n }\n\n log(`\\n${chalk.bold.cyan(\"Hodor - AI Code Review Agent\")}`);\n if (localMode) {\n log(chalk.dim(`Mode: Local diff review`));\n log(chalk.dim(`Diff against: ${diffAgainst}`));\n log(chalk.dim(`Workspace: ${workspace ?? process.cwd()}`));\n } else {\n log(chalk.dim(`Platform: ${platform.toUpperCase()}`));\n log(chalk.dim(`PR URL: ${prUrl}`));\n }\n log(chalk.dim(`Model: ${model}`));\n if (reasoningEffort) {\n log(chalk.dim(`Reasoning Effort: ${reasoningEffort}`));\n }\n log();\n\n streamLog(chalk.dim(\"▶ Setting up workspace...\"));\n const { review, metricsFooter, headSha, metrics, workspacePath } = await reviewPr({\n prUrl: localMode ? undefined : prUrl,\n model,\n reasoningEffort,\n customPrompt: prompt,\n promptFile,\n cleanup: !workspace,\n workspaceDir: workspace,\n includeMetricsFooter: post && !localMode,\n onEvent: handleEvent,\n bedrockTags,\n localMode,\n diffAgainst,\n });\n const reviewText = renderMarkdown(review);\n\n streamLog(chalk.green(\"✔ Review complete!\"));\n\n if (codeQuality) {\n try {\n const { formatCodeQualityReport } = await import(\"./codequality.js\");\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(\n codeQuality,\n formatCodeQualityReport(review, process.env.CI_PROJECT_DIR ?? workspacePath),\n \"utf-8\",\n );\n log(chalk.dim(`Wrote code quality report to ${codeQuality}`));\n } catch (err) {\n log(chalk.yellow(`Failed to write code quality report: ${err}`));\n }\n }\n\n if (post && prUrl) {\n log(chalk.cyan(\"\\nPosting review to PR/MR...\"));\n\n const platform = detectPlatform(prUrl);\n const useStructured = platform === \"gitlab\" && reviewStyle !== \"summary\";\n\n let result: PostCommentResult;\n if (useStructured) {\n result = await postReviewStructured({\n prUrl,\n review,\n model,\n metricsFooter,\n reviewStyle: reviewStyle ?? \"hybrid\",\n commitStatus,\n headSha,\n workspacePath,\n });\n } else {\n result = await postReviewComment({\n prUrl,\n reviewText,\n model,\n metricsFooter,\n headSha,\n });\n\n if (platform === \"gitlab\" && commitStatus) {\n try {\n const { getGitlabMrDiffRefs } = await import(\"./gitlab.js\");\n const parsed = parsePrUrl(prUrl);\n const diffRefs = await getGitlabMrDiffRefs(\n parsed.owner,\n parsed.repo,\n parsed.prNumber,\n parsed.host,\n );\n await postGitlabReviewCommitStatus(parsed, review, diffRefs);\n } catch (err) {\n result = {\n success: false,\n platform: \"gitlab\",\n mrNumber: parsePrUrl(prUrl).prNumber,\n error: `Failed to post commit status: ${err instanceof Error ? err.message : err}`,\n };\n }\n }\n }\n\n if (result.success) {\n log(chalk.bold.green(\"Review posted successfully!\"));\n log(chalk.dim(` ${platform === \"gitlab\" ? \"MR\" : \"PR\"}: ${prUrl}`));\n } else {\n log(chalk.bold.red(`Failed to post review: ${result.error}`));\n log(chalk.yellow(\"\\nReview output:\\n\"));\n console.log(reviewText);\n }\n } else {\n log(chalk.bold.green(\"Review Complete\\n\"));\n console.log(reviewText);\n if (!localMode) {\n log(chalk.dim(\"\\nTip: Use --post to automatically post this review to the PR/MR\"));\n }\n }\n\n // Push metrics to Prometheus Pushgateway (best-effort, never fails the run)\n if (prometheusPush) {\n const labels: Record<string, string> = {\n platform,\n model,\n verdict: review.overall_correctness === \"patch is correct\" ? \"correct\" : \"incorrect\",\n };\n if (metricsProject) labels.project = metricsProject;\n if (metricsMrIid) labels.mr_iid = metricsMrIid;\n\n await pushMetrics({\n pushgatewayUrl: prometheusPush,\n metrics,\n findings: review.findings,\n labels,\n });\n }\n } catch (err) {\n streamLog(chalk.red(\"✗ Review failed\"));\n console.error(\n chalk.bold.red(`\\nError: ${err instanceof Error ? err.message : err}`),\n );\n if (verbose && err instanceof Error && err.stack) {\n console.error(chalk.dim(err.stack));\n }\n process.exit(1);\n }\n });\n\nprogram.parse();\n"],"mappings":"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","names":["platform"]}
@@ -0,0 +1,42 @@
1
+ import {
2
+ relativizeWorkspacePath
3
+ } from "./chunk-AMUK6GDX.js";
4
+
5
+ // src/codequality.ts
6
+ import { createHash } from "crypto";
7
+ var PRIORITY_TO_SEVERITY = {
8
+ 0: "critical",
9
+ 1: "major",
10
+ 2: "minor",
11
+ 3: "info"
12
+ };
13
+ function fingerprint(finding, relativePath) {
14
+ const input = `${finding.title}:${relativePath}:${finding.code_location.line_range.start}`;
15
+ return createHash("md5").update(input).digest("hex");
16
+ }
17
+ function formatCodeQualityReport(review, workspacePrefix) {
18
+ const issues = review.findings.map((finding) => {
19
+ const relPath = relativizeWorkspacePath(finding.code_location.absolute_file_path, workspacePrefix);
20
+ return {
21
+ type: "issue",
22
+ check_name: `hodor/P${finding.priority}`,
23
+ description: finding.title,
24
+ content: { body: finding.body },
25
+ categories: ["Bug Risk"],
26
+ severity: PRIORITY_TO_SEVERITY[finding.priority] ?? "info",
27
+ location: {
28
+ path: relPath,
29
+ lines: {
30
+ begin: finding.code_location.line_range.start,
31
+ end: finding.code_location.line_range.end
32
+ }
33
+ },
34
+ fingerprint: fingerprint(finding, relPath)
35
+ };
36
+ });
37
+ return JSON.stringify(issues, null, 2);
38
+ }
39
+ export {
40
+ formatCodeQualityReport
41
+ };
42
+ //# sourceMappingURL=codequality-DTJK2LGF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/codequality.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { ReviewOutput, ReviewFinding, ReviewPriority } from \"./types.js\";\nimport { relativizeWorkspacePath } from \"./utils/path.js\";\n\nconst PRIORITY_TO_SEVERITY: Record<ReviewPriority, string> = {\n 0: \"critical\",\n 1: \"major\",\n 2: \"minor\",\n 3: \"info\",\n};\n\nfunction fingerprint(finding: ReviewFinding, relativePath: string): string {\n const input = `${finding.title}:${relativePath}:${finding.code_location.line_range.start}`;\n return createHash(\"md5\").update(input).digest(\"hex\");\n}\n\nexport function formatCodeQualityReport(\n review: ReviewOutput,\n workspacePrefix?: string,\n): string {\n const issues = review.findings.map((finding) => {\n const relPath = relativizeWorkspacePath(finding.code_location.absolute_file_path, workspacePrefix);\n return {\n type: \"issue\",\n check_name: `hodor/P${finding.priority}`,\n description: finding.title,\n content: { body: finding.body },\n categories: [\"Bug Risk\"],\n severity: PRIORITY_TO_SEVERITY[finding.priority] ?? \"info\",\n location: {\n path: relPath,\n lines: {\n begin: finding.code_location.line_range.start,\n end: finding.code_location.line_range.end,\n },\n },\n fingerprint: fingerprint(finding, relPath),\n };\n });\n\n return JSON.stringify(issues, null, 2);\n}\n"],"mappings":";;;;;AAAA,SAAS,kBAAkB;AAI3B,IAAM,uBAAuD;AAAA,EAC3D,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AAEA,SAAS,YAAY,SAAwB,cAA8B;AACzE,QAAM,QAAQ,GAAG,QAAQ,KAAK,IAAI,YAAY,IAAI,QAAQ,cAAc,WAAW,KAAK;AACxF,SAAO,WAAW,KAAK,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACrD;AAEO,SAAS,wBACd,QACA,iBACQ;AACR,QAAM,SAAS,OAAO,SAAS,IAAI,CAAC,YAAY;AAC9C,UAAM,UAAU,wBAAwB,QAAQ,cAAc,oBAAoB,eAAe;AACjG,WAAO;AAAA,MACL,MAAM;AAAA,MACN,YAAY,UAAU,QAAQ,QAAQ;AAAA,MACtC,aAAa,QAAQ;AAAA,MACrB,SAAS,EAAE,MAAM,QAAQ,KAAK;AAAA,MAC9B,YAAY,CAAC,UAAU;AAAA,MACvB,UAAU,qBAAqB,QAAQ,QAAQ,KAAK;AAAA,MACpD,UAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,UACL,OAAO,QAAQ,cAAc,WAAW;AAAA,UACxC,KAAK,QAAQ,cAAc,WAAW;AAAA,QACxC;AAAA,MACF;AAAA,MACA,aAAa,YAAY,SAAS,OAAO;AAAA,IAC3C;AAAA,EACF,CAAC;AAED,SAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;AACvC;","names":[]}
@@ -0,0 +1,33 @@
1
+ import {
2
+ GitLabAPIError,
3
+ HODOR_REVIEW_MARKER,
4
+ bulkPublishGitlabDraftNotes,
5
+ cleanupHodorComments,
6
+ createGitlabDraftNote,
7
+ fetchGitlabMrInfo,
8
+ getGitlabMrDiffRefs,
9
+ listHodorDiscussions,
10
+ parseGlabPaginatedJson,
11
+ postGitlabCommitStatus,
12
+ postGitlabInlineComment,
13
+ postGitlabMrComment,
14
+ resolveGitlabDiscussions,
15
+ summarizeGitlabNotes
16
+ } from "./chunk-DVJVQTVW.js";
17
+ export {
18
+ GitLabAPIError,
19
+ HODOR_REVIEW_MARKER,
20
+ bulkPublishGitlabDraftNotes,
21
+ cleanupHodorComments,
22
+ createGitlabDraftNote,
23
+ fetchGitlabMrInfo,
24
+ getGitlabMrDiffRefs,
25
+ listHodorDiscussions,
26
+ parseGlabPaginatedJson,
27
+ postGitlabCommitStatus,
28
+ postGitlabInlineComment,
29
+ postGitlabMrComment,
30
+ resolveGitlabDiscussions,
31
+ summarizeGitlabNotes
32
+ };
33
+ //# sourceMappingURL=gitlab-5TWHYHEP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- type Platform = "github" | "gitlab";
1
+ import { ThinkingLevel } from '@earendil-works/pi-ai';
2
+
3
+ type Platform = "github" | "gitlab" | "gitea";
2
4
  interface ParsedPrUrl {
3
5
  owner: string;
4
6
  repo: string;
@@ -61,6 +63,10 @@ interface ReviewFinding {
61
63
  end: number;
62
64
  };
63
65
  };
66
+ /** Verbatim copy of the source lines the finding refers to, used to resolve
67
+ * line_range against the on-disk file. Optional; falls back to line_range. */
68
+ existing_code?: string;
69
+ suggestion?: string;
64
70
  }
65
71
  interface ReviewOutput {
66
72
  findings: ReviewFinding[];
@@ -75,6 +81,15 @@ interface PostCommentResult {
75
81
  error?: string;
76
82
  }
77
83
 
84
+ /**
85
+ * Render structured review output into clean markdown for PR/MR comments.
86
+ */
87
+
88
+ /**
89
+ * Render a ReviewOutput into clean markdown for posting as a PR/MR comment.
90
+ */
91
+ declare function renderMarkdown(review: ReviewOutput): string;
92
+
78
93
  interface AgentProgressEvent {
79
94
  type: "tool_start" | "tool_end" | "thinking" | "turn_start" | "turn_end" | "agent_start" | "agent_end" | "text_delta" | "thinking_delta" | "tool_result";
80
95
  toolName?: string;
@@ -111,6 +126,7 @@ declare function reviewPr(opts: {
111
126
  metricsFooter: string | null;
112
127
  headSha: string | null;
113
128
  metrics: ReviewMetrics;
129
+ workspacePath: string;
114
130
  }>;
115
131
 
116
132
  declare function buildPrReviewPrompt(opts: {
@@ -139,19 +155,22 @@ declare function parseModelString(model: string): ParsedModel;
139
155
  * Map reasoning effort strings to pi-ai thinking levels.
140
156
  * Returns undefined for no reasoning.
141
157
  */
142
- declare function mapReasoningEffort(effort: string | undefined): "low" | "medium" | "high" | undefined;
158
+ declare function mapReasoningEffort(effort: string | undefined): ThinkingLevel | undefined;
143
159
  /**
144
160
  * Get API key with provider-aware selection.
145
161
  *
146
162
  * Priority:
147
163
  * 1. LLM_API_KEY (universal override)
148
- * 2. Provider-specific key (ANTHROPIC_API_KEY, OPENAI_API_KEY)
149
- * 3. Fallback to any available key
164
+ * 2. Provider-specific key known by pi-ai (ANTHROPIC_API_KEY, OPENAI_API_KEY,
165
+ * OPENROUTER_API_KEY, etc.)
150
166
  *
151
167
  * Returns null for bedrock (uses AWS credentials).
152
168
  */
153
169
  declare function getApiKey(model?: string): string | null;
154
170
 
171
+ type FindingPriority = {
172
+ priority: number;
173
+ };
155
174
  declare function formatMetricsMarkdown(metrics: ReviewMetrics): string;
156
175
  declare function printMetrics(metrics: ReviewMetrics, stream?: NodeJS.WritableStream): void;
157
176
  /**
@@ -161,18 +180,10 @@ declare function printMetrics(metrics: ReviewMetrics, stream?: NodeJS.WritableSt
161
180
  declare function pushMetrics(opts: {
162
181
  pushgatewayUrl: string;
163
182
  metrics: ReviewMetrics;
183
+ findings?: FindingPriority[];
164
184
  labels?: Record<string, string>;
165
185
  }): Promise<void>;
166
186
 
167
187
  declare function validateReviewOutput(review: ReviewOutput): ReviewOutput;
168
188
 
169
- /**
170
- * Render structured review output into clean markdown for PR/MR comments.
171
- */
172
-
173
- /**
174
- * Render a ReviewOutput into clean markdown for posting as a PR/MR comment.
175
- */
176
- declare function renderMarkdown(review: ReviewOutput): string;
177
-
178
189
  export { type AgentProgressEvent, type MrMetadata, type NoteEntry, type ParsedPrUrl, type Platform, type PostCommentResult, type ReviewCorrectness, type ReviewFinding, type ReviewMetrics, type ReviewOutput, type ReviewPriority, buildPrReviewPrompt, detectPlatform, formatMetricsMarkdown, getApiKey, mapReasoningEffort, parseModelString, parsePrUrl, postReviewComment, printMetrics, pushMetrics, renderMarkdown, reviewPr, validateReviewOutput };
package/dist/index.js CHANGED
@@ -9,10 +9,13 @@ import {
9
9
  postReviewComment,
10
10
  printMetrics,
11
11
  pushMetrics,
12
- renderMarkdown,
13
12
  reviewPr,
14
13
  validateReviewOutput
15
- } from "./chunk-QGUJENIG.js";
14
+ } from "./chunk-MRMLGIXO.js";
15
+ import {
16
+ renderMarkdown
17
+ } from "./chunk-DVJVQTVW.js";
18
+ import "./chunk-AMUK6GDX.js";
16
19
  export {
17
20
  buildPrReviewPrompt,
18
21
  detectPlatform,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.5.0",
6
+ "version": "0.6.2-rc.2",
7
7
  "description": "AI-powered code review agent that finds bugs, security issues, and logic errors in pull requests",
8
8
  "type": "module",
9
9
  "main": "dist/index.js",
@@ -43,19 +43,19 @@
43
43
  "node": ">=22"
44
44
  },
45
45
  "dependencies": {
46
- "@sinclair/typebox": "^0.34.48",
47
- "@anthropic-ai/sdk": "^0.39.0",
48
- "@mariozechner/pi-ai": "^0.62.0",
49
- "@mariozechner/pi-coding-agent": "^0.62.0",
50
- "chalk": "^5.4.0",
51
- "commander": "^13.1.0",
52
- "dotenv": "^16.4.0"
46
+ "@sinclair/typebox": "^0.34.49",
47
+ "@anthropic-ai/sdk": "^0.95.2",
48
+ "@earendil-works/pi-ai": "^0.74.0",
49
+ "@earendil-works/pi-coding-agent": "^0.74.0",
50
+ "chalk": "^5.6.2",
51
+ "commander": "^14.0.3",
52
+ "dotenv": "^17.4.2"
53
53
  },
54
54
  "devDependencies": {
55
- "@types/node": "^22.0.0",
56
- "tsup": "^8.0.0",
57
- "tsx": "^4.0.0",
58
- "typescript": "^5.7.0",
59
- "vitest": "^3.0.0"
55
+ "@types/node": "^25.7.0",
56
+ "tsup": "^8.5.1",
57
+ "tsx": "^4.21.0",
58
+ "typescript": "^6.0.3",
59
+ "vitest": "^4.1.6"
60
60
  }
61
61
  }
@@ -72,6 +72,16 @@ Always include the matching numeric priority field in the `submit_review` payloa
72
72
 
73
73
  Output all findings that the original author would fix if they knew about it. If there is no finding that a person would definitely love to see and fix, prefer outputting no findings. Do not stop at the first qualifying finding. Continue until you've listed every qualifying finding.
74
74
 
75
+ ### Contract Trace Checklist
76
+
77
+ For changes that introduce or modify routes, handlers, API parameters, auth/session/token logic, database schema or queries, cache keys, config contracts, or public interfaces:
78
+
79
+ 1. Trace each externally supplied value through the layers it crosses: route/query/body/header → handler extraction and parsing → service method signature → DB/query/cache key → tests or mocks.
80
+ 2. Compare semantic identity, not just variable names. Examples: public `user_id` string vs internal integer primary key, client/account ID vs database UID, token key vs full token value, app ID vs API key, enum label vs stored code, timestamp units/timezones, paise vs rupees.
81
+ 3. Read the minimal adjacent convention needed when a changed file depends on it: nearby routes for the same resource, model/schema definitions, query files, auth middleware, or changed tests.
82
+ 4. Treat a semantic mismatch as a concrete bug when production input will pass one kind of value but the changed code stores, queries, authorizes, or tests a different kind.
83
+ 5. Keep this scoped to the diff. Do not browse unrelated code unless it defines a contract that the changed lines directly depend on.
84
+
75
85
  ### Additional Guidelines
76
86
 
77
87
  - Ignore trivial style unless it obscures meaning or violates documented standards.
@@ -82,10 +92,30 @@ Output all findings that the original author would fix if they knew about it. If
82
92
 
83
93
  {review_process_section}
84
94
 
85
- **Analysis Focus:**
86
- - Check edge cases: empty inputs, null values, boundary conditions, error paths
87
- - Think: What user input or race condition breaks this?
88
- - Focus on the changes (+ and - lines), use full file context sparingly
95
+ ### Conditional Review Lenses
96
+
97
+ After identifying the changed files and diff hunks, classify which lenses apply. Apply only the relevant lenses below; do not run a broad style review just because a lens exists.
98
+
99
+ - **Silent failure / error handling lens**: If the diff changes `try`/`catch`, rescue/recover blocks, fallback logic, retries, default values after failures, logging, metrics, or error returns, verify that failures are not swallowed, fallbacks are explicit and safe, and operators/users get enough context to debug the failing operation. Flag only concrete silent failures or misleading success paths.
100
+ - **Critical test gap lens**: If the diff adds behavior, changes edge-case handling, alters async/concurrent behavior, or fixes a bug, compare changed behavior against changed or existing tests. Flag missing tests only when the gap is likely to let a meaningful regression through; do not require line coverage or tests for trivial code.
101
+ - **Comment/documentation accuracy lens**: If the diff adds or modifies comments, docstrings, README snippets, API docs, or examples, verify the prose matches the changed code and commands. Flag comments or docs that are factually wrong, misleading, or likely to cause misuse; ignore harmless wording preferences.
102
+ - **Type/API invariant lens**: If the diff adds or changes types, schemas, DTOs, request/response shapes, config objects, database models, enums, or public interfaces, check whether invalid states became representable, required validation moved out of the boundary, or consumers can now pass semantically wrong values. Prefer concrete invariant or compatibility bugs over abstract design advice.
103
+ - **Simplification lens**: If the diff introduces complex branching, duplicated logic, clever one-liners, or unnecessary abstraction, mention it only when the complexity creates a plausible bug, hides an important invariant, or makes future fixes risky. Do not suggest cosmetic refactors.
104
+
105
+ ### Attack Surface Analysis
106
+
107
+ When the diff touches the areas below, investigate them specifically — do not skip a category unless it is provably inapplicable to the changed code:
108
+
109
+ - **Race conditions / TOCTOU**: Shared mutable state, non-atomic check-then-act sequences, missing locks or transactions
110
+ - **Off-by-one / boundary errors**: Loop bounds, slice indices, size comparisons (`<` vs `<=`), pagination math, overflow
111
+ - **Schema / contract drift**: Renamed fields, changed types, enum values, units (paise vs rupees, ms vs s), encoding assumptions, wire format changes
112
+ - **Auth / permission gaps**: New routes or handlers missing auth middleware, privilege escalation paths, token or session misuse, missing ownership checks
113
+ - **Rollback safety**: Migrations that cannot be rolled back cleanly, stateful side effects committed before DB transactions complete, partial-write failure modes
114
+ - **Data loss / silent discard**: Early returns or swallowed errors that drop user data, leave state inconsistent, or suppress failures silently
115
+ - **Observability gaps**: New error paths unreachable from logs or metrics, silent failures, missing structured context on errors
116
+ - **Input validation**: Unvalidated user-supplied values crossing trust boundaries — SQL injection, shell injection, path traversal, template injection, deserialization
117
+
118
+ Default to skepticism: a change that looks mechanical (rename, refactor, constant tweak) may still introduce one of these. If a category is provably inapplicable, skip it — but verify, don't assume.
89
119
 
90
120
  ## Final Submission
91
121
 
@@ -103,7 +133,9 @@ When you are done, call `submit_review` exactly once with the final structured r
103
133
  "code_location": {
104
134
  "absolute_file_path": "<absolute file path>",
105
135
  "line_range": {"start": <int>, "end": <int>}
106
- }
136
+ },
137
+ "existing_code": "<verbatim copy of the exact current source lines this finding covers>",
138
+ "suggestion": "<optional: exact replacement code for the flagged line range>"
107
139
  }
108
140
  ],
109
141
  "overall_correctness": "patch is correct" | "patch is incorrect",
@@ -117,9 +149,13 @@ When you are done, call `submit_review` exactly once with the final structured r
117
149
  * Do not print the review as normal assistant text.
118
150
  * Do not wrap the payload in markdown fences when calling the tool.
119
151
  * If there are no findings, submit `"findings": []`.
152
+ * If `findings` is non-empty, `overall_correctness` must be `"patch is incorrect"`.
153
+ * If `findings` is empty, `overall_correctness` must be `"patch is correct"`.
120
154
  * Every finding must include `title`, `body`, `priority`, and `code_location`.
155
+ * Always include `existing_code`: copy the exact current source lines the finding refers to, verbatim and contiguous, from the file or the diff's context/added lines. Do NOT include diff `+`/`-` markers, line numbers, or markdown fences. These lines are matched mechanically against the file to pinpoint the comment location, so they must be an exact quote covering the same lines as `line_range`.
121
156
  * Use absolute file paths (for example, `/workspace/path/to/file.py`) not relative paths.
122
157
  * The title must start with a priority tag: `[P0]`, `[P1]`, `[P2]`, or `[P3]`.
123
158
  * `overall_correctness` must be exactly `"patch is correct"` or `"patch is incorrect"`.
159
+ * If you can suggest a specific code fix for a finding, include the replacement code in the `suggestion` field. This should be the exact code to replace the lines in `line_range`, without markdown fences or extra context. Omit `suggestion` if no specific fix is available.
124
160
 
125
161
  {start_instruction}