git-reviewer 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,11 +3,9 @@
3
3
  [![CI](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml/badge.svg)](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](package.json)
6
- [![Tests](https://img.shields.io/badge/tests-206-brightgreen.svg)](test/)
6
+ [![Tests](https://img.shields.io/badge/tests-261-brightgreen.svg)](test/)
7
7
  [![Dependencies](https://img.shields.io/badge/dependencies-3-brightgreen.svg)](package.json)
8
8
 
9
- ![Plain grey code lines on the left resolving into colored diff stripes, with threaded comment markers attached in the right margin](docs/banner.jpg)
10
-
11
9
  When an agent writes the code, reading it becomes the bottleneck — and the tool
12
10
  built for reading a diff, the pull request, wants a branch, a remote and a push
13
11
  you are not ready to make yet.
@@ -18,13 +16,21 @@ mean, with no branch, no remote, no push and no account. Then it hands the
18
16
  marked-up review back to the agent to act on.
19
17
 
20
18
  ```bash
21
- npx git-reviewer . # read the diff, leave comments
22
- npx git-reviewer export . --format prompt | claude -p "Apply this review."
19
+ npm install -g git-reviewer
20
+
21
+ cd ~/your-project
22
+ reviewer # read the diff, comment on it
23
+ reviewer export . --format prompt | claude -p "Apply this review."
23
24
  ```
24
25
 
25
- The first command starts a local server and opens your browser on the
26
- repository you named. Nothing is installed into the project, there is nothing
27
- to sign in to, and nothing leaves your machine.
26
+ ![A terminal installing git-reviewer and running it inside a project; the browser opens on the diff with the first changed file already showing; two review comments are written inline, one with a threaded follow-up; then reviewer export prints those same comments back for a coding agent to act on](docs/demo.gif)
27
+
28
+ *The whole loop, unedited and in real time — [full resolution](docs/demo.mp4).*
29
+
30
+ `reviewer` starts a local server and opens your browser on the repository you
31
+ are standing in. Nothing is installed into the project, there is nothing to
32
+ sign in to, and nothing leaves your machine. If you would rather not install
33
+ it, `npx git-reviewer` works the same way.
28
34
 
29
35
  Comments persist between sessions and survive the code moving underneath them —
30
36
  including the agent's own edits, which shift every line below the first change —
@@ -43,7 +49,8 @@ comment they answer, and everything is saved as you type.
43
49
  reviewer [repository] [options]
44
50
  reviewer export [repository] [--format json|prompt]
45
51
 
46
- repository Path to a git repository (default: the current directory)
52
+ repository A git repository, or any directory inside one
53
+ (default: the current directory)
47
54
 
48
55
  -p, --port <n> Port to listen on (default 4500; falls back to a free
49
56
  port if that one is taken)
@@ -62,6 +69,25 @@ reviewer . --port 8080 # somewhere other than 4500
62
69
 
63
70
  Two at once is fine — the second one finds its own port.
64
71
 
72
+ ### When you are not standing in a repository
73
+
74
+ The page opens on a picker rather than on an empty box:
75
+
76
+ - **Recent** — repositories you have opened before, newest first, with how many
77
+ comments are saved against each. One click reopens one.
78
+ - **Browse** — walk the filesystem from your home directory. Repositories are
79
+ marked, so you can see where to stop; a folder row walks into it, and the
80
+ button beside a marked one opens it.
81
+
82
+ Only directory names are listed, never file names, and the two endpoints behind
83
+ this refuse a request a browser says came from another origin.
84
+
85
+ The header leads with the repository you have open; click it to switch. Beside
86
+ it is a small keyboard icon that reveals a path box, for when the path is
87
+ already on your clipboard and a navigator is the slow way round.
88
+
89
+ ![The picker: a Recent list of previously opened repositories, each showing where it lives and how many comments are saved against it, above a Browse list walking the filesystem with git repositories marked](docs/screenshot-picker.png)
90
+
65
91
  To install it as a command rather than running it through `npx`:
66
92
 
67
93
  ```bash
@@ -70,8 +96,14 @@ reviewer .
70
96
  ```
71
97
 
72
98
  The package is `git-reviewer` because `reviewer` and `code-reviewer` were both
73
- already taken on npm. The command it installs is still `reviewer` -- the
74
- package name is a distribution detail, the command is the product.
99
+ already taken on npm. It installs the command under both names, so whichever
100
+ one you reach for is the one that is there:
101
+
102
+ ```bash
103
+ reviewer # the command
104
+ git-reviewer # the package name, if that is what you typed
105
+ git reviewer # git dispatches to it like any other subcommand
106
+ ```
75
107
 
76
108
  From a clone, if you would rather not install anything:
77
109
 
@@ -239,7 +271,7 @@ rather than a public issue.
239
271
 
240
272
  ```bash
241
273
  npm install # 3 dependencies, no build step, ~6MB
242
- npm test # 206 tests
274
+ npm test # 261 tests
243
275
  npm run test:watch
244
276
  npm run test:coverage
245
277
  ```
package/bin/reviewer.js CHANGED
@@ -9,6 +9,7 @@ const { openInBrowser } = require('../lib/browser');
9
9
  const { formatPrompt } = require('../lib/agent');
10
10
  const { ReviewOwnershipError } = require('../lib/store');
11
11
  const { reviewsDir, adoptLegacyReviews } = require('../lib/paths');
12
+ const { repoRoot } = require('../lib/repo');
12
13
  const {
13
14
  loadReviewDocument,
14
15
  NoReviewError,
@@ -50,7 +51,13 @@ async function listen(port) {
50
51
  * @param {import('../lib/cli').CliOptions} options
51
52
  */
52
53
  async function exportReview(options) {
53
- const repoPath = options.repoPath ?? process.cwd();
54
+ // Saved reviews are filed under the repository root, so exporting from a
55
+ // subdirectory has to look there too — otherwise the browser saves a review
56
+ // one directory up and this reports that none exists. Falling back to the
57
+ // path as given keeps the case `lib/export.js` deliberately supports: a
58
+ // review whose repository is no longer a repository is still exportable.
59
+ const given = options.repoPath ?? process.cwd();
60
+ const repoPath = (await repoRoot(given)) ?? given;
54
61
 
55
62
  let document;
56
63
  try {
@@ -76,6 +83,27 @@ async function exportReview(options) {
76
83
  );
77
84
  }
78
85
 
86
+ /**
87
+ * Which repository the page should open.
88
+ *
89
+ * `reviewer` with no argument reviews the repository you are standing in.
90
+ * `--help` has promised that since the first release and the code never did
91
+ * it, so the page opened on an empty path box instead.
92
+ *
93
+ * Naming a directory and defaulting to one are answered differently on
94
+ * purpose. Name one, and you are told when it is not a repository, because
95
+ * you asked about it. Nobody asked for the current directory in particular —
96
+ * so when that is not inside a repository the page opens on its picker
97
+ * exactly as it always has, rather than greeting you with an error.
98
+ *
99
+ * @param {string|null} given the repository named on the command line
100
+ * @returns {Promise<string|null>} repository to open, or null for the picker
101
+ */
102
+ async function repositoryToOpen(given) {
103
+ if (given) return (await repoRoot(given)) ?? given;
104
+ return repoRoot(process.cwd());
105
+ }
106
+
79
107
  async function main() {
80
108
  const options = parseArgs(process.argv.slice(2));
81
109
 
@@ -114,11 +142,13 @@ async function main() {
114
142
  return exportReview(options);
115
143
  }
116
144
 
145
+ const repoPath = await repositoryToOpen(options.repoPath);
146
+
117
147
  const server = await listen(options.port ?? (Number(process.env.PORT) || DEFAULT_PORT));
118
- const url = buildUrl(`http://${DEFAULT_HOST}:${server.address().port}`, options.repoPath);
148
+ const url = buildUrl(`http://${DEFAULT_HOST}:${server.address().port}`, repoPath);
119
149
 
120
150
  console.log(`\n Code Reviewer ${url}`);
121
- if (options.repoPath) console.log(` reviewing ${options.repoPath}`);
151
+ if (repoPath) console.log(` reviewing ${repoPath}`);
122
152
  console.log('\n Press Ctrl+C to stop.\n');
123
153
 
124
154
  if (options.open && !(await openInBrowser(url))) {
package/lib/browse.js ADDED
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs').promises;
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const { homeRelative } = require('./paths');
8
+
9
+ /**
10
+ * Listing directories, so a repository can be found by looking rather than by
11
+ * being typed from memory.
12
+ *
13
+ * Only directories are reported. A picker picks a folder, so file names are
14
+ * not the question — and not answering it keeps this from being a way to read
15
+ * the shape of someone's disk when all it needs to do is navigate.
16
+ */
17
+
18
+ /**
19
+ * Directories that are never worth offering.
20
+ *
21
+ * `.git` is a repository's own plumbing: listing it invites clicking into it,
22
+ * and what is inside is not reviewable. `node_modules` is thousands of
23
+ * directories nobody is looking for.
24
+ */
25
+ const NEVER_LIST = new Set(['.git', 'node_modules']);
26
+
27
+ /**
28
+ * Is this directory the root of a working tree?
29
+ *
30
+ * Decided by the presence of `.git` rather than by asking git, because this
31
+ * runs once per row and a subprocess per row would make a home directory take
32
+ * seconds to list. `.git` is a directory in an ordinary clone and a *file* in
33
+ * a worktree or a submodule, so existence is the question and type is not.
34
+ *
35
+ * This is a hint for the UI. `lib/repo.js` stays the authority on what gets
36
+ * opened, so a wrong guess here costs a click, not a wrong review.
37
+ *
38
+ * @param {string} dir
39
+ * @returns {Promise<boolean>}
40
+ */
41
+ async function isRepository(dir) {
42
+ try {
43
+ await fs.stat(path.join(dir, '.git'));
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Resolve one directory entry, following symlinks.
52
+ *
53
+ * `withFileTypes` reports a symlink as a symlink, not as what it points at,
54
+ * and a home directory with `~/code -> /Volumes/work/code` in it is ordinary.
55
+ * Following it costs a stat; not following it hides the projects.
56
+ *
57
+ * @param {string} parent
58
+ * @param {import('fs').Dirent} entry
59
+ * @returns {Promise<{name: string, path: string, isRepository: boolean}|null>}
60
+ * null when the entry is not a directory, or cannot be read at all
61
+ */
62
+ async function describe(parent, entry) {
63
+ if (NEVER_LIST.has(entry.name)) return null;
64
+
65
+ const full = path.join(parent, entry.name);
66
+
67
+ if (!entry.isDirectory()) {
68
+ if (!entry.isSymbolicLink()) return null;
69
+ try {
70
+ if (!(await fs.stat(full)).isDirectory()) return null;
71
+ } catch {
72
+ return null; // dangling symlink
73
+ }
74
+ }
75
+
76
+ const repository = await isRepository(full);
77
+
78
+ // Hidden directories are noise in a home directory -- `.cache`, `.npm`,
79
+ // `.Trash` -- but `~/.dotfiles` is one of the most commonly reviewed
80
+ // repositories there is. Hide the hidden ones, except the ones that are
81
+ // repositories, which is the only reason to be looking in here.
82
+ if (entry.name.startsWith('.') && !repository) return null;
83
+
84
+ return { name: entry.name, path: full, isRepository: repository };
85
+ }
86
+
87
+ /**
88
+ * List the directories inside `target`.
89
+ *
90
+ * An entry that cannot be read is skipped rather than failing the listing: one
91
+ * unreadable directory in a home folder should not make the folder unbrowsable.
92
+ *
93
+ * @param {string} [target] directory to list; the home directory by default
94
+ * @returns {Promise<{path: string, display: string, parent: string|null,
95
+ * isRepository: boolean,
96
+ * entries: Array<{name: string, path: string, isRepository: boolean}>}>}
97
+ * @throws an fs error with `code` ENOENT, ENOTDIR or EACCES for the caller to map
98
+ */
99
+ async function browse(target) {
100
+ const asked = typeof target === 'string' ? target.trim() : '';
101
+ const dir = path.resolve(asked === '' ? os.homedir() : asked);
102
+
103
+ const stats = await fs.stat(dir);
104
+ if (!stats.isDirectory()) {
105
+ throw Object.assign(new Error(`Not a directory: ${dir}`), { code: 'ENOTDIR' });
106
+ }
107
+
108
+ const described = await Promise.all(
109
+ (await fs.readdir(dir, { withFileTypes: true })).map(entry => describe(dir, entry))
110
+ );
111
+
112
+ const parent = path.dirname(dir);
113
+
114
+ return {
115
+ path: dir,
116
+ // The same directory, written the way a person writes it. `path` stays
117
+ // absolute because that is what gets opened; this is what gets read.
118
+ display: homeRelative(dir),
119
+ // At the filesystem root, dirname answers with the root again. There is
120
+ // nowhere further up, and a parent row that goes nowhere is a bug report.
121
+ parent: parent === dir ? null : parent,
122
+ isRepository: await isRepository(dir),
123
+ entries: described
124
+ .filter(Boolean)
125
+ .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }))
126
+ };
127
+ }
128
+
129
+ module.exports = { browse, isRepository, NEVER_LIST };
package/lib/cli.js CHANGED
@@ -15,9 +15,13 @@ Usage:
15
15
  reviewer [repository] [options]
16
16
  reviewer export [repository] [--format json|prompt] [--file <path>]
17
17
 
18
+ Installed under both names: reviewer and git-reviewer. The second name also
19
+ makes it reachable as a git subcommand: git reviewer
20
+
18
21
  Arguments:
19
- repository Path to a git repository. Defaults to the current
20
- directory. It is opened automatically once the page loads.
22
+ repository Path to a git repository, or to any directory inside one.
23
+ Defaults to the current directory. It is opened
24
+ automatically once the page loads.
21
25
 
22
26
  Commands:
23
27
  export Print the saved review of a repository to stdout, for a
@@ -228,7 +232,17 @@ function parseFile(value, flag) {
228
232
  */
229
233
  function buildUrl(origin, repoPath) {
230
234
  if (!repoPath) return origin;
231
- return `${origin}/?repo=${encodeURIComponent(repoPath)}`;
235
+
236
+ // Slashes are left as slashes. They are legal in a query value -- RFC 3986
237
+ // has `query = *( pchar / "/" / "?" )` -- and encoding them turns the most
238
+ // prominent line the command prints into %2F soup:
239
+ //
240
+ // before http://127.0.0.1:4500/?repo=%2FUsers%2Fyou%2Fwork%2Fapi
241
+ // after http://127.0.0.1:4500/?repo=/Users/you/work/api
242
+ //
243
+ // Everything else stays encoded, including the characters that would
244
+ // otherwise end the query and take the rest of the path with them.
245
+ return `${origin}/?repo=${encodeURIComponent(repoPath).replace(/%2F/g, '/')}`;
232
246
  }
233
247
 
234
248
  module.exports = { parseArgs, buildUrl, UsageError, USAGE, FORMATS };
package/lib/paths.js CHANGED
@@ -155,12 +155,37 @@ function resolveRepoFile(repoPath, filePath) {
155
155
  return resolved;
156
156
  }
157
157
 
158
+ /**
159
+ * A path as a person would write it, with the home directory as `~`.
160
+ *
161
+ * For display only — nothing resolves a `~` back, and nothing should. An
162
+ * absolute path is the right thing to store, to compare and to hand to git;
163
+ * it is the wrong thing to put in a list of projects, where the first forty
164
+ * characters are the same on every row and the part that identifies the row
165
+ * is the part that gets cut off.
166
+ *
167
+ * @param {string} target
168
+ * @param {string} [home]
169
+ * @returns {string}
170
+ */
171
+ function homeRelative(target, home = os.homedir()) {
172
+ const resolved = path.resolve(String(target ?? ''));
173
+
174
+ if (resolved === home) return '~';
175
+ if (home && resolved.startsWith(home + path.sep)) {
176
+ return `~${resolved.slice(home.length)}`;
177
+ }
178
+
179
+ return resolved;
180
+ }
181
+
158
182
  module.exports = {
159
183
  reviewsDir,
160
184
  userDataDir,
161
185
  adoptLegacyReviews,
162
186
  LEGACY_REVIEWS_DIR,
163
187
  resolveRepoFile,
188
+ homeRelative,
164
189
  PathEscapeError
165
190
  };
166
191
 
package/lib/recents.js ADDED
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs').promises;
4
+ const pathModule = require('path');
5
+
6
+ const { canonicalRepoPath, repoSlug } = require('./review');
7
+ const { homeRelative } = require('./paths');
8
+
9
+ /**
10
+ * The repositories this machine has opened, newest first.
11
+ *
12
+ * `listReviews` already answers a related question — which repositories have a
13
+ * saved review — but that is not the same list. A repository you opened,
14
+ * read and left without commenting on has no review and is exactly the one you
15
+ * want offered back to you next time.
16
+ */
17
+
18
+ /** Filename inside the data directory. Not a review, so `listReviews` skips it. */
19
+ const RECENTS_FILE = 'recent-projects.json';
20
+
21
+ /** How many to keep. Long enough to cover what you are working on, short enough to read. */
22
+ const LIMIT = 10;
23
+
24
+ /**
25
+ * @param {string} dataDir
26
+ * @returns {string}
27
+ */
28
+ function recentsFile(dataDir) {
29
+ return pathModule.join(dataDir, RECENTS_FILE);
30
+ }
31
+
32
+ /**
33
+ * Read the recorded list.
34
+ *
35
+ * Answers an empty list for every failure — absent, unreadable, not JSON, or
36
+ * JSON of the wrong shape. A damaged recents file is a lost convenience, and
37
+ * it must not be allowed to become a failure to open a repository.
38
+ *
39
+ * @param {string} dataDir
40
+ * @returns {Promise<Array<{path: string, openedAt: string}>>} newest first
41
+ */
42
+ async function readRecents(dataDir) {
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(await fs.readFile(recentsFile(dataDir), 'utf-8'));
46
+ } catch {
47
+ return [];
48
+ }
49
+
50
+ if (!Array.isArray(parsed?.projects)) return [];
51
+
52
+ return parsed.projects.filter(
53
+ project =>
54
+ typeof project?.path === 'string' &&
55
+ project.path !== '' &&
56
+ typeof project.openedAt === 'string'
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Record a repository as just opened.
62
+ *
63
+ * Written to a temporary file and renamed, so two `reviewer` processes racing
64
+ * cannot leave a half-written list behind. The loser of the race loses one
65
+ * entry, which is the right thing to lose.
66
+ *
67
+ * @param {string} dataDir
68
+ * @param {string} repoPath
69
+ * @param {Date} [now]
70
+ * @returns {Promise<Array<{path: string, openedAt: string}>>} the new list
71
+ */
72
+ async function recordRecent(dataDir, repoPath, now = new Date()) {
73
+ const canonical = canonicalRepoPath(repoPath);
74
+ const existing = await readRecents(dataDir);
75
+
76
+ const projects = [
77
+ { path: canonical, openedAt: now.toISOString() },
78
+ ...existing.filter(project => canonicalRepoPath(project.path) !== canonical)
79
+ ].slice(0, LIMIT);
80
+
81
+ await fs.mkdir(dataDir, { recursive: true });
82
+
83
+ const temporary = `${recentsFile(dataDir)}.${process.pid}.tmp`;
84
+ await fs.writeFile(temporary, `${JSON.stringify({ version: 1, projects }, null, 2)}\n`);
85
+ await fs.rename(temporary, recentsFile(dataDir));
86
+
87
+ return projects;
88
+ }
89
+
90
+ /**
91
+ * The recent list, as the picker needs to draw it.
92
+ *
93
+ * A repository that has been moved or deleted is reported rather than dropped:
94
+ * seeing it greyed out with the path you remember is a better answer than a
95
+ * list that quietly gets shorter.
96
+ *
97
+ * @param {string} dataDir
98
+ * @param {Map<string, number>} [commentCounts] repository path -> saved comments
99
+ * @returns {Promise<Array<{path: string, name: string, location: string,
100
+ * openedAt: string, exists: boolean, comments: number}>>}
101
+ */
102
+ async function describeRecents(dataDir, commentCounts = new Map()) {
103
+ const projects = await readRecents(dataDir);
104
+
105
+ return Promise.all(
106
+ projects.map(async project => ({
107
+ path: project.path,
108
+ name: repoSlug(project.path),
109
+ // Where it lives, rather than all of where it lives. The name is on the
110
+ // row already, and two checkouts are told apart by the directory holding
111
+ // them, not by the forty characters they have in common.
112
+ location: homeRelative(pathModule.dirname(project.path)),
113
+ openedAt: project.openedAt,
114
+ exists: await fs
115
+ .stat(project.path)
116
+ .then(stats => stats.isDirectory())
117
+ .catch(() => false),
118
+ comments: commentCounts.get(canonicalRepoPath(project.path)) ?? 0
119
+ }))
120
+ );
121
+ }
122
+
123
+ module.exports = { readRecents, recordRecent, describeRecents, RECENTS_FILE, LIMIT };
package/lib/repo.js ADDED
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const simpleGit = require('simple-git');
6
+
7
+ /**
8
+ * Finding the repository a directory belongs to.
9
+ *
10
+ * Everything downstream of this — the session, the saved review's filename,
11
+ * every diff — is keyed on one path, and that path has to be the root of the
12
+ * working tree rather than wherever the person happened to be standing. Two
13
+ * things go wrong otherwise, and neither announces itself:
14
+ *
15
+ * - `git status --porcelain` reports paths relative to the root, but a
16
+ * pathspec is resolved relative to the process's directory. Ask a git
17
+ * rooted at `repo/src` for `git diff -- src/deep/file.txt` and you get an
18
+ * empty string back, not an error. The file is listed, the diff is blank,
19
+ * and nothing says why.
20
+ * - reviews are filed under a fingerprint of the repository path, so a
21
+ * review written from the root is invisible from a subdirectory. The
22
+ * browser saves it, `reviewer export` run one directory down reports that
23
+ * no review exists, and the loop in the README quietly stops working.
24
+ */
25
+
26
+ /**
27
+ * The root of the working tree containing `dir`.
28
+ *
29
+ * Answers `null` rather than raising for every way this can fail — not a
30
+ * repository, a bare repository with no working tree, git not installed, the
31
+ * directory gone. Callers decide what an absent repository means; for some it
32
+ * is an error and for others it is simply a directory they will use as given.
33
+ *
34
+ * The `simpleGit()` call is inside the `try` on purpose. It throws
35
+ * *synchronously, at construction* when the directory does not exist, so a
36
+ * `.catch()` on the returned promise never runs — the same trap documented at
37
+ * `readGitPosition` in `lib/export.js`.
38
+ *
39
+ * @param {string} dir any directory inside the repository
40
+ * @param {(dir: string) => import('simple-git').SimpleGit} [gitFactory]
41
+ * @returns {Promise<string|null>} absolute path to the root, or null
42
+ */
43
+ async function repoRoot(dir, gitFactory = simpleGit) {
44
+ try {
45
+ const top = await gitFactory(dir).revparse(['--show-toplevel']);
46
+ const trimmed = String(top).trim();
47
+ return trimmed === '' ? null : path.resolve(trimmed);
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ module.exports = { repoRoot };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "git-reviewer",
3
- "version": "2.2.0",
4
- "description": "Review your coding agent's changes like a pull request \u2014 locally, with inline comments and threaded follow-ups, then hand the marked-up review back to the agent",
3
+ "version": "2.3.0",
4
+ "description": "Review your coding agent's changes like a pull request — locally, with inline comments and threaded follow-ups, then hand the marked-up review back to the agent",
5
5
  "author": "Dheeraj Jha",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/dheerajjha/reviewer#readme",
@@ -31,7 +31,8 @@
31
31
  },
32
32
  "main": "server.js",
33
33
  "bin": {
34
- "reviewer": "bin/reviewer.js"
34
+ "reviewer": "bin/reviewer.js",
35
+ "git-reviewer": "bin/reviewer.js"
35
36
  },
36
37
  "files": [
37
38
  "bin/",