diffprism 0.37.2 → 0.38.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
@@ -3,11 +3,12 @@ import {
3
3
  createGitHubClient,
4
4
  fetchPullRequest,
5
5
  fetchPullRequestDiff,
6
+ isPrRef,
6
7
  normalizePr,
7
8
  parsePrRef,
8
9
  resolveGitHubToken,
9
10
  submitGitHubReview
10
- } from "./chunk-OR6PCPZX.js";
11
+ } from "./chunk-6J6PSBL2.js";
11
12
  import {
12
13
  demo
13
14
  } from "./chunk-UYZ3A2PB.js";
@@ -26,6 +27,7 @@ import "./chunk-JSBRDJBE.js";
26
27
  import { Command } from "commander";
27
28
 
28
29
  // cli/src/commands/review.ts
30
+ import readline from "readline";
29
31
  async function review(ref, flags) {
30
32
  let diffRef;
31
33
  if (flags.staged) {
@@ -38,65 +40,63 @@ async function review(ref, flags) {
38
40
  diffRef = "working-copy";
39
41
  }
40
42
  try {
41
- const serverInfo = await ensureServer({ dev: flags.dev });
42
- console.log("Opening review in browser...");
43
- const { result } = await submitReviewToServer(serverInfo, diffRef, {
44
- title: flags.title,
45
- cwd: process.cwd(),
46
- diffRef
47
- });
48
- console.log(JSON.stringify(result, null, 2));
49
- process.exit(0);
43
+ if (isPrRef(diffRef)) {
44
+ await reviewPrFlow(diffRef, flags);
45
+ } else {
46
+ await reviewLocalFlow(diffRef, flags);
47
+ }
50
48
  } catch (err) {
51
49
  const message = err instanceof Error ? err.message : String(err);
52
50
  console.error(`Error: ${message}`);
53
51
  process.exit(1);
54
52
  }
55
53
  }
56
-
57
- // cli/src/commands/review-pr.ts
58
- import readline from "readline";
59
- async function reviewPr(pr, flags) {
60
- try {
61
- const token = resolveGitHubToken();
62
- const { owner, repo, number } = parsePrRef(pr);
63
- console.log(`Fetching PR #${number} from ${owner}/${repo}...`);
64
- const client = createGitHubClient(token);
65
- const [prMetadata, rawDiff] = await Promise.all([
66
- fetchPullRequest(client, owner, repo, number),
67
- fetchPullRequestDiff(client, owner, repo, number)
68
- ]);
69
- if (!rawDiff.trim()) {
70
- console.log("PR has no changes to review.");
71
- return;
72
- }
73
- const { payload, diffSet } = normalizePr(rawDiff, prMetadata, {
74
- title: flags.title,
75
- reasoning: flags.reasoning
76
- });
77
- console.log(
78
- `${diffSet.files.length} files, +${diffSet.files.reduce((s, f) => s + f.additions, 0)} -${diffSet.files.reduce((s, f) => s + f.deletions, 0)}`
79
- );
80
- const serverInfo = await ensureServer({ dev: flags.dev });
81
- const { result } = await submitReviewToServer(serverInfo, `PR #${number}`, {
82
- injectedPayload: payload,
83
- projectPath: `github:${owner}/${repo}`,
84
- diffRef: `PR #${number}`
85
- });
86
- console.log(JSON.stringify(result, null, 2));
87
- if (flags.postToGithub || result.decision !== "dismissed" && await promptPostToGithub()) {
88
- console.log("Posting review to GitHub...");
89
- const posted = await submitGitHubReview(client, owner, repo, number, result);
90
- if (posted) {
91
- console.log(`Review posted: ${prMetadata.url}#pullrequestreview-${posted.reviewId}`);
92
- }
54
+ async function reviewLocalFlow(diffRef, flags) {
55
+ const serverInfo = await ensureServer({ dev: flags.dev });
56
+ console.log("Opening review in browser...");
57
+ const { result } = await submitReviewToServer(serverInfo, diffRef, {
58
+ title: flags.title,
59
+ cwd: process.cwd(),
60
+ diffRef
61
+ });
62
+ console.log(JSON.stringify(result, null, 2));
63
+ process.exit(0);
64
+ }
65
+ async function reviewPrFlow(pr, flags) {
66
+ const token = resolveGitHubToken();
67
+ const { owner, repo, number } = parsePrRef(pr);
68
+ 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
+ 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 (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}`);
93
97
  }
94
- process.exit(0);
95
- } catch (err) {
96
- const message = err instanceof Error ? err.message : String(err);
97
- console.error(`Error: ${message}`);
98
- process.exit(1);
99
98
  }
99
+ process.exit(0);
100
100
  }
101
101
  async function promptPostToGithub() {
102
102
  if (!process.stdin.isTTY) {
@@ -136,9 +136,10 @@ description: Open current code changes in DiffPrism's browser-based review UI fo
136
136
 
137
137
  When the user invokes \`/review\`, call \`mcp__diffprism__open_review\` with:
138
138
 
139
- - \`diff_ref\`: \`"working-copy"\` (or what the user specified, e.g. \`"staged"\`)
139
+ - \`diff_ref\`: \`"working-copy"\` (or what the user specified, e.g. \`"staged"\`, or a GitHub PR ref like \`"owner/repo#123"\`)
140
140
  - \`title\`: Brief summary of the changes
141
141
  - \`reasoning\`: Your reasoning about the implementation decisions
142
+ - \`post_to_github\`: Set to \`true\` to post the review back to GitHub (only for PR refs)
142
143
 
143
144
  The tool blocks until the human submits their review. Handle the result:
144
145
 
@@ -244,12 +245,6 @@ function setupSkill(gitRoot, global, force) {
244
245
  if (existingContent === skillContent) {
245
246
  return { action: "skipped", filePath };
246
247
  }
247
- if (!force) {
248
- console.log(
249
- ` Warning: ${filePath} exists with different content. Use --force to overwrite.`
250
- );
251
- return { action: "skipped", filePath };
252
- }
253
248
  }
254
249
  if (!fs.existsSync(skillDir)) {
255
250
  fs.mkdirSync(skillDir, { recursive: true });
@@ -761,10 +756,10 @@ async function serverStop() {
761
756
 
762
757
  // cli/src/index.ts
763
758
  var program = new Command();
764
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.37.2" : "0.0.0-dev");
759
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.38.0" : "0.0.0-dev");
765
760
  program.command("demo").description("Open a sample review to see DiffPrism in action").option("--dev", "Use Vite dev server").action(demo);
766
- program.command("review [ref]").description("Open a browser-based diff review").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--dev", "Use Vite dev server with HMR instead of static files").action(review);
767
- program.command("review-pr <pr>").description("Review a GitHub pull request in DiffPrism").option("-t, --title <title>", "Override review title").option("--reasoning <text>", "Agent reasoning about the PR").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(reviewPr);
761
+ 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);
762
+ program.command("review-pr <pr>", { hidden: true }).action((pr, flags) => review(pr, flags));
768
763
  program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
769
764
  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) => {
770
765
  setup(flags);
@@ -3719,6 +3719,11 @@ async function fetchPullRequestDiff(client, owner, repo, number) {
3719
3719
  });
3720
3720
  return data;
3721
3721
  }
3722
+ function isPrRef(input) {
3723
+ if (/github\.com\/[^/]+\/[^/]+\/pull\/\d+/.test(input)) return true;
3724
+ if (/^[^/]+\/[^#]+#\d+$/.test(input)) return true;
3725
+ return false;
3726
+ }
3722
3727
  function parsePrRef(input) {
3723
3728
  const urlMatch = input.match(
3724
3729
  /github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/
@@ -3842,6 +3847,7 @@ export {
3842
3847
  createGitHubClient,
3843
3848
  fetchPullRequest,
3844
3849
  fetchPullRequestDiff,
3850
+ isPrRef,
3845
3851
  parsePrRef,
3846
3852
  normalizePr,
3847
3853
  submitGitHubReview
@@ -2,11 +2,12 @@ import {
2
2
  createGitHubClient,
3
3
  fetchPullRequest,
4
4
  fetchPullRequestDiff,
5
+ isPrRef,
5
6
  normalizePr,
6
7
  parsePrRef,
7
8
  resolveGitHubToken,
8
9
  submitGitHubReview
9
- } from "./chunk-OR6PCPZX.js";
10
+ } from "./chunk-6J6PSBL2.js";
10
11
  import {
11
12
  ensureServer,
12
13
  isServerAlive,
@@ -26,21 +27,109 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
26
27
  import { z } from "zod";
27
28
  var lastGlobalSessionId = null;
28
29
  var lastGlobalServerInfo = null;
30
+ async function handleLocalReview(diffRef, options) {
31
+ const serverInfo = await ensureServer({ silent: true });
32
+ const { result, sessionId } = await submitReviewToServer(
33
+ serverInfo,
34
+ diffRef,
35
+ {
36
+ title: options.title,
37
+ description: options.description,
38
+ reasoning: options.reasoning,
39
+ cwd: process.cwd(),
40
+ annotations: options.annotations,
41
+ diffRef
42
+ }
43
+ );
44
+ return {
45
+ mcpResult: {
46
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
47
+ },
48
+ sessionId,
49
+ serverInfo
50
+ };
51
+ }
52
+ async function handlePrReview(pr, options) {
53
+ const token = resolveGitHubToken();
54
+ const { owner, repo, number } = parsePrRef(pr);
55
+ const client = createGitHubClient(token);
56
+ const [prMetadata, rawDiff] = await Promise.all([
57
+ fetchPullRequest(client, owner, repo, number),
58
+ fetchPullRequestDiff(client, owner, repo, number)
59
+ ]);
60
+ if (!rawDiff.trim()) {
61
+ return {
62
+ mcpResult: {
63
+ content: [{
64
+ type: "text",
65
+ text: JSON.stringify({
66
+ decision: "approved",
67
+ comments: [],
68
+ summary: "PR has no changes to review."
69
+ }, null, 2)
70
+ }]
71
+ },
72
+ sessionId: "",
73
+ serverInfo: { httpPort: 0, wsPort: 0, pid: 0, startedAt: 0 }
74
+ };
75
+ }
76
+ const { payload } = normalizePr(rawDiff, prMetadata, {
77
+ title: options.title,
78
+ reasoning: options.reasoning
79
+ });
80
+ const serverInfo = await ensureServer({ silent: true });
81
+ const { result, sessionId } = await submitReviewToServer(
82
+ serverInfo,
83
+ `PR #${number}`,
84
+ {
85
+ injectedPayload: payload,
86
+ projectPath: `github:${owner}/${repo}`,
87
+ diffRef: `PR #${number}`
88
+ }
89
+ );
90
+ if ((options.post_to_github || result.postToGithub) && result.decision !== "dismissed") {
91
+ const posted = await submitGitHubReview(client, owner, repo, number, result);
92
+ if (posted) {
93
+ return {
94
+ mcpResult: {
95
+ content: [{
96
+ type: "text",
97
+ text: JSON.stringify({
98
+ ...result,
99
+ githubReviewId: posted.reviewId,
100
+ githubReviewUrl: `${prMetadata.url}#pullrequestreview-${posted.reviewId}`
101
+ }, null, 2)
102
+ }]
103
+ },
104
+ sessionId,
105
+ serverInfo
106
+ };
107
+ }
108
+ }
109
+ return {
110
+ mcpResult: {
111
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
112
+ },
113
+ sessionId,
114
+ serverInfo
115
+ };
116
+ }
29
117
  async function startMcpServer() {
30
118
  const server = new McpServer({
31
119
  name: "diffprism",
32
- version: true ? "0.37.2" : "0.0.0-dev"
120
+ version: true ? "0.38.0" : "0.0.0-dev"
33
121
  });
34
122
  server.tool(
35
123
  "open_review",
36
- "Open a browser-based code review for local git changes. Blocks until the engineer submits their review decision. The result may include a `postReviewAction` field ('commit' or 'commit_and_pr') if the reviewer requested a post-review action.",
124
+ "Open a browser-based code review for local git changes or a GitHub pull request. Blocks until the engineer submits their review decision. The result may include a `postReviewAction` field ('commit' or 'commit_and_pr') if the reviewer requested a post-review action.",
37
125
  {
38
126
  diff_ref: z.string().describe(
39
- 'Git diff reference: "staged", "unstaged", "working-copy" (staged+unstaged grouped), or a ref range like "HEAD~3..HEAD"'
127
+ 'Git diff reference: "staged", "unstaged", "working-copy" (staged+unstaged grouped), a ref range like "HEAD~3..HEAD", or a GitHub PR ref like "owner/repo#123" or a GitHub PR URL'
40
128
  ),
41
129
  title: z.string().optional().describe("Title for the review"),
42
130
  description: z.string().optional().describe("Description of the changes"),
43
131
  reasoning: z.string().optional().describe("Agent reasoning about why these changes were made"),
132
+ post_to_github: z.boolean().optional().describe("Post the review back to GitHub after submission (only for PR refs, default: false)"),
44
133
  annotations: z.array(
45
134
  z.object({
46
135
  file: z.string().describe("File path within the diff to annotate"),
@@ -62,31 +151,30 @@ async function startMcpServer() {
62
151
  })
63
152
  ).optional().describe("Initial annotations to attach to the review")
64
153
  },
65
- async ({ diff_ref, title, description, reasoning, annotations }) => {
154
+ async ({ diff_ref, title, description, reasoning, post_to_github, annotations }) => {
66
155
  try {
67
- const serverInfo = await ensureServer({ silent: true });
68
- const { result, sessionId } = await submitReviewToServer(
69
- serverInfo,
70
- diff_ref,
71
- {
156
+ let mcpResult;
157
+ let sessionId;
158
+ let serverInfo;
159
+ if (isPrRef(diff_ref)) {
160
+ ({ mcpResult, sessionId, serverInfo } = await handlePrReview(diff_ref, {
161
+ title,
162
+ reasoning,
163
+ post_to_github
164
+ }));
165
+ } else {
166
+ ({ mcpResult, sessionId, serverInfo } = await handleLocalReview(diff_ref, {
72
167
  title,
73
168
  description,
74
169
  reasoning,
75
- cwd: process.cwd(),
76
- annotations,
77
- diffRef: diff_ref
78
- }
79
- );
80
- lastGlobalSessionId = sessionId;
81
- lastGlobalServerInfo = serverInfo;
82
- return {
83
- content: [
84
- {
85
- type: "text",
86
- text: JSON.stringify(result, null, 2)
87
- }
88
- ]
89
- };
170
+ annotations
171
+ }));
172
+ }
173
+ if (sessionId) {
174
+ lastGlobalSessionId = sessionId;
175
+ lastGlobalServerInfo = serverInfo;
176
+ }
177
+ return mcpResult;
90
178
  } catch (err) {
91
179
  const message = err instanceof Error ? err.message : String(err);
92
180
  return {
@@ -619,7 +707,7 @@ async function startMcpServer() {
619
707
  );
620
708
  server.tool(
621
709
  "review_pr",
622
- "Open a browser-based code review for a GitHub pull request. Fetches the PR diff, runs DiffPrism analysis, and opens the review UI. Blocks until the engineer submits their review decision. Optionally posts the review back to GitHub. The result may include a `postReviewAction` field ('commit' or 'commit_and_pr') if the reviewer requested a post-review action.",
710
+ "Alias for open_review with a GitHub PR ref. Prefer using open_review with a PR ref in diff_ref instead.",
623
711
  {
624
712
  pr: z.string().describe(
625
713
  'GitHub PR reference: "owner/repo#123" or "https://github.com/owner/repo/pull/123"'
@@ -630,65 +718,16 @@ async function startMcpServer() {
630
718
  },
631
719
  async ({ pr, title, reasoning, post_to_github }) => {
632
720
  try {
633
- const token = resolveGitHubToken();
634
- const { owner, repo, number } = parsePrRef(pr);
635
- const client = createGitHubClient(token);
636
- const [prMetadata, rawDiff] = await Promise.all([
637
- fetchPullRequest(client, owner, repo, number),
638
- fetchPullRequestDiff(client, owner, repo, number)
639
- ]);
640
- if (!rawDiff.trim()) {
641
- return {
642
- content: [
643
- {
644
- type: "text",
645
- text: JSON.stringify({
646
- decision: "approved",
647
- comments: [],
648
- summary: "PR has no changes to review."
649
- }, null, 2)
650
- }
651
- ]
652
- };
653
- }
654
- const { payload } = normalizePr(rawDiff, prMetadata, { title, reasoning });
655
- const serverInfo = await ensureServer({ silent: true });
656
- const { result, sessionId } = await submitReviewToServer(
657
- serverInfo,
658
- `PR #${number}`,
659
- {
660
- injectedPayload: payload,
661
- projectPath: `github:${owner}/${repo}`,
662
- diffRef: `PR #${number}`
663
- }
664
- );
665
- lastGlobalSessionId = sessionId;
666
- lastGlobalServerInfo = serverInfo;
667
- if ((post_to_github || result.postToGithub) && result.decision !== "dismissed") {
668
- const posted = await submitGitHubReview(client, owner, repo, number, result);
669
- if (posted) {
670
- return {
671
- content: [
672
- {
673
- type: "text",
674
- text: JSON.stringify({
675
- ...result,
676
- githubReviewId: posted.reviewId,
677
- githubReviewUrl: `${prMetadata.url}#pullrequestreview-${posted.reviewId}`
678
- }, null, 2)
679
- }
680
- ]
681
- };
682
- }
721
+ const { mcpResult, sessionId, serverInfo } = await handlePrReview(pr, {
722
+ title,
723
+ reasoning,
724
+ post_to_github
725
+ });
726
+ if (sessionId) {
727
+ lastGlobalSessionId = sessionId;
728
+ lastGlobalServerInfo = serverInfo;
683
729
  }
684
- return {
685
- content: [
686
- {
687
- type: "text",
688
- text: JSON.stringify(result, null, 2)
689
- }
690
- ]
691
- };
730
+ return mcpResult;
692
731
  } catch (err) {
693
732
  const message = err instanceof Error ? err.message : String(err);
694
733
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.37.2",
3
+ "version": "0.38.0",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {