issue-scribe-mcp 1.2.0 → 1.3.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/.github/workflows/ci.yml +29 -0
- package/README.md +110 -5
- package/README_EN.md +108 -3
- package/dist/index.js +7 -1689
- package/dist/lib/env.js +9 -0
- package/dist/lib/errors.js +46 -0
- package/dist/lib/octokit.js +13 -0
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/response.js +32 -0
- package/dist/lib/safety.js +12 -0
- package/dist/lib/search.js +32 -0
- package/dist/lib/version.js +14 -0
- package/dist/tools/branches.js +249 -0
- package/dist/tools/comments.js +233 -0
- package/dist/tools/index.js +29 -0
- package/dist/tools/issues.js +393 -0
- package/dist/tools/labels.js +204 -0
- package/dist/tools/pull-requests.js +724 -0
- package/dist/tools/types.js +1 -0
- package/package.json +3 -3
- package/src/index.ts +7 -1869
- package/src/lib/env.ts +15 -0
- package/src/lib/errors.ts +64 -0
- package/src/lib/octokit.ts +17 -0
- package/src/lib/pagination.ts +72 -0
- package/src/lib/response.ts +42 -0
- package/src/lib/safety.ts +25 -0
- package/src/lib/search.ts +52 -0
- package/src/lib/version.ts +19 -0
- package/src/tools/branches.ts +287 -0
- package/src/tools/comments.ts +272 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/issues.ts +452 -0
- package/src/tools/labels.ts +239 -0
- package/src/tools/pull-requests.ts +838 -0
- package/src/tools/types.ts +18 -0
- package/test/safety.test.mjs +28 -0
- package/test/search.test.mjs +28 -0
- package/test/version-and-metadata.test.mjs +32 -0
- package/test-local.sh +1 -1
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ToolValidationError } from "../lib/errors.js";
|
|
3
|
+
import { getOctokit } from "../lib/octokit.js";
|
|
4
|
+
import { collectPaginated, PaginationSchema, resolvePagination } from "../lib/pagination.js";
|
|
5
|
+
import { executeTool } from "../lib/response.js";
|
|
6
|
+
import { buildRepositorySearchQuery, normalizeSearchSort } from "../lib/search.js";
|
|
7
|
+
import { assertConfirmation, assertExpectedValue } from "../lib/safety.js";
|
|
8
|
+
const GetPRContextSchema = z.object({
|
|
9
|
+
owner: z.string(),
|
|
10
|
+
repo: z.string(),
|
|
11
|
+
pull_number: z.number(),
|
|
12
|
+
include_reviews: z.boolean().optional(),
|
|
13
|
+
include_review_comments: z.boolean().optional(),
|
|
14
|
+
include_files: z.boolean().optional(),
|
|
15
|
+
include_file_patches: z.boolean().optional(),
|
|
16
|
+
include_ci: z.boolean().optional(),
|
|
17
|
+
}).merge(PaginationSchema);
|
|
18
|
+
const CreatePRSchema = z.object({
|
|
19
|
+
owner: z.string(),
|
|
20
|
+
repo: z.string(),
|
|
21
|
+
title: z.string(),
|
|
22
|
+
body: z.string().optional(),
|
|
23
|
+
head: z.string(),
|
|
24
|
+
base: z.string(),
|
|
25
|
+
draft: z.boolean().optional(),
|
|
26
|
+
maintainer_can_modify: z.boolean().optional(),
|
|
27
|
+
});
|
|
28
|
+
const MergePRSchema = z.object({
|
|
29
|
+
owner: z.string(),
|
|
30
|
+
repo: z.string(),
|
|
31
|
+
pull_number: z.number(),
|
|
32
|
+
merge_method: z.enum(["merge", "squash", "rebase"]).optional(),
|
|
33
|
+
commit_title: z.string().optional(),
|
|
34
|
+
commit_message: z.string().optional(),
|
|
35
|
+
dry_run: z.boolean().optional(),
|
|
36
|
+
expected_head_sha: z.string().optional(),
|
|
37
|
+
confirm_token: z.string().optional(),
|
|
38
|
+
});
|
|
39
|
+
const SearchPRsSchema = z.object({
|
|
40
|
+
owner: z.string(),
|
|
41
|
+
repo: z.string(),
|
|
42
|
+
query: z.string().optional(),
|
|
43
|
+
state: z.enum(["open", "closed", "all"]).optional(),
|
|
44
|
+
qualifiers: z.array(z.string()).optional(),
|
|
45
|
+
sort: z.enum(["created", "updated", "comments", "best-match", "popularity", "long-running"]).optional(),
|
|
46
|
+
direction: z.enum(["asc", "desc"]).optional(),
|
|
47
|
+
}).merge(PaginationSchema);
|
|
48
|
+
const GetPRDiffSchema = z.object({
|
|
49
|
+
owner: z.string(),
|
|
50
|
+
repo: z.string(),
|
|
51
|
+
pull_number: z.number(),
|
|
52
|
+
max_chars: z.number().int().min(1).optional(),
|
|
53
|
+
});
|
|
54
|
+
const GetPRFilesSchema = z.object({
|
|
55
|
+
owner: z.string(),
|
|
56
|
+
repo: z.string(),
|
|
57
|
+
pull_number: z.number(),
|
|
58
|
+
include_patch: z.boolean().optional(),
|
|
59
|
+
}).merge(PaginationSchema);
|
|
60
|
+
function summarizeReviews(reviews) {
|
|
61
|
+
const sorted = [...reviews].sort((a, b) => {
|
|
62
|
+
const left = a.submitted_at ? Date.parse(a.submitted_at) : 0;
|
|
63
|
+
const right = b.submitted_at ? Date.parse(b.submitted_at) : 0;
|
|
64
|
+
return left - right;
|
|
65
|
+
});
|
|
66
|
+
const latestByReviewer = new Map();
|
|
67
|
+
for (const review of sorted) {
|
|
68
|
+
const reviewer = review.user?.login;
|
|
69
|
+
if (!reviewer) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
latestByReviewer.set(reviewer, review.state);
|
|
73
|
+
}
|
|
74
|
+
const approvedBy = [];
|
|
75
|
+
const changesRequestedBy = [];
|
|
76
|
+
const commentedBy = [];
|
|
77
|
+
for (const [reviewer, state] of latestByReviewer.entries()) {
|
|
78
|
+
if (state === "APPROVED") {
|
|
79
|
+
approvedBy.push(reviewer);
|
|
80
|
+
}
|
|
81
|
+
else if (state === "CHANGES_REQUESTED") {
|
|
82
|
+
changesRequestedBy.push(reviewer);
|
|
83
|
+
}
|
|
84
|
+
else if (state === "COMMENTED") {
|
|
85
|
+
commentedBy.push(reviewer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
total_reviews: reviews.length,
|
|
90
|
+
latest_state_by_reviewer: Object.fromEntries(latestByReviewer.entries()),
|
|
91
|
+
approved_by: approvedBy,
|
|
92
|
+
changes_requested_by: changesRequestedBy,
|
|
93
|
+
commented_by: commentedBy,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function listCheckRuns(owner, repo, ref, page, perPage, fetchAll) {
|
|
97
|
+
if (!fetchAll) {
|
|
98
|
+
const result = await getOctokit().rest.checks.listForRef({
|
|
99
|
+
owner,
|
|
100
|
+
repo,
|
|
101
|
+
ref,
|
|
102
|
+
page,
|
|
103
|
+
per_page: perPage,
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
total_count: result.data.total_count,
|
|
107
|
+
check_runs: result.data.check_runs,
|
|
108
|
+
pagination: {
|
|
109
|
+
page,
|
|
110
|
+
per_page: perPage,
|
|
111
|
+
fetch_all: false,
|
|
112
|
+
pages_fetched: 1,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const allRuns = [];
|
|
117
|
+
let currentPage = page;
|
|
118
|
+
let pagesFetched = 0;
|
|
119
|
+
let totalCount = 0;
|
|
120
|
+
while (true) {
|
|
121
|
+
const result = await getOctokit().rest.checks.listForRef({
|
|
122
|
+
owner,
|
|
123
|
+
repo,
|
|
124
|
+
ref,
|
|
125
|
+
page: currentPage,
|
|
126
|
+
per_page: perPage,
|
|
127
|
+
});
|
|
128
|
+
if (pagesFetched === 0) {
|
|
129
|
+
totalCount = result.data.total_count;
|
|
130
|
+
}
|
|
131
|
+
allRuns.push(...result.data.check_runs);
|
|
132
|
+
pagesFetched += 1;
|
|
133
|
+
if (result.data.check_runs.length < perPage) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
if (allRuns.length >= totalCount) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
currentPage += 1;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
total_count: totalCount,
|
|
143
|
+
check_runs: allRuns,
|
|
144
|
+
pagination: {
|
|
145
|
+
page,
|
|
146
|
+
per_page: perPage,
|
|
147
|
+
fetch_all: true,
|
|
148
|
+
pages_fetched: pagesFetched,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function searchPRsWithPagination(args) {
|
|
153
|
+
const pagination = resolvePagination(args, {
|
|
154
|
+
page: 1,
|
|
155
|
+
perPage: 30,
|
|
156
|
+
fetchAll: false,
|
|
157
|
+
});
|
|
158
|
+
const q = buildRepositorySearchQuery({
|
|
159
|
+
owner: args.owner,
|
|
160
|
+
repo: args.repo,
|
|
161
|
+
kind: "pr",
|
|
162
|
+
state: args.state,
|
|
163
|
+
query: args.query,
|
|
164
|
+
qualifiers: args.qualifiers,
|
|
165
|
+
});
|
|
166
|
+
const sort = normalizeSearchSort(args.sort);
|
|
167
|
+
const order = args.direction;
|
|
168
|
+
if (!pagination.fetchAll) {
|
|
169
|
+
const response = await getOctokit().rest.search.issuesAndPullRequests({
|
|
170
|
+
q,
|
|
171
|
+
sort: sort,
|
|
172
|
+
order: order,
|
|
173
|
+
page: pagination.page,
|
|
174
|
+
per_page: pagination.perPage,
|
|
175
|
+
});
|
|
176
|
+
const pullRequests = response.data.items.filter((item) => "pull_request" in item);
|
|
177
|
+
return {
|
|
178
|
+
query: q,
|
|
179
|
+
total_count: response.data.total_count,
|
|
180
|
+
incomplete_results: response.data.incomplete_results,
|
|
181
|
+
page: pagination.page,
|
|
182
|
+
per_page: pagination.perPage,
|
|
183
|
+
fetch_all: false,
|
|
184
|
+
count: pullRequests.length,
|
|
185
|
+
pull_requests: pullRequests.map((pr) => ({
|
|
186
|
+
number: pr.number,
|
|
187
|
+
title: pr.title,
|
|
188
|
+
state: pr.state,
|
|
189
|
+
user: pr.user?.login,
|
|
190
|
+
comments: pr.comments,
|
|
191
|
+
created_at: pr.created_at,
|
|
192
|
+
updated_at: pr.updated_at,
|
|
193
|
+
html_url: pr.html_url,
|
|
194
|
+
score: pr.score,
|
|
195
|
+
})),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const allItems = [];
|
|
199
|
+
let page = pagination.page;
|
|
200
|
+
let totalCount = 0;
|
|
201
|
+
let incompleteResults = false;
|
|
202
|
+
while (true) {
|
|
203
|
+
const response = await getOctokit().rest.search.issuesAndPullRequests({
|
|
204
|
+
q,
|
|
205
|
+
sort: sort,
|
|
206
|
+
order: order,
|
|
207
|
+
page,
|
|
208
|
+
per_page: pagination.perPage,
|
|
209
|
+
});
|
|
210
|
+
if (page === pagination.page) {
|
|
211
|
+
totalCount = response.data.total_count;
|
|
212
|
+
incompleteResults = response.data.incomplete_results;
|
|
213
|
+
}
|
|
214
|
+
allItems.push(...response.data.items);
|
|
215
|
+
if (response.data.items.length < pagination.perPage) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
if (allItems.length >= 1000) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
page += 1;
|
|
222
|
+
}
|
|
223
|
+
const pullRequests = allItems.filter((item) => "pull_request" in item);
|
|
224
|
+
return {
|
|
225
|
+
query: q,
|
|
226
|
+
total_count: totalCount,
|
|
227
|
+
incomplete_results: incompleteResults,
|
|
228
|
+
page: pagination.page,
|
|
229
|
+
per_page: pagination.perPage,
|
|
230
|
+
fetch_all: true,
|
|
231
|
+
pages_fetched: page - pagination.page + 1,
|
|
232
|
+
count: pullRequests.length,
|
|
233
|
+
pull_requests: pullRequests.map((pr) => ({
|
|
234
|
+
number: pr.number,
|
|
235
|
+
title: pr.title,
|
|
236
|
+
state: pr.state,
|
|
237
|
+
user: pr.user?.login,
|
|
238
|
+
comments: pr.comments,
|
|
239
|
+
created_at: pr.created_at,
|
|
240
|
+
updated_at: pr.updated_at,
|
|
241
|
+
html_url: pr.html_url,
|
|
242
|
+
score: pr.score,
|
|
243
|
+
})),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
export const pullRequestTools = [
|
|
247
|
+
{
|
|
248
|
+
definition: {
|
|
249
|
+
name: "github_get_pr_context",
|
|
250
|
+
description: "Get full PR context including issue comments, commits, reviews, review comments, changed files, and CI checks",
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: {
|
|
254
|
+
owner: { type: "string", description: "Repository owner" },
|
|
255
|
+
repo: { type: "string", description: "Repository name" },
|
|
256
|
+
pull_number: { type: "number", description: "Pull request number" },
|
|
257
|
+
include_reviews: { type: "boolean", description: "Include PR reviews and approval summary (optional, default: true)" },
|
|
258
|
+
include_review_comments: { type: "boolean", description: "Include line-level review comments (optional, default: true)" },
|
|
259
|
+
include_files: { type: "boolean", description: "Include changed files (optional, default: true)" },
|
|
260
|
+
include_file_patches: { type: "boolean", description: "Include patch text for files (optional, default: false)" },
|
|
261
|
+
include_ci: { type: "boolean", description: "Include combined status + check runs (optional, default: true)" },
|
|
262
|
+
page: { type: "number", description: "Page number for paginated collections (optional, default: 1)" },
|
|
263
|
+
per_page: { type: "number", description: "Items per page for paginated collections (optional, default: 100)" },
|
|
264
|
+
fetch_all: { type: "boolean", description: "Fetch all pages for comments/commits/reviews/files/check-runs (optional, default: true)" },
|
|
265
|
+
},
|
|
266
|
+
required: ["owner", "repo", "pull_number"],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
handler: async (rawArgs) => executeTool(rawArgs, GetPRContextSchema, "Failed to fetch PR context", async (args) => {
|
|
270
|
+
const pagination = resolvePagination(args, {
|
|
271
|
+
page: 1,
|
|
272
|
+
perPage: 100,
|
|
273
|
+
fetchAll: true,
|
|
274
|
+
});
|
|
275
|
+
const includeReviews = args.include_reviews ?? true;
|
|
276
|
+
const includeReviewComments = args.include_review_comments ?? true;
|
|
277
|
+
const includeFiles = args.include_files ?? true;
|
|
278
|
+
const includeFilePatches = args.include_file_patches ?? false;
|
|
279
|
+
const includeCI = args.include_ci ?? true;
|
|
280
|
+
const pr = await getOctokit().rest.pulls.get({
|
|
281
|
+
owner: args.owner,
|
|
282
|
+
repo: args.repo,
|
|
283
|
+
pull_number: args.pull_number,
|
|
284
|
+
});
|
|
285
|
+
const issueCommentsPromise = collectPaginated(pagination, async (page, perPage) => {
|
|
286
|
+
const response = await getOctokit().rest.issues.listComments({
|
|
287
|
+
owner: args.owner,
|
|
288
|
+
repo: args.repo,
|
|
289
|
+
issue_number: args.pull_number,
|
|
290
|
+
page,
|
|
291
|
+
per_page: perPage,
|
|
292
|
+
});
|
|
293
|
+
return response.data;
|
|
294
|
+
});
|
|
295
|
+
const commitsPromise = collectPaginated(pagination, async (page, perPage) => {
|
|
296
|
+
const response = await getOctokit().rest.pulls.listCommits({
|
|
297
|
+
owner: args.owner,
|
|
298
|
+
repo: args.repo,
|
|
299
|
+
pull_number: args.pull_number,
|
|
300
|
+
page,
|
|
301
|
+
per_page: perPage,
|
|
302
|
+
});
|
|
303
|
+
return response.data;
|
|
304
|
+
});
|
|
305
|
+
const reviewsPromise = includeReviews
|
|
306
|
+
? collectPaginated(pagination, async (page, perPage) => {
|
|
307
|
+
const response = await getOctokit().rest.pulls.listReviews({
|
|
308
|
+
owner: args.owner,
|
|
309
|
+
repo: args.repo,
|
|
310
|
+
pull_number: args.pull_number,
|
|
311
|
+
page,
|
|
312
|
+
per_page: perPage,
|
|
313
|
+
});
|
|
314
|
+
return response.data;
|
|
315
|
+
})
|
|
316
|
+
: Promise.resolve(null);
|
|
317
|
+
const reviewCommentsPromise = includeReviewComments
|
|
318
|
+
? collectPaginated(pagination, async (page, perPage) => {
|
|
319
|
+
const response = await getOctokit().rest.pulls.listReviewComments({
|
|
320
|
+
owner: args.owner,
|
|
321
|
+
repo: args.repo,
|
|
322
|
+
pull_number: args.pull_number,
|
|
323
|
+
page,
|
|
324
|
+
per_page: perPage,
|
|
325
|
+
});
|
|
326
|
+
return response.data;
|
|
327
|
+
})
|
|
328
|
+
: Promise.resolve(null);
|
|
329
|
+
const filesPromise = includeFiles
|
|
330
|
+
? collectPaginated(pagination, async (page, perPage) => {
|
|
331
|
+
const response = await getOctokit().rest.pulls.listFiles({
|
|
332
|
+
owner: args.owner,
|
|
333
|
+
repo: args.repo,
|
|
334
|
+
pull_number: args.pull_number,
|
|
335
|
+
page,
|
|
336
|
+
per_page: perPage,
|
|
337
|
+
});
|
|
338
|
+
return response.data;
|
|
339
|
+
})
|
|
340
|
+
: Promise.resolve(null);
|
|
341
|
+
const ciPromise = includeCI
|
|
342
|
+
? Promise.all([
|
|
343
|
+
getOctokit().rest.repos.getCombinedStatusForRef({
|
|
344
|
+
owner: args.owner,
|
|
345
|
+
repo: args.repo,
|
|
346
|
+
ref: pr.data.head.sha,
|
|
347
|
+
}),
|
|
348
|
+
listCheckRuns(args.owner, args.repo, pr.data.head.sha, pagination.page, pagination.perPage, pagination.fetchAll),
|
|
349
|
+
])
|
|
350
|
+
: Promise.resolve(null);
|
|
351
|
+
const [issueComments, commits, reviews, reviewComments, files, ci] = await Promise.all([
|
|
352
|
+
issueCommentsPromise,
|
|
353
|
+
commitsPromise,
|
|
354
|
+
reviewsPromise,
|
|
355
|
+
reviewCommentsPromise,
|
|
356
|
+
filesPromise,
|
|
357
|
+
ciPromise,
|
|
358
|
+
]);
|
|
359
|
+
const reviewSummary = reviews ? summarizeReviews(reviews.items) : null;
|
|
360
|
+
return {
|
|
361
|
+
pull_request: {
|
|
362
|
+
number: pr.data.number,
|
|
363
|
+
title: pr.data.title,
|
|
364
|
+
body: pr.data.body,
|
|
365
|
+
state: pr.data.state,
|
|
366
|
+
draft: pr.data.draft,
|
|
367
|
+
mergeable: pr.data.mergeable,
|
|
368
|
+
mergeable_state: pr.data.mergeable_state,
|
|
369
|
+
user: pr.data.user?.login,
|
|
370
|
+
assignees: pr.data.assignees?.map((assignee) => assignee.login),
|
|
371
|
+
requested_reviewers: pr.data.requested_reviewers?.map((reviewer) => reviewer.login),
|
|
372
|
+
labels: pr.data.labels.map((label) => (typeof label === "string" ? label : label.name)),
|
|
373
|
+
base: pr.data.base.ref,
|
|
374
|
+
head: pr.data.head.ref,
|
|
375
|
+
head_sha: pr.data.head.sha,
|
|
376
|
+
created_at: pr.data.created_at,
|
|
377
|
+
updated_at: pr.data.updated_at,
|
|
378
|
+
merged_at: pr.data.merged_at,
|
|
379
|
+
html_url: pr.data.html_url,
|
|
380
|
+
},
|
|
381
|
+
issue_comments: issueComments.items.map((comment) => ({
|
|
382
|
+
id: comment.id,
|
|
383
|
+
user: comment.user?.login,
|
|
384
|
+
body: comment.body,
|
|
385
|
+
created_at: comment.created_at,
|
|
386
|
+
updated_at: comment.updated_at,
|
|
387
|
+
html_url: comment.html_url,
|
|
388
|
+
})),
|
|
389
|
+
commits: commits.items.map((commit) => ({
|
|
390
|
+
sha: commit.sha,
|
|
391
|
+
message: commit.commit.message,
|
|
392
|
+
author: commit.commit.author?.name,
|
|
393
|
+
date: commit.commit.author?.date,
|
|
394
|
+
html_url: commit.html_url,
|
|
395
|
+
})),
|
|
396
|
+
reviews: reviews
|
|
397
|
+
? {
|
|
398
|
+
items: reviews.items.map((review) => ({
|
|
399
|
+
id: review.id,
|
|
400
|
+
user: review.user?.login,
|
|
401
|
+
state: review.state,
|
|
402
|
+
body: review.body,
|
|
403
|
+
submitted_at: review.submitted_at,
|
|
404
|
+
commit_id: review.commit_id,
|
|
405
|
+
})),
|
|
406
|
+
summary: reviewSummary,
|
|
407
|
+
pagination: {
|
|
408
|
+
page: reviews.page,
|
|
409
|
+
per_page: reviews.per_page,
|
|
410
|
+
fetch_all: reviews.fetch_all,
|
|
411
|
+
pages_fetched: reviews.pages_fetched,
|
|
412
|
+
},
|
|
413
|
+
}
|
|
414
|
+
: null,
|
|
415
|
+
review_comments: reviewComments
|
|
416
|
+
? {
|
|
417
|
+
items: reviewComments.items.map((comment) => ({
|
|
418
|
+
id: comment.id,
|
|
419
|
+
user: comment.user?.login,
|
|
420
|
+
body: comment.body,
|
|
421
|
+
path: comment.path,
|
|
422
|
+
line: comment.line,
|
|
423
|
+
side: comment.side,
|
|
424
|
+
commit_id: comment.commit_id,
|
|
425
|
+
created_at: comment.created_at,
|
|
426
|
+
updated_at: comment.updated_at,
|
|
427
|
+
html_url: comment.html_url,
|
|
428
|
+
})),
|
|
429
|
+
pagination: {
|
|
430
|
+
page: reviewComments.page,
|
|
431
|
+
per_page: reviewComments.per_page,
|
|
432
|
+
fetch_all: reviewComments.fetch_all,
|
|
433
|
+
pages_fetched: reviewComments.pages_fetched,
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
: null,
|
|
437
|
+
files: files
|
|
438
|
+
? {
|
|
439
|
+
items: files.items.map((file) => ({
|
|
440
|
+
filename: file.filename,
|
|
441
|
+
status: file.status,
|
|
442
|
+
additions: file.additions,
|
|
443
|
+
deletions: file.deletions,
|
|
444
|
+
changes: file.changes,
|
|
445
|
+
blob_url: file.blob_url,
|
|
446
|
+
raw_url: file.raw_url,
|
|
447
|
+
patch: includeFilePatches ? file.patch : undefined,
|
|
448
|
+
})),
|
|
449
|
+
pagination: {
|
|
450
|
+
page: files.page,
|
|
451
|
+
per_page: files.per_page,
|
|
452
|
+
fetch_all: files.fetch_all,
|
|
453
|
+
pages_fetched: files.pages_fetched,
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
|
+
: null,
|
|
457
|
+
ci: ci
|
|
458
|
+
? {
|
|
459
|
+
combined_status: {
|
|
460
|
+
state: ci[0].data.state,
|
|
461
|
+
sha: ci[0].data.sha,
|
|
462
|
+
total_count: ci[0].data.total_count,
|
|
463
|
+
statuses: ci[0].data.statuses.map((status) => ({
|
|
464
|
+
context: status.context,
|
|
465
|
+
state: status.state,
|
|
466
|
+
description: status.description,
|
|
467
|
+
target_url: status.target_url,
|
|
468
|
+
created_at: status.created_at,
|
|
469
|
+
updated_at: status.updated_at,
|
|
470
|
+
})),
|
|
471
|
+
},
|
|
472
|
+
checks: {
|
|
473
|
+
total_count: ci[1].total_count,
|
|
474
|
+
check_runs: ci[1].check_runs.map((checkRun) => ({
|
|
475
|
+
id: checkRun.id,
|
|
476
|
+
name: checkRun.name,
|
|
477
|
+
status: checkRun.status,
|
|
478
|
+
conclusion: checkRun.conclusion,
|
|
479
|
+
started_at: checkRun.started_at,
|
|
480
|
+
completed_at: checkRun.completed_at,
|
|
481
|
+
details_url: checkRun.details_url,
|
|
482
|
+
})),
|
|
483
|
+
pagination: ci[1].pagination,
|
|
484
|
+
},
|
|
485
|
+
}
|
|
486
|
+
: null,
|
|
487
|
+
pagination: {
|
|
488
|
+
page: pagination.page,
|
|
489
|
+
per_page: pagination.perPage,
|
|
490
|
+
fetch_all: pagination.fetchAll,
|
|
491
|
+
issue_comments_pages_fetched: issueComments.pages_fetched,
|
|
492
|
+
commits_pages_fetched: commits.pages_fetched,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}),
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
definition: {
|
|
499
|
+
name: "github_create_pr",
|
|
500
|
+
description: "Create a new pull request",
|
|
501
|
+
inputSchema: {
|
|
502
|
+
type: "object",
|
|
503
|
+
properties: {
|
|
504
|
+
owner: { type: "string", description: "Repository owner" },
|
|
505
|
+
repo: { type: "string", description: "Repository name" },
|
|
506
|
+
title: { type: "string", description: "PR title" },
|
|
507
|
+
body: { type: "string", description: "PR description (optional)" },
|
|
508
|
+
head: { type: "string", description: "Source branch" },
|
|
509
|
+
base: { type: "string", description: "Target branch" },
|
|
510
|
+
draft: { type: "boolean", description: "Create as draft PR (optional)" },
|
|
511
|
+
maintainer_can_modify: { type: "boolean", description: "Allow maintainer edits (optional)" },
|
|
512
|
+
},
|
|
513
|
+
required: ["owner", "repo", "title", "head", "base"],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
handler: async (rawArgs) => executeTool(rawArgs, CreatePRSchema, "Failed to create PR", async (args) => {
|
|
517
|
+
const pr = await getOctokit().rest.pulls.create({
|
|
518
|
+
owner: args.owner,
|
|
519
|
+
repo: args.repo,
|
|
520
|
+
title: args.title,
|
|
521
|
+
body: args.body,
|
|
522
|
+
head: args.head,
|
|
523
|
+
base: args.base,
|
|
524
|
+
draft: args.draft,
|
|
525
|
+
maintainer_can_modify: args.maintainer_can_modify,
|
|
526
|
+
});
|
|
527
|
+
return {
|
|
528
|
+
success: true,
|
|
529
|
+
pull_request: {
|
|
530
|
+
number: pr.data.number,
|
|
531
|
+
title: pr.data.title,
|
|
532
|
+
state: pr.data.state,
|
|
533
|
+
html_url: pr.data.html_url,
|
|
534
|
+
draft: pr.data.draft,
|
|
535
|
+
head: pr.data.head.ref,
|
|
536
|
+
base: pr.data.base.ref,
|
|
537
|
+
created_at: pr.data.created_at,
|
|
538
|
+
},
|
|
539
|
+
message: `PR #${pr.data.number} created successfully`,
|
|
540
|
+
};
|
|
541
|
+
}),
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
definition: {
|
|
545
|
+
name: "github_search_prs",
|
|
546
|
+
description: "Search repository pull requests using GitHub Search API and optional qualifiers",
|
|
547
|
+
inputSchema: {
|
|
548
|
+
type: "object",
|
|
549
|
+
properties: {
|
|
550
|
+
owner: { type: "string", description: "Repository owner" },
|
|
551
|
+
repo: { type: "string", description: "Repository name" },
|
|
552
|
+
query: { type: "string", description: "Search text (optional)" },
|
|
553
|
+
state: { type: "string", enum: ["open", "closed", "all"], description: "PR state (optional)" },
|
|
554
|
+
qualifiers: { type: "array", items: { type: "string" }, description: "Extra GitHub search qualifiers like author:foo (optional)" },
|
|
555
|
+
sort: { type: "string", enum: ["created", "updated", "comments", "best-match", "popularity", "long-running"], description: "Sort strategy (optional)" },
|
|
556
|
+
direction: { type: "string", enum: ["asc", "desc"], description: "Sort direction (optional)" },
|
|
557
|
+
page: { type: "number", description: "Page number (optional, default: 1)" },
|
|
558
|
+
per_page: { type: "number", description: "Results per page, max 100 (optional, default: 30)" },
|
|
559
|
+
fetch_all: { type: "boolean", description: "Fetch all pages up to GitHub search limit (optional, default: false)" },
|
|
560
|
+
},
|
|
561
|
+
required: ["owner", "repo"],
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
handler: async (rawArgs) => executeTool(rawArgs, SearchPRsSchema, "Failed to search PRs", async (args) => searchPRsWithPagination(args)),
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
definition: {
|
|
568
|
+
name: "github_merge_pr",
|
|
569
|
+
description: "Merge a pull request with dry-run, expected HEAD SHA check, and confirmation safeguards",
|
|
570
|
+
inputSchema: {
|
|
571
|
+
type: "object",
|
|
572
|
+
properties: {
|
|
573
|
+
owner: { type: "string", description: "Repository owner" },
|
|
574
|
+
repo: { type: "string", description: "Repository name" },
|
|
575
|
+
pull_number: { type: "number", description: "PR number" },
|
|
576
|
+
merge_method: { type: "string", enum: ["merge", "squash", "rebase"], description: "Merge method (optional, default: merge)" },
|
|
577
|
+
commit_title: { type: "string", description: "Custom merge commit title (optional)" },
|
|
578
|
+
commit_message: { type: "string", description: "Custom merge commit message (optional)" },
|
|
579
|
+
dry_run: { type: "boolean", description: "Preview merge without executing (optional, default: false)" },
|
|
580
|
+
expected_head_sha: { type: "string", description: "Optional guard: PR head SHA must match this value" },
|
|
581
|
+
confirm_token: { type: "string", description: "Must be \"CONFIRM\" to execute merge when dry_run=false" },
|
|
582
|
+
},
|
|
583
|
+
required: ["owner", "repo", "pull_number"],
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
handler: async (rawArgs) => executeTool(rawArgs, MergePRSchema, "Failed to merge PR", async (args) => {
|
|
587
|
+
const pr = await getOctokit().rest.pulls.get({
|
|
588
|
+
owner: args.owner,
|
|
589
|
+
repo: args.repo,
|
|
590
|
+
pull_number: args.pull_number,
|
|
591
|
+
});
|
|
592
|
+
if (pr.data.state !== "open") {
|
|
593
|
+
throw new ToolValidationError(`PR #${args.pull_number} is not open`, 409);
|
|
594
|
+
}
|
|
595
|
+
assertExpectedValue(args.expected_head_sha, pr.data.head.sha, "pr head sha");
|
|
596
|
+
if (args.dry_run ?? false) {
|
|
597
|
+
return {
|
|
598
|
+
success: true,
|
|
599
|
+
dry_run: true,
|
|
600
|
+
action: "merge_pr",
|
|
601
|
+
target: {
|
|
602
|
+
pull_number: args.pull_number,
|
|
603
|
+
title: pr.data.title,
|
|
604
|
+
head: pr.data.head.ref,
|
|
605
|
+
base: pr.data.base.ref,
|
|
606
|
+
head_sha: pr.data.head.sha,
|
|
607
|
+
mergeable: pr.data.mergeable,
|
|
608
|
+
mergeable_state: pr.data.mergeable_state,
|
|
609
|
+
},
|
|
610
|
+
message: "Dry run only. No merge executed.",
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
assertConfirmation(args.confirm_token, "PR merge");
|
|
614
|
+
const mergeResult = await getOctokit().rest.pulls.merge({
|
|
615
|
+
owner: args.owner,
|
|
616
|
+
repo: args.repo,
|
|
617
|
+
pull_number: args.pull_number,
|
|
618
|
+
merge_method: args.merge_method,
|
|
619
|
+
commit_title: args.commit_title,
|
|
620
|
+
commit_message: args.commit_message,
|
|
621
|
+
});
|
|
622
|
+
return {
|
|
623
|
+
success: true,
|
|
624
|
+
dry_run: false,
|
|
625
|
+
merged: mergeResult.data.merged,
|
|
626
|
+
sha: mergeResult.data.sha,
|
|
627
|
+
message: mergeResult.data.message,
|
|
628
|
+
};
|
|
629
|
+
}),
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
definition: {
|
|
633
|
+
name: "github_get_pr_diff",
|
|
634
|
+
description: "Get full PR diff with optional output truncation",
|
|
635
|
+
inputSchema: {
|
|
636
|
+
type: "object",
|
|
637
|
+
properties: {
|
|
638
|
+
owner: { type: "string", description: "Repository owner" },
|
|
639
|
+
repo: { type: "string", description: "Repository name" },
|
|
640
|
+
pull_number: { type: "number", description: "PR number" },
|
|
641
|
+
max_chars: { type: "number", description: "Maximum diff characters to return (optional)" },
|
|
642
|
+
},
|
|
643
|
+
required: ["owner", "repo", "pull_number"],
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
handler: async (rawArgs) => executeTool(rawArgs, GetPRDiffSchema, "Failed to get PR diff", async (args) => {
|
|
647
|
+
const diff = await getOctokit().rest.pulls.get({
|
|
648
|
+
owner: args.owner,
|
|
649
|
+
repo: args.repo,
|
|
650
|
+
pull_number: args.pull_number,
|
|
651
|
+
mediaType: {
|
|
652
|
+
format: "diff",
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
const diffText = typeof diff.data === "string" ? diff.data : String(diff.data);
|
|
656
|
+
const maxChars = args.max_chars;
|
|
657
|
+
const truncated = Boolean(maxChars && diffText.length > maxChars);
|
|
658
|
+
return {
|
|
659
|
+
pull_number: args.pull_number,
|
|
660
|
+
max_chars: maxChars,
|
|
661
|
+
total_chars: diffText.length,
|
|
662
|
+
truncated,
|
|
663
|
+
diff: truncated ? diffText.slice(0, maxChars) : diffText,
|
|
664
|
+
};
|
|
665
|
+
}),
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
definition: {
|
|
669
|
+
name: "github_get_pr_files",
|
|
670
|
+
description: "List files changed in a PR with pagination support",
|
|
671
|
+
inputSchema: {
|
|
672
|
+
type: "object",
|
|
673
|
+
properties: {
|
|
674
|
+
owner: { type: "string", description: "Repository owner" },
|
|
675
|
+
repo: { type: "string", description: "Repository name" },
|
|
676
|
+
pull_number: { type: "number", description: "PR number" },
|
|
677
|
+
include_patch: { type: "boolean", description: "Include patch text per file (optional, default: true)" },
|
|
678
|
+
page: { type: "number", description: "Page number (optional, default: 1)" },
|
|
679
|
+
per_page: { type: "number", description: "Results per page, max 100 (optional, default: 30)" },
|
|
680
|
+
fetch_all: { type: "boolean", description: "Fetch all pages (optional, default: false)" },
|
|
681
|
+
},
|
|
682
|
+
required: ["owner", "repo", "pull_number"],
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
handler: async (rawArgs) => executeTool(rawArgs, GetPRFilesSchema, "Failed to get PR files", async (args) => {
|
|
686
|
+
const pagination = resolvePagination(args, {
|
|
687
|
+
page: 1,
|
|
688
|
+
perPage: 30,
|
|
689
|
+
fetchAll: false,
|
|
690
|
+
});
|
|
691
|
+
const includePatch = args.include_patch ?? true;
|
|
692
|
+
const files = await collectPaginated(pagination, async (page, perPage) => {
|
|
693
|
+
const response = await getOctokit().rest.pulls.listFiles({
|
|
694
|
+
owner: args.owner,
|
|
695
|
+
repo: args.repo,
|
|
696
|
+
pull_number: args.pull_number,
|
|
697
|
+
page,
|
|
698
|
+
per_page: perPage,
|
|
699
|
+
});
|
|
700
|
+
return response.data;
|
|
701
|
+
});
|
|
702
|
+
return {
|
|
703
|
+
pull_number: args.pull_number,
|
|
704
|
+
total_files: files.items.length,
|
|
705
|
+
files: files.items.map((file) => ({
|
|
706
|
+
filename: file.filename,
|
|
707
|
+
status: file.status,
|
|
708
|
+
additions: file.additions,
|
|
709
|
+
deletions: file.deletions,
|
|
710
|
+
changes: file.changes,
|
|
711
|
+
blob_url: file.blob_url,
|
|
712
|
+
raw_url: file.raw_url,
|
|
713
|
+
patch: includePatch ? file.patch : undefined,
|
|
714
|
+
})),
|
|
715
|
+
pagination: {
|
|
716
|
+
page: files.page,
|
|
717
|
+
per_page: files.per_page,
|
|
718
|
+
fetch_all: files.fetch_all,
|
|
719
|
+
pages_fetched: files.pages_fetched,
|
|
720
|
+
},
|
|
721
|
+
};
|
|
722
|
+
}),
|
|
723
|
+
},
|
|
724
|
+
];
|