diffprism 0.49.0 → 1.0.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 +60 -20
- package/dist/bin.js +457 -358
- package/dist/{chunk-QGWYCEJN.js → chunk-3GMPE2ZR.js} +13 -0
- package/dist/{chunk-7UQM4WBZ.js → chunk-CJ6LV2XV.js} +2 -2
- package/dist/{chunk-EPUCA2N5.js → chunk-CU2BAL6Q.js} +704 -287
- package/dist/{chunk-24B33UN6.js → chunk-EPU4F7WT.js} +6 -56
- package/dist/{demo-VPKUCZT3.js → demo-X5LWQDVU.js} +3 -3
- package/dist/mcp-server.js +432 -963
- package/dist/{src-KF5HRJPX.js → src-DNNNQ2RD.js} +2 -2
- package/dist/{src-AMCPIYDZ.js → src-OZQ32NDN.js} +3 -1
- package/package.json +1 -1
- package/ui-dist/assets/index-BulxhOr3.css +1 -0
- package/ui-dist/assets/index-Zw97bHGn.js +345 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-DpPy4Hg4.css +0 -1
- package/ui-dist/assets/index-Dt04Ez08.js +0 -335
package/dist/mcp-server.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
|
-
isPrRef
|
|
3
|
-
|
|
4
|
-
} from "./chunk-24B33UN6.js";
|
|
2
|
+
isPrRef
|
|
3
|
+
} from "./chunk-EPU4F7WT.js";
|
|
5
4
|
import {
|
|
5
|
+
DEFAULT_DIFF_REF,
|
|
6
|
+
DIFF_REF_DESCRIPTION,
|
|
7
|
+
REPORT_HINT,
|
|
8
|
+
ReviewTimeoutError,
|
|
9
|
+
ReviewerAskedError,
|
|
10
|
+
awaitingAgent,
|
|
11
|
+
currentVersion,
|
|
6
12
|
ensureServer,
|
|
7
13
|
isServerAlive,
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
recordError,
|
|
15
|
+
submitReviewToServer,
|
|
16
|
+
waitForDecision
|
|
17
|
+
} from "./chunk-CU2BAL6Q.js";
|
|
10
18
|
import {
|
|
11
19
|
getDiff
|
|
12
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-3GMPE2ZR.js";
|
|
13
21
|
import {
|
|
14
22
|
analyze
|
|
15
23
|
} from "./chunk-DHCVZGHE.js";
|
|
@@ -19,385 +27,249 @@ import "./chunk-JSBRDJBE.js";
|
|
|
19
27
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20
28
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
29
|
import { z } from "zod";
|
|
22
|
-
var
|
|
23
|
-
var
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (lastGlobalSessionId) return lastGlobalSessionId;
|
|
27
|
-
try {
|
|
28
|
-
const response = await fetch(
|
|
29
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews`
|
|
30
|
-
);
|
|
31
|
-
if (response.ok) {
|
|
32
|
-
const data = await response.json();
|
|
33
|
-
if (data.sessions.length > 0) {
|
|
34
|
-
const sorted = [...data.sessions].sort((a, b) => b.createdAt - a.createdAt);
|
|
35
|
-
return sorted[0].id;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
30
|
+
var DEFAULT_WAIT_MS = 6e5;
|
|
31
|
+
var NO_SERVER = "No DiffPrism server is running. A review has to be open before this tool can act on it \u2014 open one with open_review, `diffprism review`, or the DiffPrism dashboard.";
|
|
32
|
+
function jsonResult(value) {
|
|
33
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
41
34
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
function toolError(text) {
|
|
36
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
37
|
+
}
|
|
38
|
+
function errorMessage(err) {
|
|
39
|
+
return err instanceof Error ? err.message : String(err);
|
|
40
|
+
}
|
|
41
|
+
var targetParams = {
|
|
42
|
+
session_id: z.string().optional().describe("The review session to act on. Takes precedence over repo_path."),
|
|
43
|
+
repo_path: z.string().optional().describe(
|
|
44
|
+
"Any directory inside the repository whose review to act on. Defaults to the directory this agent is running in \u2014 pass it when working on a repo other than your own."
|
|
45
|
+
)
|
|
46
|
+
};
|
|
47
|
+
async function resolveTarget(serverInfo, params) {
|
|
48
|
+
if (params.session_id) {
|
|
49
|
+
return { sessionId: params.session_id };
|
|
50
|
+
}
|
|
51
|
+
const lookupPath = params.repo_path ?? process.cwd();
|
|
52
|
+
const response = await fetch(
|
|
53
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/resolve?path=${encodeURIComponent(lookupPath)}`
|
|
56
54
|
);
|
|
57
|
-
if (
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
return { error: `Could not look up a review for ${lookupPath}: server returned ${response.status}.` };
|
|
57
|
+
}
|
|
58
|
+
const { repoRoot, sessions } = await response.json();
|
|
59
|
+
if (sessions.length === 1) {
|
|
60
|
+
return { sessionId: sessions[0].id };
|
|
61
|
+
}
|
|
62
|
+
const from = params.repo_path ? "repo_path" : "the current directory";
|
|
63
|
+
if (sessions.length === 0) {
|
|
58
64
|
return {
|
|
59
|
-
|
|
60
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
61
|
-
},
|
|
62
|
-
sessionId,
|
|
63
|
-
serverInfo
|
|
65
|
+
error: `No review is open for ${repoRoot} (resolved from ${from}). Open one with open_review, \`diffprism review\`, or the DiffPrism dashboard \u2014 or pass session_id.`
|
|
64
66
|
};
|
|
65
67
|
}
|
|
68
|
+
const candidates = sessions.map((s) => ` ${s.id} ${s.title ?? "(untitled)"} (${[s.diffRef, s.status].filter(Boolean).join(", ")})`).join("\n");
|
|
66
69
|
return {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
type: "text",
|
|
70
|
-
text: JSON.stringify({
|
|
71
|
-
status: "session_created",
|
|
72
|
-
sessionId,
|
|
73
|
-
message: "Review session opened in DiffPrism dashboard. Use get_review_result to check for a decision."
|
|
74
|
-
}, null, 2)
|
|
75
|
-
}]
|
|
76
|
-
},
|
|
77
|
-
sessionId,
|
|
78
|
-
serverInfo
|
|
70
|
+
error: `${sessions.length} reviews are open for ${repoRoot}. Pass session_id to choose one:
|
|
71
|
+
${candidates}`
|
|
79
72
|
};
|
|
80
73
|
}
|
|
81
|
-
async function
|
|
82
|
-
const { owner, repo, number } = parsePrRef(pr);
|
|
83
|
-
const serverInfo = await ensureServer({ silent: true });
|
|
74
|
+
async function readThreads(serverInfo, sessionId) {
|
|
84
75
|
const response = await fetch(
|
|
85
|
-
`http://localhost:${serverInfo.httpPort}/api/
|
|
86
|
-
{
|
|
87
|
-
method: "POST",
|
|
88
|
-
headers: { "Content-Type": "application/json" },
|
|
89
|
-
body: JSON.stringify({ prUrl: pr })
|
|
90
|
-
}
|
|
76
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations`
|
|
91
77
|
);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
mcpResult: {
|
|
96
|
-
content: [{ type: "text", text: `Error: ${data.error ?? "Failed to open PR"}` }],
|
|
97
|
-
isError: true
|
|
98
|
-
},
|
|
99
|
-
sessionId: "",
|
|
100
|
-
serverInfo
|
|
101
|
-
};
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
return null;
|
|
102
80
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
81
|
+
const { annotations } = await response.json();
|
|
82
|
+
return annotations.map((a) => ({ ...a, awaitingReply: awaitingAgent(a) }));
|
|
83
|
+
}
|
|
84
|
+
function reviewerAskedResult(err, waitWith) {
|
|
85
|
+
return jsonResult({
|
|
86
|
+
status: "reviewer_asked",
|
|
87
|
+
sessionId: err.sessionId,
|
|
88
|
+
threads: err.threads.map((t) => ({ ...t, awaitingReply: true })),
|
|
89
|
+
message: `The reviewer asked you something before deciding. Answer each thread with reply (session_id: ${err.sessionId}, annotation_id from threads) \u2014 change the code too if that's what they asked for. Then ${waitWith}. The review stays open; the decision still comes.`
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function withSession(params, run) {
|
|
93
|
+
try {
|
|
94
|
+
const serverInfo = await isServerAlive();
|
|
95
|
+
if (!serverInfo) {
|
|
96
|
+
return toolError(NO_SERVER);
|
|
97
|
+
}
|
|
98
|
+
const target = await resolveTarget(serverInfo, params);
|
|
99
|
+
if ("error" in target) {
|
|
100
|
+
return toolError(target.error);
|
|
123
101
|
}
|
|
102
|
+
return await run({ serverInfo, sessionId: target.sessionId });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
recordError("serve (MCP)", err);
|
|
105
|
+
return toolError(`Error: ${errorMessage(err)}
|
|
106
|
+
${REPORT_HINT}`);
|
|
124
107
|
}
|
|
125
|
-
return {
|
|
126
|
-
mcpResult: {
|
|
127
|
-
content: [{
|
|
128
|
-
type: "text",
|
|
129
|
-
text: JSON.stringify({
|
|
130
|
-
status: "session_created",
|
|
131
|
-
sessionId,
|
|
132
|
-
pr: `${owner}/${repo}#${number}`,
|
|
133
|
-
fileCount: data.fileCount,
|
|
134
|
-
localRepoConnected: !!data.localRepoPath,
|
|
135
|
-
localRepoPath: data.localRepoPath,
|
|
136
|
-
message: "PR review session opened in DiffPrism. Use get_pr_context, get_file_diff, get_file_context to explore the changes. Use add_review_comment to post findings."
|
|
137
|
-
}, null, 2)
|
|
138
|
-
}]
|
|
139
|
-
},
|
|
140
|
-
sessionId,
|
|
141
|
-
serverInfo
|
|
142
|
-
};
|
|
143
108
|
}
|
|
109
|
+
var diffRefParam = z.string().optional().describe(DIFF_REF_DESCRIPTION);
|
|
110
|
+
var annotationSchema = z.object({
|
|
111
|
+
file: z.string().describe("File path within the diff"),
|
|
112
|
+
line: z.number().optional().describe("Line number to attach to (defaults to 1, for a note about the file as a whole)"),
|
|
113
|
+
body: z.string().describe("The finding, suggestion, or question"),
|
|
114
|
+
type: z.enum(["finding", "suggestion", "question", "warning"]).describe("Use 'warning' for anything the reviewer must look at \u2014 warnings flag the session for attention"),
|
|
115
|
+
confidence: z.number().min(0).max(1).optional().describe("Confidence in the finding (0-1, defaults to 1)"),
|
|
116
|
+
category: z.enum([
|
|
117
|
+
"security",
|
|
118
|
+
"performance",
|
|
119
|
+
"convention",
|
|
120
|
+
"correctness",
|
|
121
|
+
"complexity",
|
|
122
|
+
"test-coverage",
|
|
123
|
+
"documentation",
|
|
124
|
+
"other"
|
|
125
|
+
]).optional().describe("Category of the finding (defaults to 'other')")
|
|
126
|
+
});
|
|
144
127
|
async function startMcpServer() {
|
|
145
128
|
const server = new McpServer({
|
|
146
129
|
name: "diffprism",
|
|
147
|
-
version:
|
|
130
|
+
version: currentVersion()
|
|
148
131
|
});
|
|
149
132
|
server.tool(
|
|
150
133
|
"open_review",
|
|
151
|
-
|
|
134
|
+
'Open a review of local git changes in the DiffPrism dashboard and wait for the reviewer\'s decision. Blocks until they approve, request changes, or dismiss, then returns their ReviewResult (decision, inline comments, summary). If the reviewer asks you something first, returns status "reviewer_asked" with the threads: answer them with reply, then wait with get_review_result. Reviews are one per repo: opening again for the same repo updates the review already open instead of starting another, and keeps its annotations. Pass wait: false to get the session id back immediately instead. Pull requests are not opened here \u2014 open them with `diffprism review <PR URL>` or the dashboard, then use the PR tools.',
|
|
152
135
|
{
|
|
153
|
-
diff_ref:
|
|
154
|
-
'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'
|
|
155
|
-
),
|
|
136
|
+
diff_ref: diffRefParam,
|
|
156
137
|
title: z.string().optional().describe("Title for the review"),
|
|
157
138
|
description: z.string().optional().describe("Description of the changes"),
|
|
158
|
-
reasoning: z.string().optional().describe(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
annotations: z.array(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
category: z.enum([
|
|
169
|
-
"security",
|
|
170
|
-
"performance",
|
|
171
|
-
"convention",
|
|
172
|
-
"correctness",
|
|
173
|
-
"complexity",
|
|
174
|
-
"test-coverage",
|
|
175
|
-
"documentation",
|
|
176
|
-
"other"
|
|
177
|
-
]).optional().describe("Category of the finding (defaults to 'other')"),
|
|
178
|
-
source_agent: z.string().optional().describe("Agent identifier (e.g., 'security-reviewer')")
|
|
179
|
-
})
|
|
180
|
-
).optional().describe("Initial annotations to attach to the review")
|
|
139
|
+
reasoning: z.string().optional().describe(
|
|
140
|
+
"Summarize what you were trying to accomplish in plain English. Shown as the session subtitle in the dashboard \u2014 the main way a reviewer tells sessions apart. Always populate this."
|
|
141
|
+
),
|
|
142
|
+
annotations: z.array(annotationSchema).optional().describe("Findings to attach to the review when it opens"),
|
|
143
|
+
wait: z.boolean().optional().describe(
|
|
144
|
+
"Wait for the reviewer's decision (default true). With false, returns the session id at once; check for a decision later with get_review_result."
|
|
145
|
+
),
|
|
146
|
+
timeout_ms: z.number().optional().describe(
|
|
147
|
+
`How long to wait for a decision (default ${DEFAULT_WAIT_MS}ms). If it runs out the review stays open, and the session id comes back so you can check again with get_review_result.`
|
|
148
|
+
)
|
|
181
149
|
},
|
|
182
|
-
async ({ diff_ref, title, description, reasoning,
|
|
150
|
+
async ({ diff_ref = DEFAULT_DIFF_REF, title, description, reasoning, annotations, wait, timeout_ms }) => {
|
|
151
|
+
if (isPrRef(diff_ref)) {
|
|
152
|
+
return toolError(
|
|
153
|
+
"open_review does not open pull requests. Open a PR review with `diffprism review <PR URL>` or the DiffPrism dashboard, then use get_pr_context, get_file_diff, get_file_context and annotate on that session."
|
|
154
|
+
);
|
|
155
|
+
}
|
|
183
156
|
try {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
({ mcpResult, sessionId, serverInfo } = await handlePrReview(diff_ref, {
|
|
189
|
-
title,
|
|
190
|
-
reasoning,
|
|
191
|
-
post_to_github,
|
|
192
|
-
timeoutMs: timeout_ms
|
|
193
|
-
}));
|
|
194
|
-
} else {
|
|
195
|
-
({ mcpResult, sessionId, serverInfo } = await handleLocalReview(diff_ref, {
|
|
157
|
+
const serverInfo = await ensureServer({ silent: true });
|
|
158
|
+
const shouldWait = wait ?? true;
|
|
159
|
+
try {
|
|
160
|
+
const { result, sessionId } = await submitReviewToServer(serverInfo, diff_ref, {
|
|
196
161
|
title,
|
|
197
162
|
description,
|
|
198
163
|
reasoning,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
164
|
+
cwd: process.cwd(),
|
|
165
|
+
annotations: annotations?.map((a) => ({ ...a, line: a.line ?? 1 })),
|
|
166
|
+
diffRef: diff_ref,
|
|
167
|
+
timeoutMs: shouldWait ? timeout_ms ?? DEFAULT_WAIT_MS : 0
|
|
168
|
+
});
|
|
169
|
+
if (result) {
|
|
170
|
+
return jsonResult(result);
|
|
171
|
+
}
|
|
172
|
+
return jsonResult({
|
|
173
|
+
status: "open",
|
|
174
|
+
sessionId,
|
|
175
|
+
message: "Review is open in the DiffPrism dashboard. Check for a decision with get_review_result."
|
|
176
|
+
});
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err instanceof ReviewerAskedError) {
|
|
179
|
+
return reviewerAskedResult(err, `wait for the decision with get_review_result (session_id: ${err.sessionId}, wait: true)`);
|
|
180
|
+
}
|
|
181
|
+
if (err instanceof ReviewTimeoutError) {
|
|
182
|
+
return jsonResult({
|
|
183
|
+
status: "timed_out",
|
|
184
|
+
sessionId: err.sessionId,
|
|
185
|
+
message: `No decision after ${Math.round(err.waitedMs / 1e3)}s. The reviewer may still be reading \u2014 the review is open in their browser. Wait with get_review_result (session_id: ${err.sessionId}, wait: true). Don't ask the user about it in the meantime: their decision is the answer. Calling open_review again with an unchanged diff is also safe \u2014 it returns a decision already given.`
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
throw err;
|
|
206
189
|
}
|
|
207
|
-
return mcpResult;
|
|
208
190
|
} catch (err) {
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
|
|
212
|
-
{
|
|
213
|
-
type: "text",
|
|
214
|
-
text: `Error: ${message}`
|
|
215
|
-
}
|
|
216
|
-
],
|
|
217
|
-
isError: true
|
|
218
|
-
};
|
|
191
|
+
recordError("serve (MCP) open_review", err);
|
|
192
|
+
return toolError(`Error: ${errorMessage(err)}
|
|
193
|
+
${REPORT_HINT}`);
|
|
219
194
|
}
|
|
220
195
|
}
|
|
221
196
|
);
|
|
222
197
|
server.tool(
|
|
223
|
-
"
|
|
224
|
-
|
|
198
|
+
"get_review_result",
|
|
199
|
+
'Check the decision on a review that is already open \u2014 after open_review with wait: false, or after open_review timed out. Returns the ReviewResult once the reviewer has decided. Set wait: true to block until they do; the wait also ends with status "reviewer_asked" if they ask you something \u2014 reply, then wait again.',
|
|
225
200
|
{
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
201
|
+
...targetParams,
|
|
202
|
+
wait: z.boolean().optional().describe("Block until a decision arrives (up to timeout)"),
|
|
203
|
+
timeout: z.number().optional().describe("Max wait in seconds when wait is true (default 300, max 600)")
|
|
229
204
|
},
|
|
230
|
-
async ({
|
|
231
|
-
|
|
232
|
-
const payload = {};
|
|
233
|
-
if (reasoning !== void 0) payload.reasoning = reasoning;
|
|
234
|
-
if (title !== void 0) payload.title = title;
|
|
235
|
-
if (description !== void 0) payload.description = description;
|
|
236
|
-
const serverInfo = lastGlobalServerInfo ?? await isServerAlive();
|
|
237
|
-
if (!serverInfo || !lastGlobalSessionId) {
|
|
238
|
-
return {
|
|
239
|
-
content: [
|
|
240
|
-
{
|
|
241
|
-
type: "text",
|
|
242
|
-
text: "No DiffPrism session is running. Use `open_review` to start a review."
|
|
243
|
-
}
|
|
244
|
-
]
|
|
245
|
-
};
|
|
246
|
-
}
|
|
205
|
+
async ({ session_id, repo_path, wait, timeout }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
206
|
+
if (!wait) {
|
|
247
207
|
const response = await fetch(
|
|
248
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${
|
|
249
|
-
{
|
|
250
|
-
method: "POST",
|
|
251
|
-
headers: { "Content-Type": "application/json" },
|
|
252
|
-
body: JSON.stringify(payload)
|
|
253
|
-
}
|
|
208
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/result`
|
|
254
209
|
);
|
|
255
210
|
if (!response.ok) {
|
|
256
|
-
throw new Error(`
|
|
211
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
257
212
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
]
|
|
265
|
-
};
|
|
213
|
+
const { result } = await response.json();
|
|
214
|
+
return result ? jsonResult(result) : jsonResult({ status: "pending", sessionId, message: "No decision yet." });
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
return jsonResult(await waitForDecision(serverInfo, sessionId, Math.min(timeout ?? 300, 600) * 1e3));
|
|
266
218
|
} catch (err) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
219
|
+
if (err instanceof ReviewerAskedError) {
|
|
220
|
+
return reviewerAskedResult(err, "call get_review_result with wait: true again");
|
|
221
|
+
}
|
|
222
|
+
if (err instanceof ReviewTimeoutError) {
|
|
223
|
+
return jsonResult({
|
|
224
|
+
status: "pending",
|
|
225
|
+
sessionId,
|
|
226
|
+
message: "Still no decision; the review remains open in the reviewer's browser. Call get_review_result with wait: true again rather than asking the user \u2014 their decision is the answer."
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
throw err;
|
|
277
230
|
}
|
|
278
|
-
}
|
|
231
|
+
})
|
|
279
232
|
);
|
|
280
233
|
server.tool(
|
|
281
|
-
"
|
|
282
|
-
"
|
|
234
|
+
"update_review_context",
|
|
235
|
+
"Push reasoning, title, or description to an open review without opening a new one. Returns immediately.",
|
|
283
236
|
{
|
|
284
|
-
|
|
285
|
-
|
|
237
|
+
...targetParams,
|
|
238
|
+
reasoning: z.string().optional().describe("Agent reasoning about the current changes"),
|
|
239
|
+
title: z.string().optional().describe("Updated title for the review"),
|
|
240
|
+
description: z.string().optional().describe("Updated description of the changes")
|
|
286
241
|
},
|
|
287
|
-
async ({
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
]
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
if (wait) {
|
|
303
|
-
const start = Date.now();
|
|
304
|
-
while (Date.now() - start < maxWaitMs) {
|
|
305
|
-
const response2 = await fetch(
|
|
306
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${lastGlobalSessionId}/result`
|
|
307
|
-
);
|
|
308
|
-
if (response2.ok) {
|
|
309
|
-
const data = await response2.json();
|
|
310
|
-
if (data.result) {
|
|
311
|
-
return {
|
|
312
|
-
content: [
|
|
313
|
-
{
|
|
314
|
-
type: "text",
|
|
315
|
-
text: JSON.stringify(data.result, null, 2)
|
|
316
|
-
}
|
|
317
|
-
]
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
322
|
-
}
|
|
323
|
-
return {
|
|
324
|
-
content: [
|
|
325
|
-
{
|
|
326
|
-
type: "text",
|
|
327
|
-
text: "No review result received within timeout."
|
|
328
|
-
}
|
|
329
|
-
]
|
|
330
|
-
};
|
|
242
|
+
async ({ session_id, repo_path, reasoning, title, description }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
243
|
+
const payload = {};
|
|
244
|
+
if (reasoning !== void 0) payload.reasoning = reasoning;
|
|
245
|
+
if (title !== void 0) payload.title = title;
|
|
246
|
+
if (description !== void 0) payload.description = description;
|
|
247
|
+
const response = await fetch(
|
|
248
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/context`,
|
|
249
|
+
{
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { "Content-Type": "application/json" },
|
|
252
|
+
body: JSON.stringify(payload)
|
|
331
253
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
);
|
|
335
|
-
if (response.ok) {
|
|
336
|
-
const data = await response.json();
|
|
337
|
-
if (data.result) {
|
|
338
|
-
return {
|
|
339
|
-
content: [
|
|
340
|
-
{
|
|
341
|
-
type: "text",
|
|
342
|
-
text: JSON.stringify(data.result, null, 2)
|
|
343
|
-
}
|
|
344
|
-
]
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return {
|
|
349
|
-
content: [
|
|
350
|
-
{
|
|
351
|
-
type: "text",
|
|
352
|
-
text: "No pending review result."
|
|
353
|
-
}
|
|
354
|
-
]
|
|
355
|
-
};
|
|
356
|
-
} catch (err) {
|
|
357
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
358
|
-
return {
|
|
359
|
-
content: [
|
|
360
|
-
{
|
|
361
|
-
type: "text",
|
|
362
|
-
text: `Error reading review result: ${message}`
|
|
363
|
-
}
|
|
364
|
-
],
|
|
365
|
-
isError: true
|
|
366
|
-
};
|
|
254
|
+
);
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
return toolError(`Error updating review context: server returned ${response.status}`);
|
|
367
257
|
}
|
|
368
|
-
|
|
258
|
+
return jsonResult({ sessionId, updated: Object.keys(payload) });
|
|
259
|
+
})
|
|
369
260
|
);
|
|
370
261
|
server.tool(
|
|
371
262
|
"get_diff",
|
|
372
263
|
"Get a structured diff (DiffSet) for local git changes. Returns file-level and hunk-level change data as JSON without opening a browser. Use this to inspect what changed before deciding whether to open a full review.",
|
|
373
264
|
{
|
|
374
|
-
diff_ref:
|
|
375
|
-
'Git diff reference: "staged", "unstaged", "working-copy" (staged+unstaged grouped), or a ref range like "HEAD~3..HEAD"'
|
|
376
|
-
)
|
|
265
|
+
diff_ref: diffRefParam
|
|
377
266
|
},
|
|
378
|
-
async ({ diff_ref }) => {
|
|
267
|
+
async ({ diff_ref = DEFAULT_DIFF_REF }) => {
|
|
379
268
|
try {
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
return {
|
|
383
|
-
content: [
|
|
384
|
-
{
|
|
385
|
-
type: "text",
|
|
386
|
-
text: JSON.stringify(diffSet, null, 2)
|
|
387
|
-
}
|
|
388
|
-
]
|
|
389
|
-
};
|
|
269
|
+
const { diffSet } = getDiff(diff_ref, { cwd: process.cwd() });
|
|
270
|
+
return jsonResult(diffSet);
|
|
390
271
|
} catch (err) {
|
|
391
|
-
|
|
392
|
-
return {
|
|
393
|
-
content: [
|
|
394
|
-
{
|
|
395
|
-
type: "text",
|
|
396
|
-
text: `Error: ${message}`
|
|
397
|
-
}
|
|
398
|
-
],
|
|
399
|
-
isError: true
|
|
400
|
-
};
|
|
272
|
+
return toolError(`Error: ${errorMessage(err)}`);
|
|
401
273
|
}
|
|
402
274
|
}
|
|
403
275
|
);
|
|
@@ -405,705 +277,302 @@ async function startMcpServer() {
|
|
|
405
277
|
"analyze_diff",
|
|
406
278
|
"Analyze local git changes and return a ReviewBriefing with summary, file triage (critical/notable/mechanical), impact detection (affected modules, tests, dependencies, breaking changes), complexity scores, test coverage gaps, and pattern flags (security issues, TODOs, console.logs). Same analysis shown in the DiffPrism briefing bar, but returned as JSON without opening a browser.",
|
|
407
279
|
{
|
|
408
|
-
diff_ref:
|
|
409
|
-
'Git diff reference: "staged", "unstaged", "working-copy" (staged+unstaged grouped), or a ref range like "HEAD~3..HEAD"'
|
|
410
|
-
)
|
|
280
|
+
diff_ref: diffRefParam
|
|
411
281
|
},
|
|
412
|
-
async ({ diff_ref }) => {
|
|
282
|
+
async ({ diff_ref = DEFAULT_DIFF_REF }) => {
|
|
413
283
|
try {
|
|
414
|
-
const
|
|
415
|
-
const { diffSet } = getDiff(diff_ref, { cwd });
|
|
284
|
+
const { diffSet } = getDiff(diff_ref, { cwd: process.cwd() });
|
|
416
285
|
if (diffSet.files.length === 0) {
|
|
417
|
-
return {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
},
|
|
431
|
-
verification: { testsPass: null, typeCheck: null, lintClean: null },
|
|
432
|
-
fileStats: []
|
|
433
|
-
}, null, 2)
|
|
434
|
-
}
|
|
435
|
-
]
|
|
436
|
-
};
|
|
286
|
+
return jsonResult({
|
|
287
|
+
summary: "No changes to analyze.",
|
|
288
|
+
triage: { critical: [], notable: [], mechanical: [] },
|
|
289
|
+
impact: {
|
|
290
|
+
affectedModules: [],
|
|
291
|
+
affectedTests: [],
|
|
292
|
+
publicApiChanges: false,
|
|
293
|
+
breakingChanges: [],
|
|
294
|
+
newDependencies: []
|
|
295
|
+
},
|
|
296
|
+
verification: { testsPass: null, typeCheck: null, lintClean: null },
|
|
297
|
+
fileStats: []
|
|
298
|
+
});
|
|
437
299
|
}
|
|
438
|
-
|
|
439
|
-
return {
|
|
440
|
-
content: [
|
|
441
|
-
{
|
|
442
|
-
type: "text",
|
|
443
|
-
text: JSON.stringify(briefing, null, 2)
|
|
444
|
-
}
|
|
445
|
-
]
|
|
446
|
-
};
|
|
300
|
+
return jsonResult(analyze(diffSet));
|
|
447
301
|
} catch (err) {
|
|
448
|
-
|
|
449
|
-
return {
|
|
450
|
-
content: [
|
|
451
|
-
{
|
|
452
|
-
type: "text",
|
|
453
|
-
text: `Error: ${message}`
|
|
454
|
-
}
|
|
455
|
-
],
|
|
456
|
-
isError: true
|
|
457
|
-
};
|
|
302
|
+
return toolError(`Error: ${errorMessage(err)}`);
|
|
458
303
|
}
|
|
459
304
|
}
|
|
460
305
|
);
|
|
461
306
|
server.tool(
|
|
462
|
-
"
|
|
463
|
-
"Post
|
|
307
|
+
"annotate",
|
|
308
|
+
"Post findings to an open review. They appear inline on the diff in the DiffPrism dashboard in real time. Use type 'warning' for anything the reviewer must look at \u2014 warnings flag the session in the sidebar. Accepts one or many findings.",
|
|
464
309
|
{
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
body: z.string().describe("The annotation text \u2014 your finding, suggestion, or question"),
|
|
469
|
-
type: z.enum(["finding", "suggestion", "question", "warning"]).describe("Type of annotation"),
|
|
470
|
-
confidence: z.number().min(0).max(1).optional().describe("Confidence in the finding (0-1, defaults to 1)"),
|
|
471
|
-
category: z.enum([
|
|
472
|
-
"security",
|
|
473
|
-
"performance",
|
|
474
|
-
"convention",
|
|
475
|
-
"correctness",
|
|
476
|
-
"complexity",
|
|
477
|
-
"test-coverage",
|
|
478
|
-
"documentation",
|
|
479
|
-
"other"
|
|
480
|
-
]).optional().describe("Category of the finding (defaults to 'other')"),
|
|
481
|
-
source_agent: z.string().optional().describe("Agent identifier (e.g., 'security-reviewer')")
|
|
310
|
+
...targetParams,
|
|
311
|
+
annotations: z.array(annotationSchema).min(1).describe("One or more findings to post"),
|
|
312
|
+
source_agent: z.string().optional().describe("Who is posting these, e.g. 'security-reviewer'")
|
|
482
313
|
},
|
|
483
|
-
async ({
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
type,
|
|
489
|
-
confidence,
|
|
490
|
-
category,
|
|
491
|
-
source_agent
|
|
492
|
-
}) => {
|
|
493
|
-
try {
|
|
494
|
-
const serverInfo = await isServerAlive();
|
|
495
|
-
if (!serverInfo) {
|
|
496
|
-
return {
|
|
497
|
-
content: [
|
|
498
|
-
{
|
|
499
|
-
type: "text",
|
|
500
|
-
text: "No global server running. Start one with `diffprism server`."
|
|
501
|
-
}
|
|
502
|
-
],
|
|
503
|
-
isError: true
|
|
504
|
-
};
|
|
505
|
-
}
|
|
314
|
+
async ({ session_id, repo_path, annotations, source_agent }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
315
|
+
const annotationIds = [];
|
|
316
|
+
const failed = [];
|
|
317
|
+
for (const annotation of annotations) {
|
|
318
|
+
const line = annotation.line ?? 1;
|
|
506
319
|
const response = await fetch(
|
|
507
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${
|
|
320
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations`,
|
|
508
321
|
{
|
|
509
322
|
method: "POST",
|
|
510
323
|
headers: { "Content-Type": "application/json" },
|
|
511
324
|
body: JSON.stringify({
|
|
512
|
-
file,
|
|
325
|
+
file: annotation.file,
|
|
513
326
|
line,
|
|
514
|
-
body,
|
|
515
|
-
type,
|
|
516
|
-
confidence: confidence ?? 1,
|
|
517
|
-
category: category ?? "other",
|
|
518
|
-
source: {
|
|
519
|
-
agent: source_agent ?? "unknown",
|
|
520
|
-
tool: "add_annotation"
|
|
521
|
-
}
|
|
327
|
+
body: annotation.body,
|
|
328
|
+
type: annotation.type,
|
|
329
|
+
confidence: annotation.confidence ?? 1,
|
|
330
|
+
category: annotation.category ?? "other",
|
|
331
|
+
source: { agent: source_agent ?? "unknown", tool: "annotate" }
|
|
522
332
|
})
|
|
523
333
|
}
|
|
524
334
|
);
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
],
|
|
535
|
-
isError: true
|
|
536
|
-
};
|
|
335
|
+
if (response.ok) {
|
|
336
|
+
annotationIds.push((await response.json()).annotationId);
|
|
337
|
+
} else {
|
|
338
|
+
const data = await response.json().catch(() => ({}));
|
|
339
|
+
failed.push({
|
|
340
|
+
file: annotation.file,
|
|
341
|
+
line,
|
|
342
|
+
error: data.error ?? `Server returned ${response.status}`
|
|
343
|
+
});
|
|
537
344
|
}
|
|
538
|
-
const data = await response.json();
|
|
539
|
-
return {
|
|
540
|
-
content: [
|
|
541
|
-
{
|
|
542
|
-
type: "text",
|
|
543
|
-
text: JSON.stringify(
|
|
544
|
-
{ annotationId: data.annotationId, sessionId: session_id },
|
|
545
|
-
null,
|
|
546
|
-
2
|
|
547
|
-
)
|
|
548
|
-
}
|
|
549
|
-
]
|
|
550
|
-
};
|
|
551
|
-
} catch (err) {
|
|
552
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
553
|
-
return {
|
|
554
|
-
content: [
|
|
555
|
-
{
|
|
556
|
-
type: "text",
|
|
557
|
-
text: `Error: ${message}`
|
|
558
|
-
}
|
|
559
|
-
],
|
|
560
|
-
isError: true
|
|
561
|
-
};
|
|
562
345
|
}
|
|
563
|
-
|
|
346
|
+
const result = { sessionId, annotationIds, ...failed.length > 0 ? { failed } : {} };
|
|
347
|
+
return annotationIds.length === 0 ? { ...jsonResult(result), isError: true } : jsonResult(result);
|
|
348
|
+
})
|
|
564
349
|
);
|
|
565
350
|
server.tool(
|
|
566
351
|
"get_review_state",
|
|
567
|
-
"Get the
|
|
568
|
-
{
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (!sessionId) {
|
|
577
|
-
return {
|
|
578
|
-
content: [
|
|
579
|
-
{
|
|
580
|
-
type: "text",
|
|
581
|
-
text: "No session ID provided and no recent session available."
|
|
582
|
-
}
|
|
583
|
-
],
|
|
584
|
-
isError: true
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
const serverInfo = await isServerAlive();
|
|
588
|
-
if (!serverInfo) {
|
|
589
|
-
return {
|
|
590
|
-
content: [
|
|
591
|
-
{
|
|
592
|
-
type: "text",
|
|
593
|
-
text: "No global server running. Start one with `diffprism server`."
|
|
594
|
-
}
|
|
595
|
-
],
|
|
596
|
-
isError: true
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
const [sessionResponse, annotationsResponse] = await Promise.all([
|
|
600
|
-
fetch(
|
|
601
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}`
|
|
602
|
-
),
|
|
603
|
-
fetch(
|
|
604
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations`
|
|
605
|
-
)
|
|
606
|
-
]);
|
|
607
|
-
if (!sessionResponse.ok) {
|
|
608
|
-
return {
|
|
609
|
-
content: [
|
|
610
|
-
{
|
|
611
|
-
type: "text",
|
|
612
|
-
text: `Session not found: ${sessionId}`
|
|
613
|
-
}
|
|
614
|
-
],
|
|
615
|
-
isError: true
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
const session = await sessionResponse.json();
|
|
619
|
-
const annotations = annotationsResponse.ok ? await annotationsResponse.json() : { annotations: [] };
|
|
620
|
-
return {
|
|
621
|
-
content: [
|
|
622
|
-
{
|
|
623
|
-
type: "text",
|
|
624
|
-
text: JSON.stringify(
|
|
625
|
-
{
|
|
626
|
-
session,
|
|
627
|
-
annotations: annotations.annotations
|
|
628
|
-
},
|
|
629
|
-
null,
|
|
630
|
-
2
|
|
631
|
-
)
|
|
632
|
-
}
|
|
633
|
-
]
|
|
634
|
-
};
|
|
635
|
-
} catch (err) {
|
|
636
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
637
|
-
return {
|
|
638
|
-
content: [
|
|
639
|
-
{
|
|
640
|
-
type: "text",
|
|
641
|
-
text: `Error: ${message}`
|
|
642
|
-
}
|
|
643
|
-
],
|
|
644
|
-
isError: true
|
|
645
|
-
};
|
|
352
|
+
"Get the state of an open review: session summary (status, decision, whether it has new changes or needs attention) and all annotations.",
|
|
353
|
+
{ ...targetParams },
|
|
354
|
+
async ({ session_id, repo_path }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
355
|
+
const [sessionResponse, annotationsResponse] = await Promise.all([
|
|
356
|
+
fetch(`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}`),
|
|
357
|
+
fetch(`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations`)
|
|
358
|
+
]);
|
|
359
|
+
if (!sessionResponse.ok) {
|
|
360
|
+
return toolError(`Session not found: ${sessionId}`);
|
|
646
361
|
}
|
|
647
|
-
|
|
362
|
+
const session = await sessionResponse.json();
|
|
363
|
+
const { annotations } = annotationsResponse.ok ? await annotationsResponse.json() : { annotations: [] };
|
|
364
|
+
return jsonResult({ session, annotations });
|
|
365
|
+
})
|
|
648
366
|
);
|
|
649
367
|
server.tool(
|
|
650
|
-
"
|
|
651
|
-
"
|
|
368
|
+
"get_review_comments",
|
|
369
|
+
"Get every thread on an open review \u2014 agent findings and the reviewer's comments, each with its replies. `awaitingReply` marks threads where the reviewer spoke last and nobody has answered. Read these before adding your own.",
|
|
652
370
|
{
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
),
|
|
656
|
-
files: z.array(
|
|
657
|
-
z.object({
|
|
658
|
-
path: z.string().describe("File path to flag for attention"),
|
|
659
|
-
reason: z.string().describe("Why this file needs human attention"),
|
|
660
|
-
line: z.number().optional().describe("Specific line to highlight (defaults to 1)")
|
|
661
|
-
})
|
|
662
|
-
).describe("Files to flag for human attention"),
|
|
663
|
-
source_agent: z.string().optional().describe("Agent identifier (e.g., 'security-reviewer')")
|
|
371
|
+
...targetParams,
|
|
372
|
+
awaiting_reply: z.boolean().optional().describe("Only return threads waiting for an agent to reply")
|
|
664
373
|
},
|
|
665
|
-
async ({ session_id,
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
return {
|
|
670
|
-
content: [
|
|
671
|
-
{
|
|
672
|
-
type: "text",
|
|
673
|
-
text: "No session ID provided and no recent session available."
|
|
674
|
-
}
|
|
675
|
-
],
|
|
676
|
-
isError: true
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
const serverInfo = await isServerAlive();
|
|
680
|
-
if (!serverInfo) {
|
|
681
|
-
return {
|
|
682
|
-
content: [
|
|
683
|
-
{
|
|
684
|
-
type: "text",
|
|
685
|
-
text: "No global server running. Start one with `diffprism server`."
|
|
686
|
-
}
|
|
687
|
-
],
|
|
688
|
-
isError: true
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
let flagged = 0;
|
|
692
|
-
for (const file of files) {
|
|
693
|
-
const response = await fetch(
|
|
694
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations`,
|
|
695
|
-
{
|
|
696
|
-
method: "POST",
|
|
697
|
-
headers: { "Content-Type": "application/json" },
|
|
698
|
-
body: JSON.stringify({
|
|
699
|
-
file: file.path,
|
|
700
|
-
line: file.line ?? 1,
|
|
701
|
-
body: file.reason,
|
|
702
|
-
type: "warning",
|
|
703
|
-
confidence: 1,
|
|
704
|
-
category: "other",
|
|
705
|
-
source: {
|
|
706
|
-
agent: source_agent ?? "flag_for_attention",
|
|
707
|
-
tool: "flag_for_attention"
|
|
708
|
-
}
|
|
709
|
-
})
|
|
710
|
-
}
|
|
711
|
-
);
|
|
712
|
-
if (response.ok) {
|
|
713
|
-
flagged++;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
return {
|
|
717
|
-
content: [
|
|
718
|
-
{
|
|
719
|
-
type: "text",
|
|
720
|
-
text: JSON.stringify({ flagged, sessionId }, null, 2)
|
|
721
|
-
}
|
|
722
|
-
]
|
|
723
|
-
};
|
|
724
|
-
} catch (err) {
|
|
725
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
726
|
-
return {
|
|
727
|
-
content: [
|
|
728
|
-
{
|
|
729
|
-
type: "text",
|
|
730
|
-
text: `Error: ${message}`
|
|
731
|
-
}
|
|
732
|
-
],
|
|
733
|
-
isError: true
|
|
734
|
-
};
|
|
374
|
+
async ({ session_id, repo_path, awaiting_reply }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
375
|
+
const threads = await readThreads(serverInfo, sessionId);
|
|
376
|
+
if (!threads) {
|
|
377
|
+
return toolError(`Session not found: ${sessionId}`);
|
|
735
378
|
}
|
|
736
|
-
|
|
379
|
+
const annotations = awaiting_reply ? threads.filter((t) => t.awaitingReply) : threads;
|
|
380
|
+
return jsonResult({ sessionId, annotations });
|
|
381
|
+
})
|
|
737
382
|
);
|
|
738
383
|
server.tool(
|
|
739
|
-
"
|
|
740
|
-
"
|
|
384
|
+
"reply",
|
|
385
|
+
"Reply to a thread on an open review \u2014 answer the reviewer's question, or follow up on a finding. The reply appears under the thread in the dashboard straight away.",
|
|
741
386
|
{
|
|
742
|
-
|
|
387
|
+
...targetParams,
|
|
388
|
+
annotation_id: z.string().describe("The thread to reply to (an annotation id from get_review_comments or wait_for_comments)"),
|
|
389
|
+
body: z.string().describe("Your reply"),
|
|
390
|
+
source_agent: z.string().optional().describe("Who is replying, e.g. 'pr-reviewer'")
|
|
743
391
|
},
|
|
744
|
-
async ({ session_id }) => {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
const sessionId = await resolveSessionId(session_id, serverInfo);
|
|
754
|
-
if (!sessionId) {
|
|
755
|
-
return {
|
|
756
|
-
content: [{ type: "text", text: "No review session found. Open a PR review first with `diffprism review <PR URL>`." }],
|
|
757
|
-
isError: true
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
const response = await fetch(
|
|
761
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/payload`
|
|
762
|
-
);
|
|
763
|
-
if (!response.ok) {
|
|
764
|
-
return {
|
|
765
|
-
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
766
|
-
isError: true
|
|
767
|
-
};
|
|
392
|
+
async ({ session_id, repo_path, annotation_id, body, source_agent }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
393
|
+
const response = await fetch(
|
|
394
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations/${annotation_id}/replies`,
|
|
395
|
+
{
|
|
396
|
+
method: "POST",
|
|
397
|
+
headers: { "Content-Type": "application/json" },
|
|
398
|
+
body: JSON.stringify({ author: "agent", agent: source_agent ?? "unknown", body })
|
|
768
399
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
projectPath,
|
|
774
|
-
localRepoConnected: !projectPath.startsWith("github:"),
|
|
775
|
-
pr: payload.metadata.githubPr ?? null,
|
|
776
|
-
title: payload.metadata.title,
|
|
777
|
-
description: payload.metadata.description,
|
|
778
|
-
briefingSummary: payload.briefing.summary,
|
|
779
|
-
triage: payload.briefing.triage,
|
|
780
|
-
files: payload.diffSet.files.map((f) => ({
|
|
781
|
-
path: f.path,
|
|
782
|
-
status: f.status,
|
|
783
|
-
additions: f.additions,
|
|
784
|
-
deletions: f.deletions,
|
|
785
|
-
language: f.language
|
|
786
|
-
})),
|
|
787
|
-
totalFiles: payload.diffSet.files.length
|
|
788
|
-
};
|
|
789
|
-
return {
|
|
790
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
791
|
-
};
|
|
792
|
-
} catch (err) {
|
|
793
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
794
|
-
return {
|
|
795
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
796
|
-
isError: true
|
|
797
|
-
};
|
|
400
|
+
);
|
|
401
|
+
const data = await response.json().catch(() => ({}));
|
|
402
|
+
if (!response.ok) {
|
|
403
|
+
return toolError(`Error: ${data.error ?? `server returned ${response.status}`}`);
|
|
798
404
|
}
|
|
799
|
-
|
|
405
|
+
return jsonResult({ sessionId, annotationId: annotation_id, replyId: data.replyId });
|
|
406
|
+
})
|
|
800
407
|
);
|
|
801
408
|
server.tool(
|
|
802
|
-
"
|
|
803
|
-
"
|
|
409
|
+
"wait_for_comments",
|
|
410
|
+
"Wait for the reviewer to write something you haven't answered \u2014 a new comment on a line, or a reply in a thread. Returns those threads as soon as there are any. Use it to hold a conversation during a PR review: wait, answer each thread with `reply`, then wait again. If it returns `timed_out`, nothing new was said; wait again rather than asking the user in the terminal.",
|
|
804
411
|
{
|
|
805
|
-
|
|
806
|
-
|
|
412
|
+
...targetParams,
|
|
413
|
+
timeout: z.number().optional().describe(`Max wait in seconds (default ${DEFAULT_WAIT_MS / 1e3}, max 600)`)
|
|
807
414
|
},
|
|
808
|
-
async ({
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
);
|
|
827
|
-
if (!response.ok) {
|
|
828
|
-
return {
|
|
829
|
-
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
830
|
-
isError: true
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
const data = await response.json();
|
|
834
|
-
const diffFile = data.payload.diffSet.files.find((f) => f.path === file);
|
|
835
|
-
if (!diffFile) {
|
|
836
|
-
const available = data.payload.diffSet.files.map((f) => f.path);
|
|
837
|
-
return {
|
|
838
|
-
content: [{ type: "text", text: `File not found in diff: "${file}". Available files:
|
|
839
|
-
${available.join("\n")}` }],
|
|
840
|
-
isError: true
|
|
841
|
-
};
|
|
415
|
+
async ({ session_id, repo_path, timeout }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
416
|
+
const maxWaitMs = Math.min(timeout ?? DEFAULT_WAIT_MS / 1e3, 600) * 1e3;
|
|
417
|
+
const start = Date.now();
|
|
418
|
+
while (true) {
|
|
419
|
+
const threads = await readThreads(serverInfo, sessionId);
|
|
420
|
+
if (!threads) {
|
|
421
|
+
return toolError(`Session not found: ${sessionId}`);
|
|
422
|
+
}
|
|
423
|
+
const waiting = threads.filter((t) => t.awaitingReply);
|
|
424
|
+
if (waiting.length > 0) {
|
|
425
|
+
return jsonResult({ sessionId, threads: waiting });
|
|
426
|
+
}
|
|
427
|
+
if (Date.now() - start >= maxWaitMs) {
|
|
428
|
+
return jsonResult({
|
|
429
|
+
status: "timed_out",
|
|
430
|
+
sessionId,
|
|
431
|
+
message: "The reviewer hasn't written anything new. Call wait_for_comments again to keep listening \u2014 don't ask them in the terminal."
|
|
432
|
+
});
|
|
842
433
|
}
|
|
843
|
-
|
|
844
|
-
let category = "mechanical";
|
|
845
|
-
if (triage.critical.some((c) => c.file === file)) category = "critical";
|
|
846
|
-
else if (triage.notable.some((n) => n.file === file)) category = "notable";
|
|
847
|
-
return {
|
|
848
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
849
|
-
path: diffFile.path,
|
|
850
|
-
oldPath: diffFile.oldPath,
|
|
851
|
-
status: diffFile.status,
|
|
852
|
-
language: diffFile.language,
|
|
853
|
-
additions: diffFile.additions,
|
|
854
|
-
deletions: diffFile.deletions,
|
|
855
|
-
triageCategory: category,
|
|
856
|
-
hunks: diffFile.hunks
|
|
857
|
-
}, null, 2) }]
|
|
858
|
-
};
|
|
859
|
-
} catch (err) {
|
|
860
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
861
|
-
return {
|
|
862
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
863
|
-
isError: true
|
|
864
|
-
};
|
|
434
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
865
435
|
}
|
|
866
|
-
}
|
|
436
|
+
})
|
|
867
437
|
);
|
|
868
438
|
server.tool(
|
|
869
|
-
"
|
|
870
|
-
"Get the
|
|
871
|
-
{
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const serverInfo = await isServerAlive();
|
|
879
|
-
if (!serverInfo) {
|
|
880
|
-
return {
|
|
881
|
-
content: [{ type: "text", text: "No global server running. Start one with `diffprism server`." }],
|
|
882
|
-
isError: true
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
const sessionId = await resolveSessionId(session_id, serverInfo);
|
|
886
|
-
if (!sessionId) {
|
|
887
|
-
return {
|
|
888
|
-
content: [{ type: "text", text: "No review session found. Open a PR review first with `diffprism review <PR URL>`." }],
|
|
889
|
-
isError: true
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
const response = await fetch(
|
|
893
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/payload`
|
|
894
|
-
);
|
|
895
|
-
if (!response.ok) {
|
|
896
|
-
return {
|
|
897
|
-
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
898
|
-
isError: true
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
const data = await response.json();
|
|
902
|
-
if (data.projectPath.startsWith("github:")) {
|
|
903
|
-
return {
|
|
904
|
-
content: [{ type: "text", text: "No local repo connected. Run the server from within a local clone of the repository to enable file context." }],
|
|
905
|
-
isError: true
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
const gitRef = ref ?? (data.payload.metadata.githubPr?.headBranch ? `origin/${data.payload.metadata.githubPr.headBranch}` : "HEAD");
|
|
909
|
-
const { execSync } = await import("child_process");
|
|
910
|
-
let content;
|
|
911
|
-
try {
|
|
912
|
-
content = execSync(`git show ${gitRef}:${file}`, {
|
|
913
|
-
cwd: data.projectPath,
|
|
914
|
-
encoding: "utf-8",
|
|
915
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
916
|
-
maxBuffer: 10 * 1024 * 1024
|
|
917
|
-
// 10MB
|
|
918
|
-
});
|
|
919
|
-
} catch {
|
|
920
|
-
const fs = await import("fs");
|
|
921
|
-
const path = await import("path");
|
|
922
|
-
const filePath = path.join(data.projectPath, file);
|
|
923
|
-
try {
|
|
924
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
925
|
-
} catch {
|
|
926
|
-
return {
|
|
927
|
-
content: [{ type: "text", text: `File not found: "${file}" (tried git show ${gitRef}:${file} and working tree)` }],
|
|
928
|
-
isError: true
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
return {
|
|
933
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
934
|
-
file,
|
|
935
|
-
ref: gitRef,
|
|
936
|
-
projectPath: data.projectPath,
|
|
937
|
-
content,
|
|
938
|
-
lineCount: content.split("\n").length
|
|
939
|
-
}, null, 2) }]
|
|
940
|
-
};
|
|
941
|
-
} catch (err) {
|
|
942
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
943
|
-
return {
|
|
944
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
945
|
-
isError: true
|
|
946
|
-
};
|
|
439
|
+
"get_user_focus",
|
|
440
|
+
"Get what the reviewer is looking at right now in the DiffPrism dashboard \u2014 the selected file and any focused line range. Use this to answer questions about the code they are actively reviewing.",
|
|
441
|
+
{ ...targetParams },
|
|
442
|
+
async ({ session_id, repo_path }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
443
|
+
const response = await fetch(
|
|
444
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/focus`
|
|
445
|
+
);
|
|
446
|
+
if (!response.ok) {
|
|
447
|
+
return toolError(`Session not found: ${sessionId}`);
|
|
947
448
|
}
|
|
948
|
-
|
|
449
|
+
const data = await response.json();
|
|
450
|
+
return jsonResult({ sessionId, ...data });
|
|
451
|
+
})
|
|
949
452
|
);
|
|
950
453
|
server.tool(
|
|
951
|
-
"
|
|
952
|
-
"
|
|
953
|
-
{
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
async ({ file, line, body, type, session_id }) => {
|
|
961
|
-
try {
|
|
962
|
-
const serverInfo = await isServerAlive();
|
|
963
|
-
if (!serverInfo) {
|
|
964
|
-
return {
|
|
965
|
-
content: [{ type: "text", text: "No global server running. Start one with `diffprism server`." }],
|
|
966
|
-
isError: true
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
const sessionId = await resolveSessionId(session_id, serverInfo);
|
|
970
|
-
if (!sessionId) {
|
|
971
|
-
return {
|
|
972
|
-
content: [{ type: "text", text: "No review session found. Open a PR review first with `diffprism review <PR URL>`." }],
|
|
973
|
-
isError: true
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
|
-
const annotationType = type === "concern" ? "warning" : type === "suggestion" ? "suggestion" : "finding";
|
|
977
|
-
const response = await fetch(
|
|
978
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations`,
|
|
979
|
-
{
|
|
980
|
-
method: "POST",
|
|
981
|
-
headers: { "Content-Type": "application/json" },
|
|
982
|
-
body: JSON.stringify({
|
|
983
|
-
file,
|
|
984
|
-
line,
|
|
985
|
-
body,
|
|
986
|
-
type: annotationType,
|
|
987
|
-
confidence: 1,
|
|
988
|
-
category: "other",
|
|
989
|
-
source: {
|
|
990
|
-
agent: "ai-reviewer",
|
|
991
|
-
tool: "add_review_comment"
|
|
992
|
-
}
|
|
993
|
-
})
|
|
994
|
-
}
|
|
995
|
-
);
|
|
996
|
-
if (!response.ok) {
|
|
997
|
-
const errorData = await response.json().catch(() => ({}));
|
|
998
|
-
return {
|
|
999
|
-
content: [{ type: "text", text: `Error: ${errorData.error ?? `Server returned ${response.status}`}` }],
|
|
1000
|
-
isError: true
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
const data = await response.json();
|
|
1004
|
-
return {
|
|
1005
|
-
content: [{ type: "text", text: JSON.stringify({ annotationId: data.annotationId, sessionId }, null, 2) }]
|
|
1006
|
-
};
|
|
1007
|
-
} catch (err) {
|
|
1008
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1009
|
-
return {
|
|
1010
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
1011
|
-
isError: true
|
|
1012
|
-
};
|
|
454
|
+
"get_pr_context",
|
|
455
|
+
"Get an overview of an open PR review: PR metadata (title, author, branches, URL), briefing summary, file list with stats, and the local repo path. Orient yourself with this before reading individual files. Open the PR review first with `diffprism review <PR URL>` or the dashboard.",
|
|
456
|
+
{ ...targetParams },
|
|
457
|
+
async ({ session_id, repo_path }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
458
|
+
const response = await fetch(
|
|
459
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/payload`
|
|
460
|
+
);
|
|
461
|
+
if (!response.ok) {
|
|
462
|
+
return toolError(`Session not found: ${sessionId}`);
|
|
1013
463
|
}
|
|
1014
|
-
|
|
464
|
+
const { payload, projectPath } = await response.json();
|
|
465
|
+
return jsonResult({
|
|
466
|
+
sessionId,
|
|
467
|
+
projectPath,
|
|
468
|
+
localRepoConnected: !projectPath.startsWith("github:"),
|
|
469
|
+
pr: payload.metadata.githubPr ?? null,
|
|
470
|
+
title: payload.metadata.title,
|
|
471
|
+
description: payload.metadata.description,
|
|
472
|
+
briefingSummary: payload.briefing.summary,
|
|
473
|
+
triage: payload.briefing.triage,
|
|
474
|
+
files: payload.diffSet.files.map((f) => ({
|
|
475
|
+
path: f.path,
|
|
476
|
+
status: f.status,
|
|
477
|
+
additions: f.additions,
|
|
478
|
+
deletions: f.deletions,
|
|
479
|
+
language: f.language
|
|
480
|
+
})),
|
|
481
|
+
totalFiles: payload.diffSet.files.length
|
|
482
|
+
});
|
|
483
|
+
})
|
|
1015
484
|
);
|
|
1016
485
|
server.tool(
|
|
1017
|
-
"
|
|
1018
|
-
"Get
|
|
486
|
+
"get_file_diff",
|
|
487
|
+
"Get the diff hunks for one file in an open review, with its triage category. Use this to work through a review one file at a time.",
|
|
1019
488
|
{
|
|
1020
|
-
|
|
489
|
+
file: z.string().describe("File path within the diff (e.g., 'src/index.ts')"),
|
|
490
|
+
...targetParams
|
|
1021
491
|
},
|
|
1022
|
-
async ({ session_id }) => {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
isError: true
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
const sessionId = await resolveSessionId(session_id, serverInfo);
|
|
1032
|
-
if (!sessionId) {
|
|
1033
|
-
return {
|
|
1034
|
-
content: [{ type: "text", text: "No review session found. Open a PR review first with `diffprism review <PR URL>`." }],
|
|
1035
|
-
isError: true
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
const response = await fetch(
|
|
1039
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations`
|
|
1040
|
-
);
|
|
1041
|
-
if (!response.ok) {
|
|
1042
|
-
return {
|
|
1043
|
-
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
1044
|
-
isError: true
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
const data = await response.json();
|
|
1048
|
-
return {
|
|
1049
|
-
content: [{ type: "text", text: JSON.stringify({ sessionId, annotations: data.annotations }, null, 2) }]
|
|
1050
|
-
};
|
|
1051
|
-
} catch (err) {
|
|
1052
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1053
|
-
return {
|
|
1054
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
1055
|
-
isError: true
|
|
1056
|
-
};
|
|
492
|
+
async ({ file, session_id, repo_path }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
493
|
+
const response = await fetch(
|
|
494
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/payload`
|
|
495
|
+
);
|
|
496
|
+
if (!response.ok) {
|
|
497
|
+
return toolError(`Session not found: ${sessionId}`);
|
|
1057
498
|
}
|
|
1058
|
-
|
|
499
|
+
const data = await response.json();
|
|
500
|
+
const diffFile = data.payload.diffSet.files.find((f) => f.path === file);
|
|
501
|
+
if (!diffFile) {
|
|
502
|
+
const available = data.payload.diffSet.files.map((f) => f.path);
|
|
503
|
+
return toolError(`File not found in diff: "${file}". Available files:
|
|
504
|
+
${available.join("\n")}`);
|
|
505
|
+
}
|
|
506
|
+
const { triage } = data.payload.briefing;
|
|
507
|
+
let triageCategory = "mechanical";
|
|
508
|
+
if (triage.critical.some((c) => c.file === file)) triageCategory = "critical";
|
|
509
|
+
else if (triage.notable.some((n) => n.file === file)) triageCategory = "notable";
|
|
510
|
+
return jsonResult({
|
|
511
|
+
path: diffFile.path,
|
|
512
|
+
oldPath: diffFile.oldPath,
|
|
513
|
+
status: diffFile.status,
|
|
514
|
+
language: diffFile.language,
|
|
515
|
+
additions: diffFile.additions,
|
|
516
|
+
deletions: diffFile.deletions,
|
|
517
|
+
triageCategory,
|
|
518
|
+
hunks: diffFile.hunks
|
|
519
|
+
});
|
|
520
|
+
})
|
|
1059
521
|
);
|
|
1060
522
|
server.tool(
|
|
1061
|
-
"
|
|
1062
|
-
"Get
|
|
523
|
+
"get_file_context",
|
|
524
|
+
"Get the full content of a file from the review's local repository, read with `git show` at the PR's head branch without switching branches. Needs the review to be connected to a local clone.",
|
|
1063
525
|
{
|
|
1064
|
-
|
|
526
|
+
file: z.string().describe("File path relative to repo root (e.g., 'src/index.ts')"),
|
|
527
|
+
ref: z.string().optional().describe("Git ref to read from (e.g., 'origin/main', 'HEAD'). Defaults to the PR's head branch if available, otherwise HEAD."),
|
|
528
|
+
...targetParams
|
|
1065
529
|
},
|
|
1066
|
-
async ({ session_id }) => {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
return {
|
|
1078
|
-
content: [{ type: "text", text: "No review session found. Open a PR review first with `diffprism review <PR URL>`." }],
|
|
1079
|
-
isError: true
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
const response = await fetch(
|
|
1083
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/focus`
|
|
530
|
+
async ({ file, ref, session_id, repo_path }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
|
|
531
|
+
const response = await fetch(
|
|
532
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/payload`
|
|
533
|
+
);
|
|
534
|
+
if (!response.ok) {
|
|
535
|
+
return toolError(`Session not found: ${sessionId}`);
|
|
536
|
+
}
|
|
537
|
+
const data = await response.json();
|
|
538
|
+
if (data.projectPath.startsWith("github:")) {
|
|
539
|
+
return toolError(
|
|
540
|
+
"No local repo connected. Run the server from within a local clone of the repository to enable file context."
|
|
1084
541
|
);
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
542
|
+
}
|
|
543
|
+
const gitRef = ref ?? (data.payload.metadata.githubPr?.headBranch ? `origin/${data.payload.metadata.githubPr.headBranch}` : "HEAD");
|
|
544
|
+
const { execSync } = await import("child_process");
|
|
545
|
+
let content;
|
|
546
|
+
try {
|
|
547
|
+
content = execSync(`git show ${gitRef}:${file}`, {
|
|
548
|
+
cwd: data.projectPath,
|
|
549
|
+
encoding: "utf-8",
|
|
550
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
551
|
+
maxBuffer: 10 * 1024 * 1024
|
|
552
|
+
});
|
|
553
|
+
} catch {
|
|
554
|
+
const fs = await import("fs");
|
|
555
|
+
const path = await import("path");
|
|
556
|
+
try {
|
|
557
|
+
content = fs.readFileSync(path.join(data.projectPath, file), "utf-8");
|
|
558
|
+
} catch {
|
|
559
|
+
return toolError(`File not found: "${file}" (tried git show ${gitRef}:${file} and working tree)`);
|
|
1090
560
|
}
|
|
1091
|
-
const data = await response.json();
|
|
1092
|
-
return {
|
|
1093
|
-
content: [{ type: "text", text: JSON.stringify({ sessionId, ...data }, null, 2) }]
|
|
1094
|
-
};
|
|
1095
|
-
} catch (err) {
|
|
1096
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1097
|
-
return {
|
|
1098
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
1099
|
-
isError: true
|
|
1100
|
-
};
|
|
1101
561
|
}
|
|
1102
|
-
|
|
562
|
+
return jsonResult({
|
|
563
|
+
file,
|
|
564
|
+
ref: gitRef,
|
|
565
|
+
projectPath: data.projectPath,
|
|
566
|
+
content,
|
|
567
|
+
lineCount: content.split("\n").length
|
|
568
|
+
});
|
|
569
|
+
})
|
|
1103
570
|
);
|
|
1104
571
|
const transport = new StdioServerTransport();
|
|
1105
572
|
await server.connect(transport);
|
|
1106
573
|
}
|
|
1107
574
|
export {
|
|
575
|
+
DEFAULT_WAIT_MS,
|
|
576
|
+
resolveTarget,
|
|
1108
577
|
startMcpServer
|
|
1109
578
|
};
|