agent-reviews 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-reviews",
3
3
  "description": "Manage GitHub PR review comments from Claude Code. Automatically triage, fix, and respond to bot findings.",
4
- "version": "0.4.0",
4
+ "version": "0.5.1",
5
5
  "author": {
6
6
  "name": "Paul Bakaus",
7
7
  "url": "https://github.com/pbakaus"
package/README.md CHANGED
@@ -4,6 +4,14 @@ Manage GitHub PR review comments from the terminal and from AI coding agents.
4
4
 
5
5
  PR review bots (Copilot, Cursor Bugbot, CodeRabbit, etc.) leave inline comments on your pull requests. agent-reviews gives you a CLI to list, filter, reply to, and watch those comments, plus a Claude Code skill that automates the entire triage-fix-reply loop.
6
6
 
7
+ ## Why
8
+
9
+ **`gh` CLI is fragile for review comments.** Agents frequently get the syntax wrong, fail to paginate, and can't reliably detect whether a comment has been replied to. agent-reviews provides a single, purpose-built interface that handles all of this correctly.
10
+
11
+ **Bot reviews create a doom loop.** You fix one round of findings, push, and new comments appear. Fix those, push again, more comments. This cycle can eat hours. The included skill solves it with an integrated watcher that keeps fixing and replying until the bots go quiet.
12
+
13
+ **Works in Claude Code for the web.** Most solutions rely on local tooling that isn't available in cloud environments. agent-reviews works everywhere Claude Code runs, so you can kick off a session in your browser, let it resolve all bot findings autonomously, and come back to a clean PR.
14
+
7
15
  ## Install
8
16
 
9
17
  ### CLI (npm)
@@ -90,6 +98,7 @@ agent-reviews --pr 42
90
98
  | `--json` | `-j` | JSON output |
91
99
  | `--bots-only` | `-b` | Only bot comments |
92
100
  | `--humans-only` | `-H` | Only human comments |
101
+ | `--expanded` | `-e` | Show full detail for each listed comment |
93
102
  | `--watch` | `-w` | Poll for new comments |
94
103
  | `--interval <sec>` | `-i` | Poll interval in seconds (default: 30) |
95
104
  | `--timeout <sec>` | | Inactivity timeout in seconds (default: 600) |
@@ -150,6 +159,12 @@ Each comment displays its reply status:
150
159
 
151
160
  Polls the GitHub API at a configurable interval and reports new comments as they appear. Outputs both formatted text and JSON for AI agent consumption. Exits automatically after a configurable inactivity timeout (default: 10 minutes).
152
161
 
162
+ ## FAQ
163
+
164
+ ### How can I make the skill review human comments as well?
165
+
166
+ The skill defaults to `--bots-only` because that's the most common use case, but it's easy to change. Open your local copy of `SKILL.md` (inside `skills/agent-reviews/`) and remove the `--bots-only` flag from the commands in Steps 2, 6, and anywhere else it appears. The skill will then fetch and process all comments, regardless of author.
167
+
153
168
  ## License
154
169
 
155
170
  MIT
@@ -63,6 +63,7 @@ function parseArgs() {
63
63
  detail: null,
64
64
  help: false,
65
65
  version: false,
66
+ expanded: false,
66
67
  watch: false,
67
68
  watchInterval: 30,
68
69
  watchTimeout: 600,
@@ -118,6 +119,10 @@ function parseArgs() {
118
119
  case "--timeout":
119
120
  result.watchTimeout = Number.parseInt(args[++i], 10);
120
121
  break;
122
+ case "--expanded":
123
+ case "-e":
124
+ result.expanded = true;
125
+ break;
121
126
  case "--help":
122
127
  case "-h":
123
128
  result.help = true;
@@ -146,6 +151,7 @@ ${colors.bright}Usage:${colors.reset}
146
151
  agent-reviews --unanswered List comments without replies
147
152
  agent-reviews --reply <id> "msg" Reply to a specific comment
148
153
  agent-reviews --detail <id> Show full detail for a comment
154
+ agent-reviews --expanded Show full detail for each comment
149
155
  agent-reviews --watch Watch for new comments (poll mode)
150
156
  agent-reviews --json Output as JSON for scripting
151
157
 
@@ -158,6 +164,7 @@ ${colors.bright}Options:${colors.reset}
158
164
  -j, --json Output as JSON instead of formatted text
159
165
  -b, --bots-only Only show comments from bots
160
166
  -H, --humans-only Only show comments from humans
167
+ -e, --expanded Show full detail (body, diff hunk, replies) for each comment
161
168
  -h, --help Show this help
162
169
  -v, --version Show version
163
170
 
@@ -170,6 +177,7 @@ ${colors.bright}Examples:${colors.reset}
170
177
  agent-reviews # Show all comments
171
178
  agent-reviews -u # Show unresolved only
172
179
  agent-reviews -a --bots-only # Unanswered bot comments
180
+ agent-reviews -a --bots-only --expanded # Full detail for unanswered bot comments
173
181
  agent-reviews --reply 12345 "Fixed!" # Reply to comment #12345
174
182
  agent-reviews --detail 12345 # Full detail for a comment
175
183
  agent-reviews --detail 12345 --json # Detail as JSON
package/lib/comments.js CHANGED
@@ -130,6 +130,40 @@ function isBot(username) {
130
130
  );
131
131
  }
132
132
 
133
+ // ---------------------------------------------------------------------------
134
+ // Body cleanup
135
+ // ---------------------------------------------------------------------------
136
+
137
+ /**
138
+ * Strip bot boilerplate from comment bodies:
139
+ * - HTML comments (<!-- ... -->)
140
+ * - Cursor "Fix in Cursor" / "Fix in Web" button blocks
141
+ * - "Additional Locations" <details> blocks
142
+ * - Collapse leftover blank lines
143
+ */
144
+ function cleanBody(body) {
145
+ if (!body) return body;
146
+
147
+ let cleaned = body;
148
+
149
+ // Remove HTML comments (single and multi-line)
150
+ cleaned = cleaned.replace(/<!--[\s\S]*?-->/g, "");
151
+
152
+ // Remove <details> blocks containing "Additional Locations"
153
+ cleaned = cleaned.replace(
154
+ /<details>\s*<summary>\s*Additional Locations[\s\S]*?<\/details>/gi,
155
+ ""
156
+ );
157
+
158
+ // Remove <p> blocks containing cursor.com links
159
+ cleaned = cleaned.replace(/<p>\s*<a [^>]*cursor\.com[\s\S]*?<\/p>/gi, "");
160
+
161
+ // Collapse runs of 3+ newlines into 2
162
+ cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
163
+
164
+ return cleaned.trim();
165
+ }
166
+
133
167
  // ---------------------------------------------------------------------------
134
168
  // Processing
135
169
  // ---------------------------------------------------------------------------
@@ -148,7 +182,7 @@ function processComments(data, options = {}) {
148
182
  repliesMap.get(comment.in_reply_to_id).push({
149
183
  id: comment.id,
150
184
  user: comment.user?.login,
151
- body: comment.body,
185
+ body: cleanBody(comment.body),
152
186
  createdAt: comment.created_at,
153
187
  isBot: isBot(comment.user?.login),
154
188
  });
@@ -174,7 +208,7 @@ function processComments(data, options = {}) {
174
208
  path: comment.path,
175
209
  line: comment.line || comment.original_line,
176
210
  diffHunk: comment.diff_hunk || null,
177
- body: comment.body,
211
+ body: cleanBody(comment.body),
178
212
  createdAt: comment.created_at,
179
213
  updatedAt: comment.updated_at,
180
214
  url: comment.html_url,
@@ -197,7 +231,7 @@ function processComments(data, options = {}) {
197
231
  path: null,
198
232
  line: null,
199
233
  diffHunk: null,
200
- body: comment.body,
234
+ body: cleanBody(comment.body),
201
235
  createdAt: comment.created_at,
202
236
  updatedAt: comment.updated_at,
203
237
  url: comment.html_url,
@@ -221,7 +255,7 @@ function processComments(data, options = {}) {
221
255
  path: null,
222
256
  line: null,
223
257
  diffHunk: null,
224
- body: review.body,
258
+ body: cleanBody(review.body),
225
259
  state: review.state,
226
260
  createdAt: review.submitted_at,
227
261
  updatedAt: review.submitted_at,
@@ -330,5 +364,6 @@ module.exports = {
330
364
  replyToComment,
331
365
  isBot,
332
366
  isMetaComment,
367
+ cleanBody,
333
368
  DEFAULT_META_FILTERS,
334
369
  };
package/lib/format.js CHANGED
@@ -152,7 +152,9 @@ function formatOutput(comments, options) {
152
152
  }
153
153
 
154
154
  const header = `${colors.bright}Found ${comments.length} comment${comments.length === 1 ? "" : "s"}${colors.reset}\n`;
155
- const formatted = comments.map((c) => formatComment(c)).join("\n\n");
155
+ const formatter = options.expanded ? formatDetailedComment : formatComment;
156
+ const separator = options.expanded ? "\n\n" + "=".repeat(60) + "\n\n" : "\n\n";
157
+ const formatted = comments.map((c) => formatter(c)).join(separator);
156
158
 
157
159
  return `${header}\n${formatted}`;
158
160
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-reviews",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "CLI and Claude Code skill for managing GitHub PR review comments. List, filter, reply, and watch for bot findings.",
5
5
  "license": "MIT",
6
6
  "author": "Paul Bakaus",
@@ -28,9 +28,14 @@
28
28
  ".claude-plugin/"
29
29
  ],
30
30
  "scripts": {
31
- "build:skill": "node scripts/build-skill.js"
31
+ "build:skill": "node scripts/build-skill.js",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest"
32
34
  },
33
35
  "engines": {
34
36
  "node": ">=18"
37
+ },
38
+ "devDependencies": {
39
+ "vitest": "^4.0.18"
35
40
  }
36
41
  }
@@ -5,7 +5,7 @@ license: MIT
5
5
  compatibility: Requires git and gh (GitHub CLI) installed. Designed for Claude Code.
6
6
  metadata:
7
7
  author: pbakaus
8
- version: "0.4.0"
8
+ version: "0.5.1"
9
9
  homepage: https://github.com/pbakaus/agent-reviews
10
10
  ---
11
11
 
@@ -21,29 +21,21 @@ gh pr view --json number,url,headRefName
21
21
 
22
22
  If no PR exists, notify the user and exit.
23
23
 
24
- ### Step 2: Fetch All Bot Comments
24
+ ### Step 2: Fetch All Bot Comments (Expanded)
25
25
 
26
26
  ```bash
27
- scripts/agent-reviews.js --bots-only --unanswered
27
+ scripts/agent-reviews.js --bots-only --unanswered --expanded
28
28
  ```
29
29
 
30
- This shows only unanswered bot comments. Each comment shows its ID in brackets (e.g., `[12345678]`), the author, file path, and a truncated body.
30
+ This shows only unanswered bot comments with full detail: complete comment body (no truncation), diff hunk (code context), and all replies. Each comment shows its ID in brackets (e.g., `[12345678]`).
31
31
 
32
32
  If zero comments are returned, print "No unanswered bot comments found" and skip to Phase 2.
33
33
 
34
34
  ### Step 3: Process Each Unanswered Comment
35
35
 
36
- For each comment where `hasAnyReply === false`:
36
+ For each comment from the expanded output:
37
37
 
38
- #### A. Get Full Detail
39
-
40
- ```bash
41
- scripts/agent-reviews.js --detail <comment_id>
42
- ```
43
-
44
- This shows the full comment body (no truncation), the diff hunk (code context), and all replies. Use this instead of `gh` CLI for comment details.
45
-
46
- #### B. Evaluate the Finding
38
+ #### A. Evaluate the Finding
47
39
 
48
40
  Read the referenced code and determine:
49
41
 
@@ -72,7 +64,7 @@ Read the referenced code and determine:
72
64
  - Multiple valid interpretations exist
73
65
  - The fix could have unintended side effects
74
66
 
75
- #### C. Act on Evaluation
67
+ #### B. Act on Evaluation
76
68
 
77
69
  **If TRUE POSITIVE:** Fix the code. Track the comment ID and a brief description of the fix.
78
70
 
@@ -145,8 +137,8 @@ Use `TaskOutput` to wait for the watcher to complete (blocks up to 12 minutes).
145
137
  ### Step 8: Process New Comments (if any)
146
138
 
147
139
  If the watcher found new comments:
148
- 1. Process them exactly as in Phase 1, Steps 3-5
149
- 2. Use `--detail <id>` to read each new comment
140
+ 1. Use `--detail <id>` to read each new comment's full detail
141
+ 2. Process them exactly as in Phase 1, Steps 3-5
150
142
 
151
143
  If no new comments were found, move to the summary.
152
144
 
@@ -63,6 +63,7 @@ function parseArgs() {
63
63
  detail: null,
64
64
  help: false,
65
65
  version: false,
66
+ expanded: false,
66
67
  watch: false,
67
68
  watchInterval: 30,
68
69
  watchTimeout: 600,
@@ -118,6 +119,10 @@ function parseArgs() {
118
119
  case "--timeout":
119
120
  result.watchTimeout = Number.parseInt(args[++i], 10);
120
121
  break;
122
+ case "--expanded":
123
+ case "-e":
124
+ result.expanded = true;
125
+ break;
121
126
  case "--help":
122
127
  case "-h":
123
128
  result.help = true;
@@ -146,6 +151,7 @@ ${colors.bright}Usage:${colors.reset}
146
151
  agent-reviews --unanswered List comments without replies
147
152
  agent-reviews --reply <id> "msg" Reply to a specific comment
148
153
  agent-reviews --detail <id> Show full detail for a comment
154
+ agent-reviews --expanded Show full detail for each comment
149
155
  agent-reviews --watch Watch for new comments (poll mode)
150
156
  agent-reviews --json Output as JSON for scripting
151
157
 
@@ -158,6 +164,7 @@ ${colors.bright}Options:${colors.reset}
158
164
  -j, --json Output as JSON instead of formatted text
159
165
  -b, --bots-only Only show comments from bots
160
166
  -H, --humans-only Only show comments from humans
167
+ -e, --expanded Show full detail (body, diff hunk, replies) for each comment
161
168
  -h, --help Show this help
162
169
  -v, --version Show version
163
170
 
@@ -170,6 +177,7 @@ ${colors.bright}Examples:${colors.reset}
170
177
  agent-reviews # Show all comments
171
178
  agent-reviews -u # Show unresolved only
172
179
  agent-reviews -a --bots-only # Unanswered bot comments
180
+ agent-reviews -a --bots-only --expanded # Full detail for unanswered bot comments
173
181
  agent-reviews --reply 12345 "Fixed!" # Reply to comment #12345
174
182
  agent-reviews --detail 12345 # Full detail for a comment
175
183
  agent-reviews --detail 12345 --json # Detail as JSON
@@ -320,7 +328,7 @@ async function main() {
320
328
  const options = parseArgs();
321
329
 
322
330
  if (options.version) {
323
- console.log("0.4.0");
331
+ console.log("0.5.1");
324
332
  process.exit(0);
325
333
  }
326
334
 
@@ -130,6 +130,40 @@ function isBot(username) {
130
130
  );
131
131
  }
132
132
 
133
+ // ---------------------------------------------------------------------------
134
+ // Body cleanup
135
+ // ---------------------------------------------------------------------------
136
+
137
+ /**
138
+ * Strip bot boilerplate from comment bodies:
139
+ * - HTML comments (<!-- ... -->)
140
+ * - Cursor "Fix in Cursor" / "Fix in Web" button blocks
141
+ * - "Additional Locations" <details> blocks
142
+ * - Collapse leftover blank lines
143
+ */
144
+ function cleanBody(body) {
145
+ if (!body) return body;
146
+
147
+ let cleaned = body;
148
+
149
+ // Remove HTML comments (single and multi-line)
150
+ cleaned = cleaned.replace(/<!--[\s\S]*?-->/g, "");
151
+
152
+ // Remove <details> blocks containing "Additional Locations"
153
+ cleaned = cleaned.replace(
154
+ /<details>\s*<summary>\s*Additional Locations[\s\S]*?<\/details>/gi,
155
+ ""
156
+ );
157
+
158
+ // Remove <p> blocks containing cursor.com links
159
+ cleaned = cleaned.replace(/<p>\s*<a [^>]*cursor\.com[\s\S]*?<\/p>/gi, "");
160
+
161
+ // Collapse runs of 3+ newlines into 2
162
+ cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
163
+
164
+ return cleaned.trim();
165
+ }
166
+
133
167
  // ---------------------------------------------------------------------------
134
168
  // Processing
135
169
  // ---------------------------------------------------------------------------
@@ -148,7 +182,7 @@ function processComments(data, options = {}) {
148
182
  repliesMap.get(comment.in_reply_to_id).push({
149
183
  id: comment.id,
150
184
  user: comment.user?.login,
151
- body: comment.body,
185
+ body: cleanBody(comment.body),
152
186
  createdAt: comment.created_at,
153
187
  isBot: isBot(comment.user?.login),
154
188
  });
@@ -174,7 +208,7 @@ function processComments(data, options = {}) {
174
208
  path: comment.path,
175
209
  line: comment.line || comment.original_line,
176
210
  diffHunk: comment.diff_hunk || null,
177
- body: comment.body,
211
+ body: cleanBody(comment.body),
178
212
  createdAt: comment.created_at,
179
213
  updatedAt: comment.updated_at,
180
214
  url: comment.html_url,
@@ -197,7 +231,7 @@ function processComments(data, options = {}) {
197
231
  path: null,
198
232
  line: null,
199
233
  diffHunk: null,
200
- body: comment.body,
234
+ body: cleanBody(comment.body),
201
235
  createdAt: comment.created_at,
202
236
  updatedAt: comment.updated_at,
203
237
  url: comment.html_url,
@@ -221,7 +255,7 @@ function processComments(data, options = {}) {
221
255
  path: null,
222
256
  line: null,
223
257
  diffHunk: null,
224
- body: review.body,
258
+ body: cleanBody(review.body),
225
259
  state: review.state,
226
260
  createdAt: review.submitted_at,
227
261
  updatedAt: review.submitted_at,
@@ -330,5 +364,6 @@ module.exports = {
330
364
  replyToComment,
331
365
  isBot,
332
366
  isMetaComment,
367
+ cleanBody,
333
368
  DEFAULT_META_FILTERS,
334
369
  };
@@ -152,7 +152,9 @@ function formatOutput(comments, options) {
152
152
  }
153
153
 
154
154
  const header = `${colors.bright}Found ${comments.length} comment${comments.length === 1 ? "" : "s"}${colors.reset}\n`;
155
- const formatted = comments.map((c) => formatComment(c)).join("\n\n");
155
+ const formatter = options.expanded ? formatDetailedComment : formatComment;
156
+ const separator = options.expanded ? "\n\n" + "=".repeat(60) + "\n\n" : "\n\n";
157
+ const formatted = comments.map((c) => formatter(c)).join(separator);
156
158
 
157
159
  return `${header}\n${formatted}`;
158
160
  }