claude-review-hub 0.1.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/LICENSE +21 -0
- package/README.md +244 -0
- package/bin/claude-review-hub.mjs +86 -0
- package/logo.svg +1 -0
- package/package.json +62 -0
- package/scripts/dev.mjs +163 -0
- package/scripts/install-skill.mjs +74 -0
- package/scripts/uninstall.mjs +84 -0
- package/server/dist/agent.js +292 -0
- package/server/dist/agent.js.map +1 -0
- package/server/dist/azure/actions.js +182 -0
- package/server/dist/azure/actions.js.map +1 -0
- package/server/dist/azure/detect.js +92 -0
- package/server/dist/azure/detect.js.map +1 -0
- package/server/dist/azure/rest.js +54 -0
- package/server/dist/azure/rest.js.map +1 -0
- package/server/dist/chat.js +51 -0
- package/server/dist/chat.js.map +1 -0
- package/server/dist/config.js +137 -0
- package/server/dist/config.js.map +1 -0
- package/server/dist/db.js +140 -0
- package/server/dist/db.js.map +1 -0
- package/server/dist/diff/split.js +98 -0
- package/server/dist/diff/split.js.map +1 -0
- package/server/dist/events.js +82 -0
- package/server/dist/events.js.map +1 -0
- package/server/dist/git/diff.js +36 -0
- package/server/dist/git/diff.js.map +1 -0
- package/server/dist/git/worktree.js +79 -0
- package/server/dist/git/worktree.js.map +1 -0
- package/server/dist/index.js +116 -0
- package/server/dist/index.js.map +1 -0
- package/server/dist/inspect.js +144 -0
- package/server/dist/inspect.js.map +1 -0
- package/server/dist/mcp.js +106 -0
- package/server/dist/mcp.js.map +1 -0
- package/server/dist/paths.js +35 -0
- package/server/dist/paths.js.map +1 -0
- package/server/dist/permissions.js +99 -0
- package/server/dist/permissions.js.map +1 -0
- package/server/dist/pkg.js +25 -0
- package/server/dist/pkg.js.map +1 -0
- package/server/dist/providers/azure.js +203 -0
- package/server/dist/providers/azure.js.map +1 -0
- package/server/dist/providers/github.js +234 -0
- package/server/dist/providers/github.js.map +1 -0
- package/server/dist/providers/index.js +166 -0
- package/server/dist/providers/index.js.map +1 -0
- package/server/dist/providers/types.js +2 -0
- package/server/dist/providers/types.js.map +1 -0
- package/server/dist/repos.js +55 -0
- package/server/dist/repos.js.map +1 -0
- package/server/dist/review/actions.js +439 -0
- package/server/dist/review/actions.js.map +1 -0
- package/server/dist/review/findings.js +103 -0
- package/server/dist/review/findings.js.map +1 -0
- package/server/dist/review/profiles.js +117 -0
- package/server/dist/review/profiles.js.map +1 -0
- package/server/dist/review/prompt.js +23 -0
- package/server/dist/review/prompt.js.map +1 -0
- package/server/dist/review/promptStore.js +52 -0
- package/server/dist/review/promptStore.js.map +1 -0
- package/server/dist/review/run.js +153 -0
- package/server/dist/review/run.js.map +1 -0
- package/server/dist/review/schema.js +58 -0
- package/server/dist/review/schema.js.map +1 -0
- package/server/dist/review/tasks.js +18 -0
- package/server/dist/review/tasks.js.map +1 -0
- package/server/dist/routes.js +223 -0
- package/server/dist/routes.js.map +1 -0
- package/server/dist/sessions.js +226 -0
- package/server/dist/sessions.js.map +1 -0
- package/server/dist/skill.js +37 -0
- package/server/dist/skill.js.map +1 -0
- package/skill/azure-pr-master/SKILL.md +259 -0
- package/skill/azure-pr-master/split-diff.mjs +130 -0
- package/skill/github-pr-master/SKILL.md +298 -0
- package/skill/github-pr-master/split-diff.mjs +130 -0
- package/web/dist/assets/index-D55AjoTk.js +359 -0
- package/web/dist/assets/index-DWLH0g20.css +1 -0
- package/web/dist/favicon.svg +1 -0
- package/web/dist/index.html +20 -0
- package/web/dist/logo-small.svg +1 -0
- package/web/dist/logo.svg +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 goldynlabs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="logo.svg" alt="" width="96" height="96" />
|
|
4
|
+
|
|
5
|
+
# Claude Review Hub
|
|
6
|
+
|
|
7
|
+
**Pull request review that actually reads the code, in a dashboard you control.**
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/claude-review-hub)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
Point it at a repo, paste a PR number, and Claude Code reviews it the way a
|
|
16
|
+
senior engineer would: it clones the branch into a worktree, reads the diff
|
|
17
|
+
against the real codebase, and comes back with findings that cite evidence.
|
|
18
|
+
You decide what gets posted.
|
|
19
|
+
|
|
20
|
+
```console
|
|
21
|
+
$ npx claude-review-hub "C:/Source code/your-repo"
|
|
22
|
+
|
|
23
|
+
✳️ Claude Review Hub
|
|
24
|
+
project : C:/Source code/your-repo
|
|
25
|
+
skill : azure-pr-master installed
|
|
26
|
+
skill : github-pr-master installed
|
|
27
|
+
host : Azure DevOps
|
|
28
|
+
org : your-org
|
|
29
|
+
url : http://localhost:4319
|
|
30
|
+
|
|
31
|
+
Keep this terminal open. Press Ctrl+C to stop the dashboard.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 😵 The problem
|
|
37
|
+
|
|
38
|
+
Reviewing a pull request properly is expensive, so it mostly does not happen.
|
|
39
|
+
|
|
40
|
+
- **The diff lies.** A three-line change is safe or catastrophic depending on
|
|
41
|
+
who calls it, and the PR page will not tell you which. Finding out means
|
|
42
|
+
checking out the branch and grepping the repo.
|
|
43
|
+
- **Bot reviewers are noise.** They read the diff and nothing else, so they
|
|
44
|
+
flag style and miss the tenant id that stopped being filtered. After a week
|
|
45
|
+
of that, nobody reads their comments.
|
|
46
|
+
- **The terminal is not a record.** You can ask an agent to review a PR in a
|
|
47
|
+
chat window today, but tomorrow the session is gone, the reasoning is gone,
|
|
48
|
+
and nothing connects what it said to what you posted.
|
|
49
|
+
- **You cannot trust what you cannot see.** An agent with write access to your
|
|
50
|
+
pull requests is a liability unless you know the exact words it is about to
|
|
51
|
+
send, before it sends them.
|
|
52
|
+
|
|
53
|
+
## ✨ What Claude Review Hub does about it
|
|
54
|
+
|
|
55
|
+
**It reads the repository, not just the patch.** Every PR gets a detached git
|
|
56
|
+
worktree of the actual branch. Claude opens the files, follows the call sites,
|
|
57
|
+
and reads your `CLAUDE.md` and conventions, because the tool installs into your
|
|
58
|
+
project and runs from its root.
|
|
59
|
+
|
|
60
|
+
**Findings come with evidence and a confidence score.** Not "consider
|
|
61
|
+
refactoring this". A finding names the file and line, states the input or state
|
|
62
|
+
that produces the wrong result, and cites what it checked.
|
|
63
|
+
|
|
64
|
+
**You can argue with it.** Disagree with a finding? Hit **Challenge**, type
|
|
65
|
+
your counter-argument, and an adversarial re-check runs against it. The new
|
|
66
|
+
verdict is appended, never overwritten, so the disagreement stays on the
|
|
67
|
+
record.
|
|
68
|
+
|
|
69
|
+
**Nothing is sent unseen.** Every control that sends a prompt to the agent
|
|
70
|
+
opens one confirmation showing the exact prompt, with a note box to aim it and
|
|
71
|
+
an Edit control to rewrite it for that one request. Anything that writes to the
|
|
72
|
+
pull request is marked before you click.
|
|
73
|
+
|
|
74
|
+
**It stays yours.** Sessions, findings, verdicts and the full event log live in
|
|
75
|
+
the repo you reviewed. Nothing to host, no extra account, no analytics: the
|
|
76
|
+
dashboard runs on localhost for as long as your terminal is open, and the only
|
|
77
|
+
thing that leaves the machine is what Claude Code and your own CLIs already
|
|
78
|
+
send.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## ⚡ Quick start
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# From inside the repo you want to review
|
|
86
|
+
cd your-repo
|
|
87
|
+
npx claude-review-hub
|
|
88
|
+
|
|
89
|
+
# Or point it at one
|
|
90
|
+
npx claude-review-hub "C:/Source code/your-repo"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
That single command installs the review skills into the project, hides the
|
|
94
|
+
tool's own files from git through `.git/info/exclude` (your `.gitignore` is
|
|
95
|
+
never touched, so the repo shows no modified file), and opens the dashboard on
|
|
96
|
+
<http://localhost:4319>.
|
|
97
|
+
|
|
98
|
+
It is idempotent. Run it again after an update and it refreshes the skills in
|
|
99
|
+
place. Running it in a second repo does not clash: a port already in use is
|
|
100
|
+
skipped and the banner names the one it took.
|
|
101
|
+
|
|
102
|
+
### 🧰 Requirements
|
|
103
|
+
|
|
104
|
+
| | |
|
|
105
|
+
|---|---|
|
|
106
|
+
| Node | 20 or newer |
|
|
107
|
+
| Claude Code | Signed in on this machine, with a plan that allows Sonnet and Opus |
|
|
108
|
+
| Azure DevOps | `az login`, plus `az extension add --name azure-devops` |
|
|
109
|
+
| GitHub | `gh auth login` |
|
|
110
|
+
| Repos | A local checkout of every repository whose PRs you review |
|
|
111
|
+
|
|
112
|
+
Install only the CLI for the host you use. Which host a repo is on is read from
|
|
113
|
+
its git remote, so having both CLIs is never ambiguous: the sidebar names the
|
|
114
|
+
host in play, and every button about a pull request drives that host's CLI.
|
|
115
|
+
With one CLI installed the dashboard shows one account and the agent is told
|
|
116
|
+
about one host; the other is listed as not installed rather than as broken.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 🔁 The loop
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
paste PR ids → worktree + diff → findings
|
|
124
|
+
↓
|
|
125
|
+
post / reply / approve ← challenge
|
|
126
|
+
|
|
127
|
+
the chat box reaches every one of those steps
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**1. Start a session.** The PR box is free text. Ids, full URLs, or a sentence:
|
|
131
|
+
`4821 4830 focus on the migration path`. Anything that is not a PR id
|
|
132
|
+
becomes context for the run. One session can span several PRs and several
|
|
133
|
+
repositories.
|
|
134
|
+
|
|
135
|
+
**2. Pick what "review" means.** Criteria are configured in the tool, not
|
|
136
|
+
buried in a prompt file. A **profile** holds review dimensions (correctness,
|
|
137
|
+
security, blast radius, project rules, or your own), extra context, include and
|
|
138
|
+
exclude globs, and a severity floor. The shipped profiles are a starting point;
|
|
139
|
+
Settings is where you make them yours.
|
|
140
|
+
|
|
141
|
+
**3. Read the findings.** Each one carries severity, confidence, file and line,
|
|
142
|
+
and the evidence behind it. Dismiss what you do not want. Challenge what you
|
|
143
|
+
doubt.
|
|
144
|
+
|
|
145
|
+
**4. Act on the PR.** Post a single finding or all of them, reply to a thread,
|
|
146
|
+
set a thread's state, approve, reject, wait for the author, or merge. Every one
|
|
147
|
+
of those opens the confirmation first.
|
|
148
|
+
|
|
149
|
+
**5. Ask for anything else.** The chat box drives the same operations the
|
|
150
|
+
buttons do, in the same session, landing in the same event log. The buttons are
|
|
151
|
+
the shortcuts; the chat box is the whole surface.
|
|
152
|
+
|
|
153
|
+
Existing PR comments are deliberately **not** fed into the review pass, so the
|
|
154
|
+
first read is unbiased. They enter Claude's context only when you reply to a
|
|
155
|
+
thread or ask about one.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 🔧 Under the hood
|
|
160
|
+
|
|
161
|
+
The dashboard is not a wrapper around a chatbot. The server keeps the record;
|
|
162
|
+
the agent does the work with your own CLIs.
|
|
163
|
+
|
|
164
|
+
- **A detached worktree per PR**, built from the local checkout you already
|
|
165
|
+
have. Nothing is downloaded twice, and your working tree is never touched.
|
|
166
|
+
- **The server produces the diff**, splits it per file, and hands it over.
|
|
167
|
+
Claude spends its context reading code, not reconstructing patches.
|
|
168
|
+
- **One definition of every operation.** A reply typed in chat and one clicked
|
|
169
|
+
on a thread card are the same function call, and both land in the append-only
|
|
170
|
+
event log that the UI is drawn from. Close the browser mid-review and reopen
|
|
171
|
+
it: the session is exactly where you left it.
|
|
172
|
+
- **Prompt templates are editable for good.** Settings > Prompts lists every
|
|
173
|
+
action and lets its wording be replaced, which reaches the hover preview, the
|
|
174
|
+
confirmation, the docs tab and the agent together. The built-in wording can
|
|
175
|
+
always be restored.
|
|
176
|
+
|
|
177
|
+
### 📁 What it writes to your project
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
<project>/
|
|
181
|
+
.claude/skills/azure-pr-master/ the Azure DevOps procedure, refreshed on start
|
|
182
|
+
.claude/skills/github-pr-master/ the GitHub procedure, refreshed on start
|
|
183
|
+
.review-tool/ hidden from git, never committed
|
|
184
|
+
config/settings.json
|
|
185
|
+
config/profiles.json your review criteria
|
|
186
|
+
config/prompts.json your prompt overrides
|
|
187
|
+
data/review.db sessions, PRs, findings, evidence, verdicts
|
|
188
|
+
data/sessions/<id>/events.jsonl append-only log, the UI's source of truth
|
|
189
|
+
temp/worktrees/<repo>-pr<id>/
|
|
190
|
+
temp/diffs/<repo>-pr<id>/
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Everything lives in the repo it reviewed, so nothing is lost when npm clears
|
|
194
|
+
its npx cache.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## ⚙️ Settings worth knowing
|
|
199
|
+
|
|
200
|
+
| Setting | Default | Why |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `allowCodeEdits` | off | Review is read-only. Turn it on for auto-fix or opening a PR |
|
|
203
|
+
| `permissionMode` | auto | `ask` confirms every write and shell command the agent attempts. Separate from your own clicks, which are always confirmed |
|
|
204
|
+
| `models` | Sonnet to review, Opus to challenge and chat | The broad sweep is wide, the adversarial pass is hard |
|
|
205
|
+
| `worktreeTtlHours` | 72 | Worktrees stay browsable, then are pruned on start |
|
|
206
|
+
| `sessionLanguage` | en | What Claude writes into the dashboard. The prompts it is sent stay English |
|
|
207
|
+
| `pullRequestLanguage` | en | What Claude writes into the pull request |
|
|
208
|
+
|
|
209
|
+
## ⌨️ Commands
|
|
210
|
+
|
|
211
|
+
| Command | What it does |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `npx claude-review-hub` | Install what the repo needs, then open the dashboard |
|
|
214
|
+
| `npx claude-review-hub <repo>` | Same, for a repo other than the current directory |
|
|
215
|
+
| `npx claude-review-hub install` | Only install the skills and the git exclude entries |
|
|
216
|
+
| `npx claude-review-hub --port 4320` | Prefer a specific port |
|
|
217
|
+
| `npx claude-review-hub uninstall` | Remove the skills and the git exclude entries |
|
|
218
|
+
| `npx claude-review-hub uninstall --purge` | Also delete `.review-tool/`: sessions, findings and worktrees |
|
|
219
|
+
|
|
220
|
+
`Ctrl+C` stops the dashboard, and closing the terminal stops it too.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 🛠️ Working on the tool
|
|
225
|
+
|
|
226
|
+
`npx` runs the built output, so a source change only reaches it after a
|
|
227
|
+
rebuild. To run the sources instead, against a real repo:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
git clone https://github.com/goldynlabs/claude-review-hub
|
|
231
|
+
cd claude-review-hub
|
|
232
|
+
npm install
|
|
233
|
+
npm run dev -- "C:/Source code/your-repo" # the repo is remembered
|
|
234
|
+
npm run dev # later runs reuse it
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`server/` and `web/` reload on save. `skill/` needs a restart, because a
|
|
238
|
+
project gets its copy of the skills at start-up. Ports are chosen at start-up
|
|
239
|
+
and printed, so several instances can run at once. `npm run typecheck` before
|
|
240
|
+
opening a PR.
|
|
241
|
+
|
|
242
|
+
## 📄 License
|
|
243
|
+
|
|
244
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { installSkill, isGitRepo } from "../scripts/install-skill.mjs";
|
|
6
|
+
import { uninstallTool } from "../scripts/uninstall.mjs";
|
|
7
|
+
|
|
8
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const serverEntry = path.join(here, "..", "server", "dist", "index.js");
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const COMMANDS = ["install", "uninstall", "version"];
|
|
13
|
+
const command = COMMANDS.includes(args[0]) ? args[0] : "start";
|
|
14
|
+
|
|
15
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
16
|
+
console.log(`
|
|
17
|
+
claude-review-hub [command] [repo path] [options]
|
|
18
|
+
|
|
19
|
+
The repo path is where the review happens. It defaults to the current
|
|
20
|
+
directory, so inside a repo 'claude-review-hub' on its own is enough.
|
|
21
|
+
|
|
22
|
+
(no command) Open the dashboard for that repo
|
|
23
|
+
install Only install the skill and the git exclude entries
|
|
24
|
+
uninstall Remove them again (add --purge to delete .review-tool/ as
|
|
25
|
+
well: sessions, findings and review worktrees)
|
|
26
|
+
version Print the tool version
|
|
27
|
+
|
|
28
|
+
--port <n> Preferred port (default 4319). If it is taken, the next
|
|
29
|
+
free one is used.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
npx claude-review-hub "C:\\Source code\\your-repo"
|
|
33
|
+
cd your-repo && npx claude-review-hub
|
|
34
|
+
`);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function flag(name) {
|
|
39
|
+
const index = args.indexOf(name);
|
|
40
|
+
return index === -1 ? undefined : args[index + 1];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The repo to review: the first bare argument that is not a flag's value. */
|
|
44
|
+
const positional = args.find(
|
|
45
|
+
(arg, index) => !arg.startsWith("-") && !COMMANDS.includes(arg) && !args[index - 1]?.startsWith("-"),
|
|
46
|
+
);
|
|
47
|
+
const target = path.resolve(flag("--project") ?? positional ?? process.cwd());
|
|
48
|
+
|
|
49
|
+
if (command === "version") {
|
|
50
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(here, "..", "package.json"), "utf8"));
|
|
51
|
+
console.log(pkg.version);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!isGitRepo(target)) {
|
|
56
|
+
console.error(`\n ${target} is not a git repository.\n Pass the repo to review: npx claude-review-hub <path>\n`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`\n ${path.basename(target)}`);
|
|
61
|
+
|
|
62
|
+
if (command === "uninstall") {
|
|
63
|
+
uninstallTool(target, { purge: args.includes("--purge") });
|
|
64
|
+
console.log("");
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// One command does the whole thing: the skill is (re)installed on every start,
|
|
69
|
+
// so an updated tool never leaves a stale skill behind in a project.
|
|
70
|
+
installSkill(target);
|
|
71
|
+
|
|
72
|
+
if (command === "install") {
|
|
73
|
+
console.log(`\n Ready. Run 'npx claude-review-hub' in that repo to open the dashboard.\n`);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(serverEntry)) {
|
|
78
|
+
console.error(`\n The tool has not been built yet.\n Run 'npm run build' in ${path.join(here, "..")}\n`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const port = flag("--port");
|
|
83
|
+
if (port) process.env.PORT = port;
|
|
84
|
+
process.env.REVIEW_TOOL_PROJECT = target;
|
|
85
|
+
|
|
86
|
+
await import(new URL(`file://${serverEntry.replace(/\\/g, "/")}`));
|
package/logo.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="480px" height="480px" baseProfile="basic"><rect width="36" height="24" x="6" y="18" fill="#d77757"/><rect width="3" height="9" x="9" y="39" fill="#d77757"/><rect width="3" height="9" x="15" y="39" fill="#d77757"/><rect width="3" height="9" x="30" y="39" fill="#d77757"/><rect width="3" height="9" x="36" y="39" fill="#d77757"/><rect width="7.5" height="6" y="33" fill="#d77757"/><rect width="7.5" height="6" x="40.5" y="33" fill="#d77757"/><rect width="9" height="9" x="12" y="24"/><rect width="9" height="9" x="27" y="24"/><rect width="3" height="6" x="22.5" y="22.5" transform="rotate(90 24 25.5)"/><rect width="3" height="7" x="37" y="22" transform="rotate(90 38.5 25.5)"/><rect width="3" height="7" x="8" y="22" transform="rotate(90 9.5 25.5)"/></svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-review-hub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A local dashboard that reviews pull requests with Claude Code",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "goldynlabs",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/goldynlabs/claude-review-hub.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/goldynlabs/claude-review-hub/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/goldynlabs/claude-review-hub#readme",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude",
|
|
21
|
+
"code-review",
|
|
22
|
+
"pull-request",
|
|
23
|
+
"azure-devops",
|
|
24
|
+
"github",
|
|
25
|
+
"dashboard"
|
|
26
|
+
],
|
|
27
|
+
"workspaces": [
|
|
28
|
+
"server",
|
|
29
|
+
"web"
|
|
30
|
+
],
|
|
31
|
+
"bin": {
|
|
32
|
+
"claude-review-hub": "bin/claude-review-hub.mjs"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"logo.svg",
|
|
37
|
+
"bin",
|
|
38
|
+
"scripts",
|
|
39
|
+
"skill",
|
|
40
|
+
"server/dist",
|
|
41
|
+
"web/dist"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"dev": "node scripts/dev.mjs",
|
|
45
|
+
"dev:server": "tsx watch server/src/index.ts",
|
|
46
|
+
"dev:web": "npm -w web run dev",
|
|
47
|
+
"build": "npm -w server run build && npm -w web run build",
|
|
48
|
+
"typecheck": "npm -w server run typecheck && npm -w web run typecheck",
|
|
49
|
+
"prepare": "npm run build"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.272",
|
|
53
|
+
"better-sqlite3": "^11.7.0",
|
|
54
|
+
"cors": "^2.8.5",
|
|
55
|
+
"express": "^4.21.2",
|
|
56
|
+
"nanoid": "^5.0.9",
|
|
57
|
+
"zod": "^4.1.13"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"tsx": "^4.19.2"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/scripts/dev.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Development launcher.
|
|
4
|
+
*
|
|
5
|
+
* `claude-review-hub` runs the built output, so every source change would need a
|
|
6
|
+
* build and a restart. This runs the sources instead: the server under
|
|
7
|
+
* `tsx watch`, the dashboard under Vite, both pointed at a real repo to
|
|
8
|
+
* review. Ports are chosen at start-up and handed to Vite, so a claude-review-hub
|
|
9
|
+
* already running on 4319 cannot silently steal the proxy target.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import net from "node:net";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { installSkill, isGitRepo } from "./install-skill.mjs";
|
|
17
|
+
|
|
18
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const root = path.join(here, "..");
|
|
20
|
+
// Whatever repo was last developed against; plain `npm run dev` reuses it.
|
|
21
|
+
const lastFile = path.join(root, ".dev-project");
|
|
22
|
+
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
|
|
25
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
26
|
+
console.log(`
|
|
27
|
+
npm run dev [-- <repo path>] [--api-port <n>] [--web-port <n>]
|
|
28
|
+
|
|
29
|
+
Runs the tool from source, with hot reload, against a repo to review.
|
|
30
|
+
The repo is remembered, so later runs can be just 'npm run dev'.
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
npm run dev -- "C:\\Source code\\your-repo"
|
|
34
|
+
npm run dev
|
|
35
|
+
`);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function flag(name) {
|
|
40
|
+
const index = args.indexOf(name);
|
|
41
|
+
return index === -1 ? undefined : args[index + 1];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The repo path, given bare: anything that is not a flag or a flag's value. */
|
|
45
|
+
const positional = args.find((arg, index) => !arg.startsWith("-") && !args[index - 1]?.startsWith("-"));
|
|
46
|
+
const remembered = fs.existsSync(lastFile) ? fs.readFileSync(lastFile, "utf8").trim() : "";
|
|
47
|
+
const chosen = flag("--project") ?? positional ?? remembered;
|
|
48
|
+
const target = chosen ? path.resolve(chosen) : "";
|
|
49
|
+
|
|
50
|
+
if (!chosen) {
|
|
51
|
+
console.error(`
|
|
52
|
+
Say which repo to review, once:
|
|
53
|
+
|
|
54
|
+
npm run dev -- "C:\\Source code\\your-repo"
|
|
55
|
+
|
|
56
|
+
It is remembered after that, so later runs are just 'npm run dev'.
|
|
57
|
+
`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!isGitRepo(target)) {
|
|
62
|
+
console.error(`\n ${target} is not a git repository.\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fs.writeFileSync(lastFile, target);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Asked by connecting, not by binding: Windows lets a wildcard bind succeed
|
|
70
|
+
* while another process holds the same port on a specific address, so a bind
|
|
71
|
+
* probe calls ports free that Express or Vite then fails to take.
|
|
72
|
+
*/
|
|
73
|
+
function answers(port, host) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const socket = net.connect({ port, host });
|
|
76
|
+
const done = (result) => {
|
|
77
|
+
socket.destroy();
|
|
78
|
+
resolve(result);
|
|
79
|
+
};
|
|
80
|
+
socket.setTimeout(500);
|
|
81
|
+
socket.once("connect", () => done(true));
|
|
82
|
+
socket.once("timeout", () => done(false));
|
|
83
|
+
socket.once("error", () => done(false));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The dev server must own the port Vite proxies to, so it is picked up front. */
|
|
88
|
+
async function free(from, { skip = [], attempts = 20 } = {}) {
|
|
89
|
+
for (let candidate = from; candidate < from + attempts; candidate += 1) {
|
|
90
|
+
// `skip` holds ports already handed to a child that has not bound yet;
|
|
91
|
+
// without it both halves can be told to use the same one.
|
|
92
|
+
if (skip.includes(candidate)) continue;
|
|
93
|
+
// Both families: Express listens on IPv4 and IPv6, Vite resolves localhost
|
|
94
|
+
// to ::1 first on Windows.
|
|
95
|
+
if (!(await answers(candidate, "127.0.0.1")) && !(await answers(candidate, "::1"))) return candidate;
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`No free port from ${from}.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const apiPort = Number(flag("--api-port")) || (await free(4319));
|
|
101
|
+
const webPort = Number(flag("--web-port")) || (await free(4318, { skip: [apiPort] }));
|
|
102
|
+
|
|
103
|
+
// Same as a real start: the repo gets the current skill before anything runs.
|
|
104
|
+
installSkill(target, { quiet: true });
|
|
105
|
+
|
|
106
|
+
console.log(`
|
|
107
|
+
Claude Review Hub (dev, hot reload)
|
|
108
|
+
project : ${target}
|
|
109
|
+
api : http://localhost:${apiPort}
|
|
110
|
+
open : http://localhost:${webPort}
|
|
111
|
+
|
|
112
|
+
Editing server/ or web/ reloads by itself. Editing skill/ needs a restart.
|
|
113
|
+
Ctrl+C stops both.
|
|
114
|
+
`);
|
|
115
|
+
|
|
116
|
+
const env = {
|
|
117
|
+
...process.env,
|
|
118
|
+
REVIEW_TOOL_PROJECT: target,
|
|
119
|
+
PORT: String(apiPort),
|
|
120
|
+
// Read by web/vite.config.ts, so the proxy always finds the server above.
|
|
121
|
+
API_PORT: String(apiPort),
|
|
122
|
+
// The proxy target is fixed now, so the server must not drift off this port.
|
|
123
|
+
REVIEW_TOOL_STRICT_PORT: "1",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// The local entry points are run by node directly. Going through npx and a
|
|
127
|
+
// shell would leave the real processes as grandchildren, and on Windows those
|
|
128
|
+
// survive a kill and keep holding the ports.
|
|
129
|
+
const bin = (...parts) => path.join(root, "node_modules", ...parts);
|
|
130
|
+
const children = [
|
|
131
|
+
spawn(process.execPath, [bin("tsx", "dist", "cli.mjs"), "watch", "server/src/index.ts"], {
|
|
132
|
+
cwd: root,
|
|
133
|
+
env,
|
|
134
|
+
stdio: "inherit",
|
|
135
|
+
}),
|
|
136
|
+
spawn(process.execPath, [bin("vite", "bin", "vite.js"), "--port", String(webPort), "--strictPort"], {
|
|
137
|
+
cwd: path.join(root, "web"),
|
|
138
|
+
env,
|
|
139
|
+
stdio: "inherit",
|
|
140
|
+
}),
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
let stopping = false;
|
|
144
|
+
const stopAll = () => {
|
|
145
|
+
if (stopping) return;
|
|
146
|
+
stopping = true;
|
|
147
|
+
for (const child of children) {
|
|
148
|
+
if (!child.pid) continue;
|
|
149
|
+
// tsx runs the server in a child of its own, so the whole tree has to go;
|
|
150
|
+
// a survivor would hold the port and quietly serve stale code later.
|
|
151
|
+
if (process.platform === "win32") {
|
|
152
|
+
spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore", shell: true });
|
|
153
|
+
} else {
|
|
154
|
+
child.kill();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
setTimeout(() => process.exit(0), 1500).unref();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
process.on("SIGINT", stopAll);
|
|
161
|
+
process.on("SIGTERM", stopAll);
|
|
162
|
+
// One half dying is not a working dashboard, so the other goes with it.
|
|
163
|
+
for (const child of children) child.on("exit", stopAll);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const skillRoot = path.join(here, "..", "skill");
|
|
7
|
+
|
|
8
|
+
/** One skill per host the tool supports, discovered rather than listed twice. */
|
|
9
|
+
export const SKILL_NAMES = fs
|
|
10
|
+
.readdirSync(skillRoot, { withFileTypes: true })
|
|
11
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(skillRoot, entry.name, "SKILL.md")))
|
|
12
|
+
.map((entry) => entry.name);
|
|
13
|
+
|
|
14
|
+
/** What the tool adds to a repo, and what it must therefore keep out of git. */
|
|
15
|
+
export const EXCLUDED_PATHS = [".review-tool/", ...SKILL_NAMES.map((name) => `.claude/skills/${name}/`)];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Puts the forked per-host skills in the target repo and keeps the tool's own
|
|
19
|
+
* state out of that repo's history. Idempotent: safe to run on every start.
|
|
20
|
+
*/
|
|
21
|
+
export function installSkill(target, { quiet = false } = {}) {
|
|
22
|
+
const log = (message) => !quiet && console.log(message);
|
|
23
|
+
const done = { skill: false, excluded: false };
|
|
24
|
+
|
|
25
|
+
for (const name of SKILL_NAMES) {
|
|
26
|
+
const skillTarget = path.join(target, ".claude", "skills", name);
|
|
27
|
+
const sourceFile = path.join(skillRoot, name, "SKILL.md");
|
|
28
|
+
const targetFile = path.join(skillTarget, "SKILL.md");
|
|
29
|
+
const current = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, "utf8") : null;
|
|
30
|
+
const latest = fs.readFileSync(sourceFile, "utf8");
|
|
31
|
+
|
|
32
|
+
if (current !== latest) {
|
|
33
|
+
fs.mkdirSync(path.dirname(skillTarget), { recursive: true });
|
|
34
|
+
fs.cpSync(path.join(skillRoot, name), skillTarget, { recursive: true });
|
|
35
|
+
log(` skill ${current === null ? "installed" : "updated"} -> .claude/skills/${name}`);
|
|
36
|
+
done.skill = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// .git/info/exclude, not .gitignore: that file is tracked, and a work repo
|
|
41
|
+
// must not show a modified file just because the tool was started in it.
|
|
42
|
+
const exclude = excludeFile(target);
|
|
43
|
+
if (exclude) {
|
|
44
|
+
const existing = fs.existsSync(exclude) ? fs.readFileSync(exclude, "utf8") : "";
|
|
45
|
+
const present = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
|
|
46
|
+
const missing = EXCLUDED_PATHS.filter((line) => !present.has(line));
|
|
47
|
+
if (missing.length) {
|
|
48
|
+
fs.mkdirSync(path.dirname(exclude), { recursive: true });
|
|
49
|
+
const prefix = !existing || existing.endsWith("\n") ? "" : "\n";
|
|
50
|
+
fs.appendFileSync(exclude, `${prefix}${missing.join("\n")}\n`);
|
|
51
|
+
log(" ignored locally, in .git/info/exclude (your .gitignore is untouched)");
|
|
52
|
+
done.excluded = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return done;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isGitRepo(target) {
|
|
60
|
+
return fs.existsSync(path.join(target, ".git"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves .git/info/exclude, following the gitdir pointer when the repo is
|
|
65
|
+
* itself a worktree (there .git is a file, not a directory).
|
|
66
|
+
*/
|
|
67
|
+
export function excludeFile(target) {
|
|
68
|
+
const dotGit = path.join(target, ".git");
|
|
69
|
+
if (!fs.existsSync(dotGit)) return null;
|
|
70
|
+
if (fs.statSync(dotGit).isDirectory()) return path.join(dotGit, "info", "exclude");
|
|
71
|
+
const pointer = fs.readFileSync(dotGit, "utf8").match(/^gitdir:\s*(.+)$/m);
|
|
72
|
+
if (!pointer) return null;
|
|
73
|
+
return path.join(path.resolve(target, pointer[1].trim()), "info", "exclude");
|
|
74
|
+
}
|