diffprism 0.37.3 → 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 +57 -56
- package/dist/{chunk-OR6PCPZX.js → chunk-6J6PSBL2.js} +6 -0
- package/dist/mcp-server.js +123 -84
- package/package.json +1 -1
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-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
console.log(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
|
@@ -755,10 +756,10 @@ async function serverStop() {
|
|
|
755
756
|
|
|
756
757
|
// cli/src/index.ts
|
|
757
758
|
var program = new Command();
|
|
758
|
-
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.
|
|
759
|
+
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.38.0" : "0.0.0-dev");
|
|
759
760
|
program.command("demo").description("Open a sample review to see DiffPrism in action").option("--dev", "Use Vite dev server").action(demo);
|
|
760
|
-
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);
|
|
761
|
-
program.command("review-pr <pr>"
|
|
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));
|
|
762
763
|
program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
|
|
763
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) => {
|
|
764
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
|
package/dist/mcp-server.js
CHANGED
|
@@ -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-
|
|
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.
|
|
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),
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
"
|
|
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
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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 {
|