diffprism 0.44.0 → 0.47.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/dist/bin.js CHANGED
@@ -1,25 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- createGitHubClient,
4
- fetchPullRequest,
5
- fetchPullRequestDiff,
6
3
  isPrRef,
7
- normalizePr,
8
- parsePrRef,
9
- resolveGitHubToken,
10
- submitGitHubReview
11
- } from "./chunk-6J6PSBL2.js";
4
+ parsePrRef
5
+ } from "./chunk-24B33UN6.js";
12
6
  import {
13
7
  demo
14
- } from "./chunk-LH6H3QSC.js";
8
+ } from "./chunk-TZ5SGWHP.js";
15
9
  import {
16
10
  ensureServer,
17
11
  isServerAlive,
18
12
  readServerFile,
19
13
  startGlobalServer,
20
14
  submitReviewToServer
21
- } from "./chunk-4IQOTAHD.js";
22
- import "./chunk-QGWYCEJN.js";
15
+ } from "./chunk-PQWU5NW4.js";
16
+ import {
17
+ getDiff
18
+ } from "./chunk-QGWYCEJN.js";
23
19
  import "./chunk-DHCVZGHE.js";
24
20
  import "./chunk-JSBRDJBE.js";
25
21
 
@@ -27,7 +23,6 @@ import "./chunk-JSBRDJBE.js";
27
23
  import { Command } from "commander";
28
24
 
29
25
  // cli/src/commands/review.ts
30
- import readline from "readline";
31
26
  async function review(ref, flags) {
32
27
  let diffRef;
33
28
  if (flags.staged) {
@@ -46,72 +41,54 @@ async function review(ref, flags) {
46
41
  await reviewLocalFlow(diffRef, flags);
47
42
  }
48
43
  } catch (err) {
49
- const message = err instanceof Error ? err.message : String(err);
50
- console.error(`Error: ${message}`);
44
+ const message2 = err instanceof Error ? err.message : String(err);
45
+ console.error(`Error: ${message2}`);
51
46
  process.exit(1);
52
47
  }
53
48
  }
54
49
  async function reviewLocalFlow(diffRef, flags) {
55
50
  const serverInfo = await ensureServer({ dev: flags.dev });
56
- console.log("Opening review in browser...");
51
+ console.error("Opening review in browser...");
57
52
  const { result } = await submitReviewToServer(serverInfo, diffRef, {
58
53
  title: flags.title,
59
54
  cwd: process.cwd(),
60
55
  diffRef
61
56
  });
62
57
  console.log(JSON.stringify(result, null, 2));
63
- process.exit(0);
58
+ if (!result) {
59
+ console.error("No review result was returned.");
60
+ process.exit(1);
61
+ }
62
+ const approved = result.decision === "approved" || result.decision === "approved_with_comments";
63
+ process.exit(approved ? 0 : 1);
64
64
  }
65
65
  async function reviewPrFlow(pr, flags) {
66
- const token = resolveGitHubToken();
67
66
  const { owner, repo, number } = parsePrRef(pr);
68
67
  console.log(`Fetching PR #${number} from ${owner}/${repo}...`);
69
- const client = createGitHubClient(token);
70
- const [prMetadata, rawDiff] = await Promise.all([
71
- fetchPullRequest(client, owner, repo, number),
72
- fetchPullRequestDiff(client, owner, repo, number)
73
- ]);
74
- if (!rawDiff.trim()) {
75
- console.log("PR has no changes to review.");
76
- return;
77
- }
78
- const { payload, diffSet } = normalizePr(rawDiff, prMetadata, {
79
- title: flags.title,
80
- reasoning: flags.reasoning
81
- });
82
- console.log(
83
- `${diffSet.files.length} files, +${diffSet.files.reduce((s, f) => s + f.additions, 0)} -${diffSet.files.reduce((s, f) => s + f.deletions, 0)}`
84
- );
85
68
  const serverInfo = await ensureServer({ dev: flags.dev });
86
- const { result } = await submitReviewToServer(serverInfo, `PR #${number}`, {
87
- injectedPayload: payload,
88
- projectPath: `github:${owner}/${repo}`,
89
- diffRef: `PR #${number}`
90
- });
91
- console.log(JSON.stringify(result, null, 2));
92
- if (result && (flags.postToGithub || result.decision !== "dismissed" && await promptPostToGithub())) {
93
- console.log("Posting review to GitHub...");
94
- const posted = await submitGitHubReview(client, owner, repo, number, result);
95
- if (posted) {
96
- console.log(`Review posted: ${prMetadata.url}#pullrequestreview-${posted.reviewId}`);
69
+ const response = await fetch(
70
+ `http://localhost:${serverInfo.httpPort}/api/pr/open`,
71
+ {
72
+ method: "POST",
73
+ headers: { "Content-Type": "application/json" },
74
+ body: JSON.stringify({ prUrl: pr })
97
75
  }
76
+ );
77
+ const data = await response.json();
78
+ if (!response.ok || !data.sessionId) {
79
+ console.error(`Error: ${data.error ?? "Failed to open PR"}`);
80
+ process.exit(1);
98
81
  }
99
- process.exit(0);
100
- }
101
- async function promptPostToGithub() {
102
- if (!process.stdin.isTTY) {
103
- return false;
82
+ console.log(`${data.pr?.title ?? `PR #${number}`}`);
83
+ console.log(`${data.fileCount} file${data.fileCount !== 1 ? "s" : ""} changed`);
84
+ if (data.localRepoPath) {
85
+ console.log(`Local repo: ${data.localRepoPath}`);
86
+ } else {
87
+ console.log("No local clone detected \u2014 file context unavailable");
104
88
  }
105
- const rl = readline.createInterface({
106
- input: process.stdin,
107
- output: process.stdout
108
- });
109
- return new Promise((resolve) => {
110
- rl.question("Post this review to GitHub? (y/N) ", (answer) => {
111
- rl.close();
112
- resolve(answer.toLowerCase() === "y");
113
- });
114
- });
89
+ console.log(`
90
+ Review open in browser. Use Claude Code to ask questions about this PR.`);
91
+ console.log(`MCP tools available: get_pr_context, get_file_diff, get_file_context, add_review_comment`);
115
92
  }
116
93
 
117
94
  // cli/src/commands/serve.ts
@@ -124,7 +101,7 @@ async function serve() {
124
101
  import fs from "fs";
125
102
  import path from "path";
126
103
  import os from "os";
127
- import readline2 from "readline";
104
+ import readline from "readline";
128
105
 
129
106
  // cli/src/templates/skill.ts
130
107
  var skillContent = `---
@@ -134,7 +111,7 @@ description: Open current code changes in DiffPrism's browser-based review UI fo
134
111
 
135
112
  # DiffPrism Review
136
113
 
137
- You have 9 DiffPrism MCP tools available. Use them proactively \u2014 don't wait for the user to ask.
114
+ You have 14 DiffPrism MCP tools available. Use them proactively \u2014 don't wait for the user to ask.
138
115
 
139
116
  ## Workflow 1: Self-Review Before Human Review
140
117
 
@@ -173,22 +150,32 @@ Handle the review result:
173
150
  - If \`postReviewAction\` is \`"commit"\` \u2014 commit the changes.
174
151
  - If \`postReviewAction\` is \`"commit_and_pr"\` \u2014 commit and open a PR.
175
152
 
176
- ## Workflow 3: PR Review
153
+ ## Workflow 3: PR Super Review
154
+
155
+ When the user opens a GitHub PR for review (via \`diffprism review <PR URL>\` or the DiffPrism UI), you become their AI-powered code reviewer. The diff is visible in the browser; you provide the intelligence.
177
156
 
178
- To review a GitHub pull request:
157
+ ### Getting oriented
158
+ 1. Call \`mcp__diffprism__get_pr_context\` to understand the PR: title, author, branches, file list, briefing summary, and whether a local repo is connected.
179
159
 
180
- 1. Call \`mcp__diffprism__review_pr\` with \`pr: "owner/repo#123"\` or a full GitHub PR URL
181
- 2. Set \`post_to_github: true\` to post the review back to GitHub after the human submits
182
- 3. The tool fetches the PR diff, runs analysis, and opens the review UI
160
+ ### Investigating changes
161
+ 2. Call \`mcp__diffprism__get_file_diff\` for specific files to see their hunks and triage category (critical/notable/mechanical).
162
+ 3. Call \`mcp__diffprism__get_file_context\` to read full files from the local repo \u2014 this gives you surrounding code, not just diff hunks. Use this to understand how changed code fits into the broader file.
163
+ 4. Call \`mcp__diffprism__get_user_focus\` to see what file/line the user is currently viewing in the browser. Proactively offer context about what they're looking at.
164
+
165
+ ### Leaving findings
166
+ 5. Call \`mcp__diffprism__add_review_comment\` to post findings directly to the browser UI. Comments appear as inline annotations on the diff in real-time. Use this to flag issues, suggest improvements, or answer the user's questions visually.
167
+ 6. Call \`mcp__diffprism__get_review_comments\` to see what's already been noted before adding your own.
168
+
169
+ ### Key principle
170
+ The user sees the diff in the browser. You see it through MCP tools. Work together \u2014 they spot visual patterns, you analyze logic and context.
183
171
 
184
172
  ## Tool Reference
185
173
 
186
174
  ### Review Lifecycle
187
175
  | Tool | Purpose |
188
176
  |------|---------|
189
- | \`open_review\` | Open browser review UI for local changes. Blocks until submitted. |
190
- | \`review_pr\` | Open browser review UI for a GitHub PR. Blocks until submitted. |
191
- | \`get_review_result\` | Fetch result from a previous review (advanced \u2014 \`open_review\` already returns it). |
177
+ | \`open_review\` | Open browser review UI for local changes or a GitHub PR. |
178
+ | \`get_review_result\` | Fetch result from a previous review. |
192
179
  | \`update_review_context\` | Push updated reasoning/description to a running review session. |
193
180
 
194
181
  ### Headless Analysis
@@ -197,10 +184,20 @@ To review a GitHub pull request:
197
184
  | \`analyze_diff\` | Returns analysis JSON (patterns, complexity, test gaps) without opening a browser. |
198
185
  | \`get_diff\` | Returns structured diff JSON (file-level and hunk-level changes). |
199
186
 
200
- ### Annotation & Flagging
187
+ ### PR Super Review
188
+ | Tool | Purpose |
189
+ |------|---------|
190
+ | \`get_pr_context\` | High-level PR overview: metadata, briefing, file list, local repo status. |
191
+ | \`get_file_diff\` | Diff hunks for a specific file with triage category. |
192
+ | \`get_file_context\` | Full file content from local repo via \`git show\`. |
193
+ | \`get_user_focus\` | What file/line the user is currently viewing in the browser UI. |
194
+
195
+ ### Annotation & Commenting
201
196
  | Tool | Purpose |
202
197
  |------|---------|
203
- | \`add_annotation\` | Post a finding/suggestion/question on a specific line in a running review. |
198
+ | \`add_review_comment\` | Post a comment that appears inline in the browser diff. |
199
+ | \`get_review_comments\` | Read all comments and annotations on the session. |
200
+ | \`add_annotation\` | Post a structured finding (finding/suggestion/question/warning). |
204
201
  | \`flag_for_attention\` | Mark files for human attention with warning annotations. |
205
202
  | \`get_review_state\` | Get current state of a review session including all annotations. |
206
203
 
@@ -209,6 +206,7 @@ To review a GitHub pull request:
209
206
  - **Self-review is proactive** \u2014 run \`analyze_diff\` after significant changes without being asked.
210
207
  - **Human review requires explicit request** \u2014 only open \`open_review\` when the user asks (\`/review\`, "review my changes", or as part of a defined workflow like PR creation).
211
208
  - **Annotate generously** \u2014 the more context you provide in annotations, the faster the reviewer can make decisions.
209
+ - **PR review is conversational** \u2014 when a PR is open, use the super review tools to answer questions and post findings without being asked to use specific tools.
212
210
  `;
213
211
 
214
212
  // cli/src/commands/setup.ts
@@ -306,7 +304,7 @@ function setupSkill(gitRoot, global, force) {
306
304
  return { action, filePath };
307
305
  }
308
306
  async function promptUser(question) {
309
- const rl = readline2.createInterface({
307
+ const rl = readline.createInterface({
310
308
  input: process.stdin,
311
309
  output: process.stdout
312
310
  });
@@ -379,7 +377,7 @@ async function setupInteractive(flags) {
379
377
  }
380
378
  async function runDemo(dev) {
381
379
  console.log("");
382
- const { demo: demo2 } = await import("./demo-MDPKCAXZ.js");
380
+ const { demo: demo2 } = await import("./demo-H2NAMKCT.js");
383
381
  await demo2({ dev });
384
382
  }
385
383
  async function setupBatch(flags) {
@@ -712,9 +710,9 @@ async function server(flags) {
712
710
  await new Promise(() => {
713
711
  });
714
712
  } catch (err) {
715
- const message = err instanceof Error ? err.message : String(err);
713
+ const message2 = err instanceof Error ? err.message : String(err);
716
714
  if (!isDaemon) {
717
- console.error(`Error starting server: ${message}`);
715
+ console.error(`Error starting server: ${message2}`);
718
716
  }
719
717
  process.exit(1);
720
718
  }
@@ -787,8 +785,8 @@ async function serverStatus() {
787
785
  }
788
786
  }
789
787
  } catch (err) {
790
- const message = err instanceof Error ? err.message : String(err);
791
- console.error(`Error checking server status: ${message}`);
788
+ const message2 = err instanceof Error ? err.message : String(err);
789
+ console.error(`Error checking server status: ${message2}`);
792
790
  process.exit(1);
793
791
  }
794
792
  }
@@ -822,7 +820,7 @@ async function fetchStatus(httpPort) {
822
820
  return await response.json();
823
821
  }
824
822
  function printStatus(status, httpPort) {
825
- const version = true ? "0.44.0" : "0.0.0-dev";
823
+ const version = true ? "0.47.0" : "0.0.0-dev";
826
824
  console.log(`
827
825
  DiffPrism v${version}
828
826
  `);
@@ -854,19 +852,211 @@ async function defaultAction() {
854
852
  await open(status.uiUrl);
855
853
  }
856
854
  } catch (err) {
857
- const message = err instanceof Error ? err.message : String(err);
858
- console.error(`Error fetching server status: ${message}`);
855
+ const message2 = err instanceof Error ? err.message : String(err);
856
+ console.error(`Error fetching server status: ${message2}`);
859
857
  process.exit(1);
860
858
  }
861
859
  }
862
860
 
861
+ // cli/src/commands/hook.ts
862
+ import { execFileSync } from "child_process";
863
+ import fs4 from "fs";
864
+ import path4 from "path";
865
+ var DEFAULT_MIN_LINES = 120;
866
+ var MARKER_START = "# >>> diffprism >>>";
867
+ var MARKER_END = "# <<< diffprism <<<";
868
+ var HOOK_LINE = "diffprism hook pre-commit || exit 1";
869
+ async function preCommitHook(flags = {}) {
870
+ const cwd = process.cwd();
871
+ const minLines = resolveMinLines(flags, cwd);
872
+ let changedLines;
873
+ try {
874
+ const { diffSet } = getDiff("staged", { cwd });
875
+ changedLines = diffSet.files.reduce(
876
+ (total, file) => total + file.additions + file.deletions,
877
+ 0
878
+ );
879
+ } catch (err) {
880
+ fail(`Could not read the staged diff: ${message(err)}`);
881
+ return;
882
+ }
883
+ if (changedLines === 0) {
884
+ process.exit(0);
885
+ }
886
+ if (changedLines < minLines) {
887
+ process.exit(0);
888
+ }
889
+ console.error(
890
+ `${changedLines} staged lines (gate at ${minLines}) \u2014 opening DiffPrism review...`
891
+ );
892
+ let review2 = null;
893
+ try {
894
+ const serverInfo = await ensureServer({ dev: flags.dev });
895
+ const { result } = await submitReviewToServer(serverInfo, "staged", {
896
+ cwd,
897
+ diffRef: "staged",
898
+ title: "Pre-commit review"
899
+ });
900
+ review2 = result;
901
+ } catch (err) {
902
+ fail(`DiffPrism could not run the review: ${message(err)}`);
903
+ return;
904
+ }
905
+ const decision = review2?.decision;
906
+ if (decision === "approved" || decision === "approved_with_comments") {
907
+ console.error(`Review: ${decision} \u2014 proceeding.`);
908
+ process.exit(0);
909
+ }
910
+ if (decision === "changes_requested") {
911
+ printComments(review2?.comments ?? []);
912
+ fail("Commit blocked: the review requested changes.");
913
+ return;
914
+ }
915
+ if (decision === "dismissed") {
916
+ fail("Commit blocked: the review was dismissed without a decision.");
917
+ return;
918
+ }
919
+ fail(
920
+ `Commit blocked: no review decision was returned (got ${String(decision)}).`
921
+ );
922
+ }
923
+ function printComments(comments) {
924
+ if (comments.length === 0) {
925
+ return;
926
+ }
927
+ console.error("");
928
+ for (const c of comments) {
929
+ console.error(` ${c.file}:${c.line} [${c.type}] ${c.body}`);
930
+ }
931
+ console.error("");
932
+ }
933
+ function installHook() {
934
+ const hookPath = resolveHookPath(process.cwd());
935
+ const existing = fs4.existsSync(hookPath) ? fs4.readFileSync(hookPath, "utf8") : "";
936
+ if (existing.includes(MARKER_START)) {
937
+ console.log(`Already installed in ${hookPath}`);
938
+ return;
939
+ }
940
+ const block = `${MARKER_START}
941
+ ${HOOK_LINE}
942
+ ${MARKER_END}
943
+ `;
944
+ const base = existing === "" ? "#!/bin/sh\n" : ensureTrailingNewline(existing);
945
+ const created = existing === "";
946
+ fs4.mkdirSync(path4.dirname(hookPath), { recursive: true });
947
+ fs4.writeFileSync(hookPath, `${base}
948
+ ${block}`);
949
+ fs4.chmodSync(hookPath, 493);
950
+ console.log(`${created ? "Created" : "Updated"} ${hookPath}`);
951
+ console.log(
952
+ `Staged changes of ${DEFAULT_MIN_LINES}+ lines now open a review before the commit lands.`
953
+ );
954
+ console.log("Tune with: git config diffprism.gate-lines <n>");
955
+ console.log("Remove with: diffprism hook uninstall");
956
+ }
957
+ function uninstallHook() {
958
+ const hookPath = resolveHookPath(process.cwd());
959
+ if (!fs4.existsSync(hookPath)) {
960
+ console.log("Nothing to remove \u2014 no hook file.");
961
+ return;
962
+ }
963
+ const existing = fs4.readFileSync(hookPath, "utf8");
964
+ if (!existing.includes(MARKER_START)) {
965
+ console.log(`Nothing to remove \u2014 no diffprism block in ${hookPath}`);
966
+ return;
967
+ }
968
+ const cleaned = removeMarkedBlock(existing);
969
+ if (/^\s*(#![^\n]*)?\s*$/.test(cleaned)) {
970
+ fs4.rmSync(hookPath);
971
+ console.log(`Removed ${hookPath} (it contained nothing else).`);
972
+ return;
973
+ }
974
+ fs4.writeFileSync(hookPath, cleaned);
975
+ console.log(`Removed the diffprism block from ${hookPath}`);
976
+ }
977
+ function removeMarkedBlock(contents) {
978
+ const lines = contents.split("\n");
979
+ const out = [];
980
+ let inside = false;
981
+ for (const line of lines) {
982
+ if (line.trim() === MARKER_START) {
983
+ inside = true;
984
+ continue;
985
+ }
986
+ if (line.trim() === MARKER_END) {
987
+ inside = false;
988
+ continue;
989
+ }
990
+ if (!inside) {
991
+ out.push(line);
992
+ }
993
+ }
994
+ return ensureTrailingNewline(out.join("\n").replace(/\n{3,}/g, "\n\n"));
995
+ }
996
+ function resolveMinLines(flags, cwd) {
997
+ const fromFlag = flags.minLines ? Number.parseInt(flags.minLines, 10) : NaN;
998
+ if (Number.isFinite(fromFlag) && fromFlag > 0) {
999
+ return fromFlag;
1000
+ }
1001
+ const configured = Number.parseInt(
1002
+ git(["config", "--get", "diffprism.gate-lines"], cwd) ?? "",
1003
+ 10
1004
+ );
1005
+ if (Number.isFinite(configured) && configured > 0) {
1006
+ return configured;
1007
+ }
1008
+ return DEFAULT_MIN_LINES;
1009
+ }
1010
+ function resolveHookPath(cwd) {
1011
+ const configured = git(["config", "--get", "core.hooksPath"], cwd);
1012
+ if (configured) {
1013
+ return path4.resolve(cwd, configured, "pre-commit");
1014
+ }
1015
+ const hooksDir = git(["rev-parse", "--git-path", "hooks"], cwd) ?? ".git/hooks";
1016
+ return path4.resolve(cwd, hooksDir, "pre-commit");
1017
+ }
1018
+ function git(args, cwd) {
1019
+ try {
1020
+ const out = execFileSync("git", args, {
1021
+ cwd,
1022
+ encoding: "utf8",
1023
+ stdio: ["ignore", "pipe", "ignore"]
1024
+ });
1025
+ const trimmed = out.trim();
1026
+ return trimmed === "" ? null : trimmed;
1027
+ } catch {
1028
+ return null;
1029
+ }
1030
+ }
1031
+ function ensureTrailingNewline(text) {
1032
+ return text.endsWith("\n") ? text : `${text}
1033
+ `;
1034
+ }
1035
+ function message(err) {
1036
+ return err instanceof Error ? err.message : String(err);
1037
+ }
1038
+ function fail(text) {
1039
+ console.error(text);
1040
+ process.exit(1);
1041
+ }
1042
+
863
1043
  // cli/src/index.ts
864
1044
  var program = new Command();
865
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.44.0" : "0.0.0-dev");
1045
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.47.0" : "0.0.0-dev");
866
1046
  program.action(defaultAction);
867
1047
  program.command("demo").description("Open a sample review to see DiffPrism in action").option("--dev", "Use Vite dev server").action(demo);
868
1048
  program.command("review [ref]").description("Open a browser-based diff review (local git ref or GitHub PR ref like owner/repo#123)").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--reasoning <text>", "Agent reasoning about the changes").option("--dev", "Use Vite dev server with HMR instead of static files").option("--post-to-github", "Automatically post review back to GitHub without prompting").action(review);
869
1049
  program.command("review-pr <pr>", { hidden: true }).action((pr, flags) => review(pr, flags));
1050
+ var hookCmd = program.command("hook").description("Git hook integration \u2014 gate commits on a human review");
1051
+ hookCmd.command("pre-commit").description("Run the review gate for the staged changes (called from a git hook)").option("--min-lines <n>", "Staged lines required to trigger a review (default: 120)").option("--dev", "Use Vite dev server").action((flags) => {
1052
+ void preCommitHook(flags);
1053
+ });
1054
+ hookCmd.command("install").description("Add the diffprism gate to this repo's pre-commit hook").action(() => {
1055
+ installHook();
1056
+ });
1057
+ hookCmd.command("uninstall").description("Remove the diffprism gate from this repo's pre-commit hook").action(() => {
1058
+ uninstallHook();
1059
+ });
870
1060
  program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
871
1061
  program.command("setup").description("Configure DiffPrism for Claude Code integration").option("--global", "Configure globally (skill + permissions, no git repo required)").option("--force", "Overwrite existing configuration files").option("--dev", "Use Vite dev server").option("--no-demo", "Skip the demo review after setup").action((flags) => {
872
1062
  setup(flags);