@fullstackhouse/cerber 0.30.0 โ†’ 0.31.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
@@ -1,18 +1,38 @@
1
1
  # cerber ๐Ÿ•
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/%40fullstackhouse%2Fcerber)](https://www.npmjs.com/package/@fullstackhouse/cerber)
4
+ [![CI](https://github.com/fullstackhouse/cerber/actions/workflows/ci.yml/badge.svg)](https://github.com/fullstackhouse/cerber/actions/workflows/ci.yml)
4
5
  [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
5
6
 
6
- **AI code-review cockpit.** Claude reviews your pull requests into local artifacts โ€”
7
- a summary, a chaptered walkthrough of the changes, draft inline comments, and a
8
- verdict with a confidence score. You go through it in a local web cockpit.
7
+ **AI code-review cockpit.** Claude reviews your pull requests into local
8
+ artifacts โ€” a summary, a chaptered walkthrough of the changes, draft inline
9
+ comments, and a verdict with a confidence score. You read it in a local web
10
+ cockpit, keep or rewrite or drop each comment, then press Send when you agree
11
+ with it.
9
12
 
10
13
  **Nothing reaches GitHub until you explicitly say so.** Cerber's only GitHub
11
14
  write is the Send button (plus opt-in daemon auto-send you turn on yourself) โ€”
12
- reviewing is 100% local and read-only.
15
+ reviewing is 100% local and read-only. Reviews go out under your own account,
16
+ as your review, because you decided each line of it should.
17
+
18
+ That gate is the whole point, and the reason this is not another review bot:
19
+
20
+ > Reviewing pull requests with an AI model creates a specific danger: a system
21
+ > that can both draft opinions and hold GitHub credentials will eventually post
22
+ > one without a human deciding it should.
13
23
 
14
24
  Named after Cerberus, the gatekeeper: cerber guards what gets merged.
15
25
 
26
+ **A drafted review, as you read it** โ€” verdict and confidence beside the PR
27
+ title, the walkthrough rail on the left, a summary written for someone who has
28
+ not read the diff:
29
+
30
+ ![A review opened in the cockpit: the verdict pill by the PR title, a walkthrough rail listing chapters and their comments on the left, and a summary written in sections โ€” problem first, then the fix, then details.](docs/walkthrough.png)
31
+
32
+ **And the queue they arrive in** โ€” `cerber serve` polls GitHub for PRs awaiting
33
+ your review and drafts one for each, so they are written by the time you get
34
+ there:
35
+
16
36
  ![The queue: three drafted reviews, each with a verdict and confidence in its row; under the cursor, a strip quotes the selected review's own reasoning so it can be judged without opening it.](docs/queue.png)
17
37
 
18
38
  *Every screenshot in this README is cerber reviewing cerber's own pull
@@ -20,11 +40,25 @@ requests.*
20
40
 
21
41
  ## Quick start
22
42
 
23
- Requires Node 20+, an authenticated [`gh`](https://cli.github.com/) and a
24
- logged-in [`claude`](https://claude.com/claude-code) CLI. No API keys, no config, no database.
43
+ Requires Node 20+, an authenticated [`gh`](https://cli.github.com/), `git`, and
44
+ a logged-in [`claude`](https://claude.com/claude-code) CLI. No API keys, no
45
+ config, no database.
46
+
47
+ **Claude Code is a hard requirement, and a paid one.** Cerber drafts reviews by
48
+ running `claude` as you: it rides your existing Claude Code login and draws on
49
+ that plan's usage limits, so you need a Claude subscription that includes Claude
50
+ Code. There is no `ANTHROPIC_API_KEY` path โ€” not as a fallback, not at all.
51
+ GitHub only; no GitLab or Bitbucket. Notifications are macOS and Linux.
52
+
53
+ Run `cerber doctor` first โ€” it checks all three tools and tells you which one to
54
+ fix, instead of letting a missing login turn into an empty cockpit later.
25
55
 
26
56
  ```bash
27
- # Review a PR (writes a local artifact to ~/.cerber, nothing else)
57
+ # Open the cockpit โ€” this is the whole product
58
+ npx @fullstackhouse/cerber serve # โ†’ http://127.0.0.1:4820
59
+
60
+ # Or review one PR from the terminal (writes a local artifact to ~/.cerber,
61
+ # nothing else)
28
62
  npx @fullstackhouse/cerber review https://github.com/owner/repo/pull/123
29
63
 
30
64
  # Review several
@@ -33,22 +67,79 @@ npx @fullstackhouse/cerber review 123 124 --repo owner/repo
33
67
 
34
68
  # See what's in the queue
35
69
  npx @fullstackhouse/cerber list
36
-
37
- # Open the cockpit โ€” it's an inbox: it polls GitHub for PRs awaiting your
38
- # review and drafts a review for each, so they're ready when you arrive
39
- npx @fullstackhouse/cerber serve # โ†’ http://127.0.0.1:4820
40
70
  ```
41
71
 
42
72
  `serve` does the whole loop by default: discover โ†’ draft โ†’ wait for you.
43
73
  Sending stays a human click. Tame it with `--no-auto-review` (list awaiting
44
74
  PRs, review on click) or `--no-poll` (no GitHub polling at all) โ€” or flip those
45
75
  switches in the cockpit's Settings, which persist in `~/.cerber/config.json`
46
- and apply on the next poll. The queue only lists what
47
- still wants you: anything you have settled moves to the filed tabs right of
48
- the thin rule in the filter row โ€” reviews you sent, ones you marked reviewed
49
- or skipped, merged and closed PRs โ€” each tab appearing only while it holds
50
- something. PRs in archived repos are never picked up at all, since the repo
51
- is read-only and a review could never be sent.
76
+ and apply on the next poll.
77
+
78
+ ## Contents
79
+
80
+ - [What a review looks like](#what-a-review-looks-like) โ€” what Claude writes and how you walk it
81
+ - [The inbox](#the-inbox) โ€” what the queue shows, what settles a row, the notifications
82
+ - [It reviews the code, not just the diff](#it-reviews-the-code-not-just-the-diff)
83
+ - [Trusted PRs: reviews that can run things](#trusted-prs-reviews-that-can-run-things)
84
+ - [When the PR moves under you](#when-the-pr-moves-under-you)
85
+ - [Arguing with the review before you send it](#arguing-with-the-review-before-you-send-it)
86
+ - [Every row remembers what happened to it](#every-row-remembers-what-happened-to-it)
87
+ - [Status](#status) ยท [Running on a VPS](#running-on-a-vps) ยท [How it works](#how-it-works) ยท [Development](#development)
88
+
89
+ ## What a review looks like
90
+
91
+ Each review is a plain JSON artifact in `~/.cerber/reviews/` (override with
92
+ `CERBER_HOME`) containing:
93
+
94
+ - **Summary** โ€” what the PR actually does and why, written top-down
95
+ - **Chapters** โ€” the changed files grouped into a logical walkthrough, each
96
+ with a title, an explanation, and its slice of the diff
97
+ - **Draft comments** โ€” inline, anchored to file/line, only things worth a
98
+ human's time. Findings are graded **blocker / minor / nit** โ€” the words mean
99
+ what they mean everywhere, and only a blocker stands between the PR and
100
+ approval; a question or note carries no grade at all
101
+ - **Verdict** โ€” approve / comment / request changes, with a 0โ€“100 confidence
102
+ score and reasoning that names the worst finding still standing
103
+ - **Run metadata** โ€” model, whether it read the source or only the diff, and
104
+ the token spend as an API-rate equivalent
105
+
106
+ The cockpit (`cerber serve`) renders the queue and the per-PR walkthrough with
107
+ diffs. Draft comments sit inline in the diff, anchored to the line they're
108
+ about, each waiting for you to keep, rewrite, or drop it:
109
+
110
+ ![A chapter of the walkthrough: the diff with a draft comment card anchored under the line it discusses, explaining the finding in plain words.](docs/comment.png)
111
+
112
+ Any line of the diff is one click away from being talked about: hover it and a
113
+ `+` appears in the gutter, the way it does on GitHub. What opens is one box
114
+ with two exits โ€” write the comment yourself, or ask the reviewer about that
115
+ line and get the answer in the conversation below. A line the PR removes can be
116
+ asked about too; a comment on one posts on the file, since GitHub only takes
117
+ inline comments on the new side of a diff.
118
+
119
+ A markdown file the PR **adds** opens as the document it is, not as a thousand
120
+ rows of `+ ## Heading` โ€” because reviewing a new spec means reading it, and
121
+ every line being an addition means the diff's markers say nothing. It is still
122
+ the review: the draft comments sit in the document under the paragraph they
123
+ point at, and any paragraph takes a comment or a question where it stands. A
124
+ markdown file the PR only *edits* stays a diff โ€” there the change is the point
125
+ โ€” with "read as a document" in its header when you want the prose instead.
126
+
127
+ Artifacts are plain JSON you can `cat`, edit, or pipe into anything.
128
+
129
+ The queue is meant to be walked, not clicked through: `j`/`k` move the cursor
130
+ and the strip under the table explains whatever it lands on โ€” the verdict's own
131
+ reasoning, what the run read, how it sits against the auto-send bar โ€” so most
132
+ rows can be judged without opening them. `โ†ต` opens one, `r` drafts (or
133
+ re-drafts) it. Inside a review, `[` and `]` walk to the previous/next PR still
134
+ awaiting you, `n` steps through the chapters, and `s` sends.
135
+
136
+ ## The inbox
137
+
138
+ The queue only lists what still wants you: anything you have settled moves to
139
+ the filed tabs right of the thin rule in the filter row โ€” reviews you sent,
140
+ ones you marked reviewed or skipped, merged and closed PRs โ€” each tab appearing
141
+ only while it holds something. PRs in archived repos are never picked up at
142
+ all, since the repo is read-only and a review could never be sent.
52
143
 
53
144
  ### Settled means you settled it
54
145
 
@@ -129,56 +220,7 @@ anything, the favicon wears a red dot, and it goes away when the last review is
129
220
  dealt with. No permission, no switch โ€” just the paw with a mark on it, so a
130
221
  glance at the tab strip answers "is there anything for me?".
131
222
 
132
- ## What a review looks like
133
-
134
- Each review is a plain JSON artifact in `~/.cerber/reviews/` (override with
135
- `CERBER_HOME`) containing:
136
-
137
- - **Summary** โ€” what the PR actually does and why, written top-down
138
- - **Chapters** โ€” the changed files grouped into a logical walkthrough, each
139
- with a title, an explanation, and its slice of the diff
140
- - **Draft comments** โ€” inline, anchored to file/line, only things worth a
141
- human's time. Findings are graded **blocker / minor / nit** โ€” the words mean
142
- what they mean everywhere, and only a blocker stands between the PR and
143
- approval; a question or note carries no grade at all
144
- - **Verdict** โ€” approve / comment / request changes, with a 0โ€“100 confidence
145
- score and reasoning that names the worst finding still standing
146
- - **Run metadata** โ€” model, whether it read the source or only the diff, and
147
- the token spend as an API-rate equivalent
148
-
149
- ![A review opened in the cockpit: the verdict pill by the PR title, a walkthrough rail listing chapters and their comments on the left, and a summary written in sections โ€” problem first, then the fix, then details.](docs/walkthrough.png)
150
-
151
- The cockpit (`cerber serve`) renders the queue and the per-PR walkthrough with
152
- diffs. Draft comments sit inline in the diff, anchored to the line they're
153
- about, each waiting for you to keep, rewrite, or drop it:
154
-
155
- ![A chapter of the walkthrough: the diff with a draft comment card anchored under the line it discusses, explaining the finding in plain words.](docs/comment.png)
156
-
157
- Any line of the diff is one click away from being talked about: hover it and a
158
- `+` appears in the gutter, the way it does on GitHub. What opens is one box
159
- with two exits โ€” write the comment yourself, or ask the reviewer about that
160
- line and get the answer in the conversation below. A line the PR removes can be
161
- asked about too; a comment on one posts on the file, since GitHub only takes
162
- inline comments on the new side of a diff.
163
-
164
- A markdown file the PR **adds** opens as the document it is, not as a thousand
165
- rows of `+ ## Heading` โ€” because reviewing a new spec means reading it, and
166
- every line being an addition means the diff's markers say nothing. It is still
167
- the review: the draft comments sit in the document under the paragraph they
168
- point at, and any paragraph takes a comment or a question where it stands. A
169
- markdown file the PR only *edits* stays a diff โ€” there the change is the point
170
- โ€” with "read as a document" in its header when you want the prose instead.
171
-
172
- Artifacts are plain JSON you can `cat`, edit, or pipe into anything.
173
-
174
- The queue is meant to be walked, not clicked through: `j`/`k` move the cursor
175
- and the strip under the table explains whatever it lands on โ€” the verdict's own
176
- reasoning, what the run read, how it sits against the auto-send bar โ€” so most
177
- rows can be judged without opening them. `โ†ต` opens one, `r` drafts (or
178
- re-drafts) it. Inside a review, `[` and `]` walk to the previous/next PR still
179
- awaiting you, `n` steps through the chapters, and `s` sends.
180
-
181
- ### It reviews the code, not just the diff
223
+ ## It reviews the code, not just the diff
182
224
 
183
225
  Cerber checks the PR's head out locally and lets the review read it. That
184
226
  matters because a reviewer holding only a diff hedges over things it could have
@@ -235,7 +277,7 @@ Details worth knowing:
235
277
  review is currently reading. `cerber prune` reclaims the space now;
236
278
  `cerber prune --all` takes the ones for reviews still awaiting you too.
237
279
 
238
- ### Trusted PRs: reviews that can run things
280
+ ## Trusted PRs: reviews that can run things
239
281
 
240
282
  Reading beats guessing, but running beats reading. A review that ran the test
241
283
  covering the change reports what happened; one that only read it guesses. So
@@ -304,7 +346,7 @@ unattended by default: with trust rules set, it will run matching PRs' code
304
346
  with nobody watching. It warns at startup, the cockpit shows a โšก badge while
305
347
  it's the case, and `--no-trust` (or `--no-auto-review`) turns it off.
306
348
 
307
- ### When the PR moves under you
349
+ ## When the PR moves under you
308
350
 
309
351
  A review is written against one commit, but authors keep pushing. Opening a
310
352
  review checks the PR's head and pulls the review forward: comments follow their
@@ -315,10 +357,17 @@ commit's SHA, so inline comments land where they were written.
315
357
 
316
358
  The AI's summary and verdict still describe the commit that was reviewed. To
317
359
  get its opinion of the new code, hit **Re-review at the new head** in the
318
- cockpit (or `cerber review <pr> --force`) โ€” comments you wrote or rewrote are
319
- carried into the fresh review.
360
+ cockpit (or `cerber review <pr> --force`).
361
+
362
+ > โš ๏ธ **A re-review replaces the whole draft โ€” including comments you wrote or
363
+ > rewrote, and a send body you wrote yourself.** They are dropped when the run
364
+ > starts and they do not come back, on success or on failure. This is a
365
+ > decision, not a bug ([`docs/lifecycle.md`](docs/lifecycle.md)): the run owns
366
+ > the draft. If you have written comments you want to keep, send the review or
367
+ > copy them out first. Your *decisions* do survive โ€” a send, a `reviewed` or
368
+ > `skipped` you set, and the chat transcript are all kept.
320
369
 
321
- ### Arguing with the review before you send it
370
+ ## Arguing with the review before you send it
322
371
 
323
372
  A draft you disagree with used to leave two options: rewrite it by hand, or
324
373
  re-review and hope. Neither lets you say *what was wrong*. So every review has
@@ -376,7 +425,7 @@ the summary, comments and verdict alone, and is still one deliberate click. A
376
425
  review that has already been sent can't be argued with: that artifact is the
377
426
  record of what GitHub has.
378
427
 
379
- ### Every row remembers what happened to it
428
+ ## Every row remembers what happened to it
380
429
 
381
430
  A review keeps one "last updated" time, which means every write erases the
382
431
  answer to *when did I skip this, and did they ask again afterwards?* So each
@@ -455,6 +504,15 @@ pnpm build # dist/ (CLI+server) + web/dist (cockpit)
455
504
  Web cockpit dev with hot reload: `pnpm dev serve` in one terminal,
456
505
  `npx vite --config web/vite.config.ts` in another (proxies `/api`).
457
506
 
507
+ ## Contributing and security
508
+
509
+ Patches welcome โ€” [`CONTRIBUTING.md`](./CONTRIBUTING.md) has the gate
510
+ (`pnpm typecheck && pnpm test`) and the one rule that is not negotiable.
511
+
512
+ Cerber holds your GitHub credentials and, on a trusted PR, runs that PR's code.
513
+ [`SECURITY.md`](./SECURITY.md) says exactly what that means, what is stripped
514
+ from a trusted run, and where to report a vulnerability privately.
515
+
458
516
  ## License
459
517
 
460
518
  MIT ยฉ [Full Stack House](https://fullstack.house)
package/dist/cli/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
+ import { createRequire } from "node:module";
3
4
  import readline from "node:readline/promises";
4
5
  import { artifactId, artifactKey } from "../core/artifact.js";
5
6
  import { listCheckouts, removeCheckout } from "../core/checkout.js";
6
7
  import { toMarkdown } from "../core/export.js";
7
8
  import { configPath, loadConfig, saveConfig } from "../core/config.js";
9
+ import { formatBlockers, isBlocking, preflight } from "../core/preflight.js";
8
10
  import { withWriter } from "../core/history.js";
9
11
  import { TrustRuleError, describeRule, explainRule, parseTrustRule } from "../core/trust.js";
10
12
  import { parsePrRef, searchAwaitingMe, submitReview } from "../core/gh.js";
@@ -15,10 +17,34 @@ import { isReviewRunning } from "../runner/inflight.js";
15
17
  import { startDaemon } from "../server/daemon.js";
16
18
  import { cockpitUrl, startServer } from "../server/index.js";
17
19
  const program = new Command();
20
+ /**
21
+ * The published version, read from the manifest rather than typed here โ€” a
22
+ * hand-maintained copy drifted to 0.5.0 while 0.30.0 shipped, and `--version`
23
+ * is the first thing anyone runs to decide whether a project is alive.
24
+ */
25
+ const { version } = createRequire(import.meta.url)("../../package.json");
26
+ /**
27
+ * Stop before the work starts when a tool cerber shells out to is missing or
28
+ * logged out, rather than letting it surface as an empty queue or a retrying
29
+ * poll. `allowDegraded` is for `serve --no-poll`, which is still useful for
30
+ * reading artifacts already on disk.
31
+ */
32
+ async function requireTools(allowDegraded = false) {
33
+ const checks = await preflight();
34
+ if (!checks.some(isBlocking))
35
+ return;
36
+ const message = formatBlockers(checks);
37
+ if (allowDegraded) {
38
+ console.warn(`\n${message}\n`);
39
+ return;
40
+ }
41
+ console.error(`\n${message}\n`);
42
+ process.exit(1);
43
+ }
18
44
  program
19
45
  .name("cerber")
20
46
  .description("AI code-review cockpit. Claude reviews PRs into local artifacts; nothing reaches GitHub until you explicitly send it.")
21
- .version("0.5.0");
47
+ .version(version);
22
48
  program
23
49
  .command("review")
24
50
  .description("Run an AI review of one or more PRs (local artifact only โ€” no GitHub writes)")
@@ -32,6 +58,7 @@ program
32
58
  .option("-t, --trust", "let the review run commands in the checkout (tests, typecheck, git log) whatever the trust config says")
33
59
  .option("--no-trust", "keep the review read-only even if a trust rule matches")
34
60
  .action(async (prs, opts) => {
61
+ await requireTools();
35
62
  let refs;
36
63
  if (opts.awaitingMe) {
37
64
  const found = await searchAwaitingMe(opts.repo);
@@ -349,6 +376,31 @@ program
349
376
  `${wouldSend} shadow candidate(s), ${autoSent} auto-sent.`);
350
377
  }
351
378
  });
379
+ program
380
+ .command("doctor")
381
+ .description("Check that everything cerber needs is installed and logged in")
382
+ .action(async () => {
383
+ const checks = await preflight();
384
+ for (const c of checks) {
385
+ if (c.status === "ok") {
386
+ console.log(` โœ” ${c.name.padEnd(22)} ${c.detail ?? ""}`.trimEnd());
387
+ }
388
+ else {
389
+ console.log(` โœ— ${c.name.padEnd(22)} ${c.status === "missing" ? "not installed" : "not logged in"}`);
390
+ if (c.fix)
391
+ console.log(` ${c.fix}`);
392
+ }
393
+ }
394
+ const bad = checks.filter(isBlocking);
395
+ console.log("");
396
+ if (bad.length === 0) {
397
+ console.log("Ready. `cerber serve` opens the cockpit.");
398
+ }
399
+ else {
400
+ console.log(`${bad.length} thing(s) to fix before cerber can review anything.`);
401
+ process.exitCode = 1;
402
+ }
403
+ });
352
404
  program
353
405
  .command("serve", { isDefault: true })
354
406
  .description("Start the review cockpit โ€” an inbox: it polls GitHub for PRs awaiting your review and drafts " +
@@ -377,6 +429,9 @@ program
377
429
  `Pass --token <secret> (or CERBER_TOKEN), or --insecure if the network is trusted.`);
378
430
  process.exit(1);
379
431
  }
432
+ // Without `gh` the cockpit binds, polls into an error loop and shows an
433
+ // empty queue; `--no-poll` is the one shape that still works offline.
434
+ await requireTools(!opts.poll);
380
435
  const threshold = Math.min(100, Math.max(50, Number(opts.autoSendThreshold) || 90));
381
436
  if (opts.autoSend) {
382
437
  console.log(`โš  auto-send is ON: APPROVE verdicts with confidence โ‰ฅ ${threshold}% will be sent to GitHub as you, without a per-review confirmation.\n` +
package/dist/core/gh.js CHANGED
@@ -68,8 +68,13 @@ export async function fetchPrInfo(ref) {
68
68
  }
69
69
  /**
70
70
  * Org and team membership, for `@org/*` and `@org/team` trust rules. A 404 is
71
- * GitHub's "no" and the answer we want; anything else (no `read:org` scope, an
72
- * outage) throws, so a broken check can never read as "trusted".
71
+ * GitHub's "no"; anything else (an outage, a rate limit) throws. Either way the
72
+ * answer is never "trusted", which is the property that matters.
73
+ *
74
+ * Note that a token without `read:org` also gets a 404 rather than a 403 โ€” so a
75
+ * missing scope reads as "not a member" and silently narrows trust instead of
76
+ * announcing itself. Fail-closed, but quiet: if a rule you expect to match
77
+ * doesn't, check the scope before the rule.
73
78
  */
74
79
  export async function isOrgMember(org, login) {
75
80
  return membershipCheck(["api", `orgs/${org}/members/${login}`, "--silent"]);
@@ -0,0 +1,82 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ export function isBlocking(check) {
5
+ return check.status !== "ok";
6
+ }
7
+ async function run(bin, args) {
8
+ try {
9
+ const { stdout, stderr } = await execFileAsync(bin, args);
10
+ return { ok: true, out: (stdout || stderr).trim() };
11
+ }
12
+ catch (err) {
13
+ const e = err;
14
+ // ENOENT is "not installed"; a non-zero exit is the tool answering "no".
15
+ if (e.code === "ENOENT")
16
+ return { ok: false, out: "ENOENT" };
17
+ return { ok: false, out: (e.stderr || e.stdout || e.message || "").trim() };
18
+ }
19
+ }
20
+ export async function checkGh() {
21
+ const version = await run("gh", ["--version"]);
22
+ if (!version.ok && version.out === "ENOENT") {
23
+ return {
24
+ name: "gh (GitHub CLI)",
25
+ status: "missing",
26
+ fix: "Install it: https://cli.github.com โ€” then run `gh auth login`.",
27
+ };
28
+ }
29
+ // `gh auth status` exits non-zero when no account is logged in, which is the
30
+ // case that otherwise surfaces as an empty cockpit.
31
+ const auth = await run("gh", ["auth", "status"]);
32
+ if (!auth.ok) {
33
+ return {
34
+ name: "gh (GitHub CLI)",
35
+ status: "unauthenticated",
36
+ fix: "Run `gh auth login` โ€” cerber reads PRs as you, and sends as you.",
37
+ };
38
+ }
39
+ return { name: "gh (GitHub CLI)", status: "ok", detail: version.out.split("\n")[0] };
40
+ }
41
+ export async function checkClaude() {
42
+ const version = await run("claude", ["--version"]);
43
+ if (!version.ok && version.out === "ENOENT") {
44
+ return {
45
+ name: "claude (Claude Code)",
46
+ status: "missing",
47
+ fix: "Install it: https://claude.com/claude-code โ€” cerber rides your login, there is no API-key path.",
48
+ };
49
+ }
50
+ if (!version.ok) {
51
+ return {
52
+ name: "claude (Claude Code)",
53
+ status: "unauthenticated",
54
+ fix: "Run `claude` once and log in โ€” a review is drafted through your Claude Code session.",
55
+ };
56
+ }
57
+ return { name: "claude (Claude Code)", status: "ok", detail: version.out.split("\n")[0] };
58
+ }
59
+ export async function checkGit() {
60
+ const version = await run("git", ["--version"]);
61
+ if (!version.ok) {
62
+ return {
63
+ name: "git",
64
+ status: "missing",
65
+ fix: "Install git โ€” cerber fetches each PR head into ~/.cerber/src to review the code, not just the diff.",
66
+ };
67
+ }
68
+ return { name: "git", status: "ok", detail: version.out.split("\n")[0] };
69
+ }
70
+ /** Everything cerber shells out to, checked in parallel. */
71
+ export async function preflight() {
72
+ return Promise.all([checkGh(), checkClaude(), checkGit()]);
73
+ }
74
+ /** The blocking checks, formatted as the message printed instead of failing later. */
75
+ export function formatBlockers(checks) {
76
+ const bad = checks.filter(isBlocking);
77
+ const lines = bad.map((c) => {
78
+ const what = c.status === "missing" ? "not installed" : "not logged in";
79
+ return ` โœ— ${c.name} โ€” ${what}\n ${c.fix ?? ""}`.trimEnd();
80
+ });
81
+ return `cerber needs these before it can do anything:\n\n${lines.join("\n")}\n\nRun \`cerber doctor\` to re-check.`;
82
+ }
package/package.json CHANGED
@@ -1,8 +1,20 @@
1
1
  {
2
2
  "name": "@fullstackhouse/cerber",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "AI code-review cockpit: Claude reviews your PRs into local artifacts; nothing reaches GitHub until you say so.",
5
5
  "license": "MIT",
6
+ "keywords": [
7
+ "code-review",
8
+ "pull-request",
9
+ "ai",
10
+ "claude",
11
+ "claude-code",
12
+ "github",
13
+ "cli",
14
+ "local-first",
15
+ "developer-tools",
16
+ "human-in-the-loop"
17
+ ],
6
18
  "author": "Full Stack House (https://fullstack.house)",
7
19
  "repository": "github:fullstackhouse/cerber",
8
20
  "type": "module",