@perrylink/dsh-github 0.4.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/LICENSE +201 -0
- package/README.es.md +256 -0
- package/README.hi.md +256 -0
- package/README.md +257 -0
- package/README.pt.md +256 -0
- package/README.zh-CN.md +254 -0
- package/cordis.patch.yml +9 -0
- package/lib/approval-gate.d.ts +25 -0
- package/lib/approval-gate.d.ts.map +1 -0
- package/lib/approval-gate.js +75 -0
- package/lib/approval-gate.js.map +1 -0
- package/lib/commands.d.ts +12 -0
- package/lib/commands.d.ts.map +1 -0
- package/lib/commands.js +228 -0
- package/lib/commands.js.map +1 -0
- package/lib/config.d.ts +60 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +64 -0
- package/lib/config.js.map +1 -0
- package/lib/credential.d.ts +42 -0
- package/lib/credential.d.ts.map +1 -0
- package/lib/credential.js +80 -0
- package/lib/credential.js.map +1 -0
- package/lib/git.d.ts +52 -0
- package/lib/git.d.ts.map +1 -0
- package/lib/git.js +113 -0
- package/lib/git.js.map +1 -0
- package/lib/github.d.ts +66 -0
- package/lib/github.d.ts.map +1 -0
- package/lib/github.js +153 -0
- package/lib/github.js.map +1 -0
- package/lib/index.d.ts +55 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +44 -0
- package/lib/index.js.map +1 -0
- package/lib/jobs.d.ts +34 -0
- package/lib/jobs.d.ts.map +1 -0
- package/lib/jobs.js +255 -0
- package/lib/jobs.js.map +1 -0
- package/lib/present.d.ts +254 -0
- package/lib/present.d.ts.map +1 -0
- package/lib/present.js +149 -0
- package/lib/present.js.map +1 -0
- package/lib/review.d.ts +53 -0
- package/lib/review.d.ts.map +1 -0
- package/lib/review.js +158 -0
- package/lib/review.js.map +1 -0
- package/lib/state.d.ts +96 -0
- package/lib/state.d.ts.map +1 -0
- package/lib/state.js +86 -0
- package/lib/state.js.map +1 -0
- package/lib/tools.d.ts +21 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +937 -0
- package/lib/tools.js.map +1 -0
- package/lib/types.d.ts +147 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +78 -0
package/lib/tools.js
ADDED
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-facing tool surface of dsh-github.
|
|
3
|
+
*
|
|
4
|
+
* Eight tools: pr_create / review_post / issue_open / issue_comment /
|
|
5
|
+
* issue_close (writes, approval-gated by the tools/pre-execute listener in
|
|
6
|
+
* approval-gate.ts) and gh_review / gh_issue / gh_search (concurrency-safe
|
|
7
|
+
* reads). Every execute returns only the canonical JSON value declared by its
|
|
8
|
+
* output schema; infrastructure failures throw so the registry marks them
|
|
9
|
+
* isError. Tokens never appear in canonical values, rendered content, or
|
|
10
|
+
* thrown messages. Rate-limit facts ride every result, including errors.
|
|
11
|
+
* @module dsh-github/tools
|
|
12
|
+
*/
|
|
13
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
14
|
+
import { GithubError } from "./github.js";
|
|
15
|
+
import { readGitState } from "./git.js";
|
|
16
|
+
import { analyzeDiff, formatPostBody, parseDiffStats } from "./review.js";
|
|
17
|
+
import { rateLimitValue } from "./state.js";
|
|
18
|
+
import { ghIssueCall, ghIssueResult, ghReviewCall, ghReviewResult, ghSearchCall, ghSearchResult, identityMeta, issueCloseCall, issueCloseResult, issueCommentCall, issueCommentResult, issueOpenCall, issueOpenResult, prCreateCall, prCreateResult, reviewPostCall, reviewPostResult, } from "./present.js";
|
|
19
|
+
const RATE_LIMIT_PROPERTIES = {
|
|
20
|
+
remaining: { oneOf: [{ type: 'number' }, { type: 'null' }] },
|
|
21
|
+
resetAt: { oneOf: [{ type: 'number' }, { type: 'null' }] },
|
|
22
|
+
};
|
|
23
|
+
const RATE_LIMIT_SCHEMA = {
|
|
24
|
+
type: 'object',
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
properties: RATE_LIMIT_PROPERTIES,
|
|
27
|
+
required: true,
|
|
28
|
+
};
|
|
29
|
+
const ERROR_SCHEMA = {
|
|
30
|
+
type: 'object',
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
properties: {
|
|
33
|
+
status: { type: 'string', required: true, const: 'error' },
|
|
34
|
+
code: { type: 'string', required: true },
|
|
35
|
+
message: { type: 'string', required: true },
|
|
36
|
+
guidance: { type: 'string' },
|
|
37
|
+
rateLimit: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
properties: RATE_LIMIT_PROPERTIES,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
/** Domain error canonical value, used by every tool. */
|
|
45
|
+
export function errorValue(code, message, guidance, rateLimit) {
|
|
46
|
+
return { status: 'error', code, message, ...guidance !== undefined ? { guidance } : {}, ...rateLimit !== undefined ? { rateLimit } : {} };
|
|
47
|
+
}
|
|
48
|
+
/** Convert a GitHub API failure to the error variant; rethrow the rest. */
|
|
49
|
+
function githubErrorValue(error) {
|
|
50
|
+
if (error instanceof GithubError) {
|
|
51
|
+
return errorValue('github-api', `GitHub API ${error.status}: ${error.message}`, undefined, {
|
|
52
|
+
remaining: error.rateLimit.remaining,
|
|
53
|
+
resetAt: error.rateLimit.resetAt,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
/** Stable code for a failed supplementary section fetch. */
|
|
59
|
+
function sectionErrorCode(error) {
|
|
60
|
+
if (error instanceof GithubError)
|
|
61
|
+
return `github-api-${error.status}`;
|
|
62
|
+
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted')))
|
|
63
|
+
return 'aborted';
|
|
64
|
+
return 'fetch-failed';
|
|
65
|
+
}
|
|
66
|
+
/** Cap text to `maxChars` characters and report the truncation. */
|
|
67
|
+
function capText(text, maxChars) {
|
|
68
|
+
return text.length > maxChars ? { text: text.slice(0, maxChars), truncated: true } : { text, truncated: false };
|
|
69
|
+
}
|
|
70
|
+
/** First line of a text block, for comment-item titles. */
|
|
71
|
+
function firstLineOf(text) {
|
|
72
|
+
return text.split('\n')[0] ?? '';
|
|
73
|
+
}
|
|
74
|
+
/** Fields gh_review can select; omitted means all. */
|
|
75
|
+
const REVIEW_FIELDS = ['metadata', 'diff', 'comments', 'ci', 'findings'];
|
|
76
|
+
/** `pr_create`: create a pull request (write; approval-gated upstream). */
|
|
77
|
+
export function prCreateTool(state) {
|
|
78
|
+
return defineTool({
|
|
79
|
+
name: 'pr_create',
|
|
80
|
+
description: 'Create a GitHub pull request. Requires approval. Does NOT commit or push local changes — '
|
|
81
|
+
+ 'commit and push via bash first when the head branch is not on the remote. Returns the PR URL and number.',
|
|
82
|
+
parameters: {
|
|
83
|
+
title: { type: 'string', required: true, description: 'PR title.' },
|
|
84
|
+
body: { type: 'string', description: 'PR description body.' },
|
|
85
|
+
base: { type: 'string', description: 'Target branch. Defaults to the repository default branch.' },
|
|
86
|
+
head: { type: 'string', description: 'Source branch. Defaults to the current git branch.' },
|
|
87
|
+
draft: { type: 'boolean', description: 'Create as a draft PR.' },
|
|
88
|
+
ownerRepo: { type: 'string', description: 'Target repository as owner/repo. Defaults to configured or git origin.' },
|
|
89
|
+
},
|
|
90
|
+
output: {
|
|
91
|
+
schema: {
|
|
92
|
+
oneOf: [{
|
|
93
|
+
type: 'object',
|
|
94
|
+
additionalProperties: false,
|
|
95
|
+
properties: {
|
|
96
|
+
status: { type: 'string', required: true, const: 'created' },
|
|
97
|
+
url: { type: 'string', required: true },
|
|
98
|
+
number: { type: 'integer', required: true },
|
|
99
|
+
title: { type: 'string', required: true },
|
|
100
|
+
state: { type: 'string', required: true },
|
|
101
|
+
draft: { type: 'boolean', required: true },
|
|
102
|
+
base: { type: 'string', required: true },
|
|
103
|
+
head: { type: 'string', required: true },
|
|
104
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
105
|
+
},
|
|
106
|
+
}, ERROR_SCHEMA],
|
|
107
|
+
},
|
|
108
|
+
render: (_args, value) => {
|
|
109
|
+
if (value.status === 'error') {
|
|
110
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
111
|
+
}
|
|
112
|
+
return [{ type: 'text', text: `created pull request #${value.number}: ${value.url} (${value.base} ← ${value.head}${value.draft ? ', draft' : ''})` }];
|
|
113
|
+
},
|
|
114
|
+
presentationMeta: identityMeta,
|
|
115
|
+
},
|
|
116
|
+
isConcurrencySafe: () => false,
|
|
117
|
+
presentCall: prCreateCall,
|
|
118
|
+
presentResult: prCreateResult,
|
|
119
|
+
async execute(args, exec) {
|
|
120
|
+
if (args.title.trim().length === 0)
|
|
121
|
+
return errorValue('invalid-args', 'title must not be empty');
|
|
122
|
+
const repo = await state.resolveRepo(args.ownerRepo, exec.signal);
|
|
123
|
+
if (!repo.ok)
|
|
124
|
+
return errorValue(repo.code, repo.message, repo.guidance);
|
|
125
|
+
let head = args.head?.trim();
|
|
126
|
+
if (head === undefined || head.length === 0) {
|
|
127
|
+
const git = await readGitState(state.workspaceDir, state.runGit, exec.signal, state.apiHost);
|
|
128
|
+
if (git.branch === null)
|
|
129
|
+
return errorValue('no-head', 'could not determine the head branch', 'Pass `head` explicitly or run inside a git checkout.');
|
|
130
|
+
head = git.branch;
|
|
131
|
+
}
|
|
132
|
+
const token = await state.resolveToken(exec.signal);
|
|
133
|
+
if (!token.ok)
|
|
134
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
135
|
+
const client = state.client(token.token.value);
|
|
136
|
+
let base = args.base?.trim();
|
|
137
|
+
if (base === undefined || base.length === 0) {
|
|
138
|
+
try {
|
|
139
|
+
const repoInfo = await client.requestJson('GET', `/repos/${repo.repo}`, { signal: exec.signal });
|
|
140
|
+
base = repoInfo.data.default_branch;
|
|
141
|
+
if (base === undefined || base.length === 0)
|
|
142
|
+
return errorValue('no-base', 'the repository reports no default branch', 'Pass `base` explicitly.');
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return githubErrorValue(error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const created = await client.requestJson('POST', `/repos/${repo.repo}/pulls`, {
|
|
150
|
+
signal: exec.signal,
|
|
151
|
+
body: {
|
|
152
|
+
title: args.title,
|
|
153
|
+
...args.body !== undefined ? { body: args.body } : {},
|
|
154
|
+
head,
|
|
155
|
+
base,
|
|
156
|
+
draft: args.draft === true,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const data = created.data;
|
|
160
|
+
return {
|
|
161
|
+
status: 'created',
|
|
162
|
+
url: data.html_url,
|
|
163
|
+
number: data.number,
|
|
164
|
+
title: data.title,
|
|
165
|
+
state: data.state,
|
|
166
|
+
draft: data.draft === true,
|
|
167
|
+
base: data.base?.ref ?? base,
|
|
168
|
+
head: data.head?.ref ?? head,
|
|
169
|
+
rateLimit: rateLimitValue(created.rateLimit),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return githubErrorValue(error);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** `gh_review`: read a PR's metadata, diff, comments, CI, and findings. */
|
|
179
|
+
export function ghReviewTool(state) {
|
|
180
|
+
return defineTool({
|
|
181
|
+
name: 'gh_review',
|
|
182
|
+
description: 'Read a GitHub pull request: metadata, unified diff, review comments, CI check status, and '
|
|
183
|
+
+ 'static-analysis findings. Read-only and concurrency-safe. `pr` accepts a number, "#number", '
|
|
184
|
+
+ '"owner/repo#number", or a pull-request URL. Section fetch failures are reported per section '
|
|
185
|
+
+ '(diff.error / comments.error / ci.error); the rest of the summary stays intact.',
|
|
186
|
+
parameters: {
|
|
187
|
+
pr: { type: 'string', required: true, description: 'PR number, #number, owner/repo#number, or pull URL.' },
|
|
188
|
+
fields: { type: 'array', items: { type: 'string', enum: [...REVIEW_FIELDS] }, description: 'Sections to fetch. Omit for all.' },
|
|
189
|
+
maxDiffChars: { type: 'number', description: 'Cap for the diff text. Defaults to the plugin config.' },
|
|
190
|
+
},
|
|
191
|
+
output: {
|
|
192
|
+
schema: {
|
|
193
|
+
oneOf: [{
|
|
194
|
+
type: 'object',
|
|
195
|
+
additionalProperties: false,
|
|
196
|
+
properties: {
|
|
197
|
+
repo: { type: 'string', required: true },
|
|
198
|
+
number: { type: 'integer', required: true },
|
|
199
|
+
title: { type: 'string', required: true },
|
|
200
|
+
state: { type: 'string', required: true },
|
|
201
|
+
author: { type: 'string', required: true },
|
|
202
|
+
url: { type: 'string', required: true },
|
|
203
|
+
additions: { type: 'integer', required: true },
|
|
204
|
+
deletions: { type: 'integer', required: true },
|
|
205
|
+
base: { type: 'string', required: true },
|
|
206
|
+
head: { type: 'string', required: true },
|
|
207
|
+
ci: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
additionalProperties: false,
|
|
210
|
+
required: true,
|
|
211
|
+
properties: {
|
|
212
|
+
summary: { type: 'string', required: true },
|
|
213
|
+
status: { type: 'string' },
|
|
214
|
+
conclusion: { type: 'string' },
|
|
215
|
+
error: { type: 'string' },
|
|
216
|
+
runs: {
|
|
217
|
+
type: 'array',
|
|
218
|
+
items: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
additionalProperties: false,
|
|
221
|
+
properties: {
|
|
222
|
+
name: { type: 'string', required: true },
|
|
223
|
+
status: { type: 'string', required: true },
|
|
224
|
+
conclusion: { type: 'string' },
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
comments: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
additionalProperties: false,
|
|
233
|
+
required: true,
|
|
234
|
+
properties: {
|
|
235
|
+
items: {
|
|
236
|
+
type: 'array',
|
|
237
|
+
required: true,
|
|
238
|
+
items: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
additionalProperties: false,
|
|
241
|
+
properties: {
|
|
242
|
+
id: { type: 'integer', required: true },
|
|
243
|
+
user: { type: 'string', required: true },
|
|
244
|
+
path: { type: 'string' },
|
|
245
|
+
line: { type: 'integer' },
|
|
246
|
+
body: { type: 'string', required: true },
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
error: { type: 'string' },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
findings: {
|
|
254
|
+
type: 'array',
|
|
255
|
+
items: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
additionalProperties: false,
|
|
258
|
+
properties: {
|
|
259
|
+
file: { type: 'string', required: true },
|
|
260
|
+
line: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
|
|
261
|
+
severity: { type: 'string', required: true, enum: ['info', 'warning', 'error'] },
|
|
262
|
+
rule: { type: 'string', required: true },
|
|
263
|
+
message: { type: 'string', required: true },
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
diff: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
additionalProperties: false,
|
|
270
|
+
required: true,
|
|
271
|
+
properties: {
|
|
272
|
+
length: { type: 'integer', required: true },
|
|
273
|
+
truncated: { type: 'boolean', required: true },
|
|
274
|
+
excerpt: { type: 'string', required: true },
|
|
275
|
+
text: { type: 'string', required: true },
|
|
276
|
+
error: { type: 'string' },
|
|
277
|
+
files: {
|
|
278
|
+
type: 'array',
|
|
279
|
+
items: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
additionalProperties: false,
|
|
282
|
+
properties: {
|
|
283
|
+
path: { type: 'string', required: true },
|
|
284
|
+
added: { type: 'integer', required: true },
|
|
285
|
+
removed: { type: 'integer', required: true },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
292
|
+
},
|
|
293
|
+
}, ERROR_SCHEMA],
|
|
294
|
+
},
|
|
295
|
+
render: (_args, value) => {
|
|
296
|
+
if ('status' in value) {
|
|
297
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
298
|
+
}
|
|
299
|
+
return renderPrSummary(value);
|
|
300
|
+
},
|
|
301
|
+
presentationMeta: identityMeta,
|
|
302
|
+
},
|
|
303
|
+
isConcurrencySafe: () => true,
|
|
304
|
+
presentCall: ghReviewCall,
|
|
305
|
+
presentResult: ghReviewResult,
|
|
306
|
+
async execute(args, exec) {
|
|
307
|
+
const ref = state.parsePrRef(args.pr);
|
|
308
|
+
if (ref === null)
|
|
309
|
+
return errorValue('invalid-pr', `"${args.pr}" is not a PR reference`, 'Use a number, "#number", "owner/repo#number", or a pull URL.');
|
|
310
|
+
const repoResult = ref.repo !== undefined ? { ok: true, repo: ref.repo } : await state.resolveRepo(undefined, exec.signal);
|
|
311
|
+
if (!repoResult.ok)
|
|
312
|
+
return errorValue(repoResult.code, repoResult.message, repoResult.guidance);
|
|
313
|
+
const token = await state.resolveToken(exec.signal);
|
|
314
|
+
if (!token.ok)
|
|
315
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
316
|
+
const client = state.client(token.token.value);
|
|
317
|
+
const fields = new Set(args.fields ?? REVIEW_FIELDS);
|
|
318
|
+
const maxDiffChars = args.maxDiffChars !== undefined && args.maxDiffChars > 0 ? Math.floor(args.maxDiffChars) : state.config.maxDiffChars;
|
|
319
|
+
let payload;
|
|
320
|
+
let rateLimit;
|
|
321
|
+
try {
|
|
322
|
+
const metadataResponse = await client.requestJson('GET', `/repos/${repoResult.repo}/pulls/${ref.number}`, { signal: exec.signal });
|
|
323
|
+
payload = metadataResponse.data;
|
|
324
|
+
rateLimit = rateLimitValue(metadataResponse.rateLimit);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
return githubErrorValue(error);
|
|
328
|
+
}
|
|
329
|
+
let diffText = '';
|
|
330
|
+
let diffTruncated = false;
|
|
331
|
+
let diffError;
|
|
332
|
+
if (fields.has('diff') || fields.has('findings')) {
|
|
333
|
+
try {
|
|
334
|
+
const capped = capText((await client.requestText('GET', `/repos/${repoResult.repo}/pulls/${ref.number}`, { signal: exec.signal })).text, maxDiffChars);
|
|
335
|
+
diffText = capped.text;
|
|
336
|
+
diffTruncated = capped.truncated;
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
diffError = sectionErrorCode(error);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
let comments = [];
|
|
343
|
+
let commentsError;
|
|
344
|
+
if (fields.has('comments')) {
|
|
345
|
+
try {
|
|
346
|
+
comments = (await client.requestJson('GET', `/repos/${repoResult.repo}/pulls/${ref.number}/comments?per_page=${state.config.maxComments}`, { signal: exec.signal })).data;
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
commentsError = sectionErrorCode(error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
let ci = { summary: 'unknown' };
|
|
353
|
+
if (fields.has('ci')) {
|
|
354
|
+
try {
|
|
355
|
+
const runs = (await client.requestJson('GET', `/repos/${repoResult.repo}/commits/${payload.head?.sha ?? ''}/check-runs`, { signal: exec.signal })).data.check_runs ?? [];
|
|
356
|
+
ci = summarizeChecks(runs);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
ci = { summary: 'unknown', error: sectionErrorCode(error) };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const findings = fields.has('findings') ? analyzeDiff(diffText, maxDiffChars).findings : [];
|
|
363
|
+
const value = {
|
|
364
|
+
repo: repoResult.repo,
|
|
365
|
+
number: payload.number,
|
|
366
|
+
title: payload.title,
|
|
367
|
+
state: payload.state,
|
|
368
|
+
author: payload.user?.login ?? 'unknown',
|
|
369
|
+
url: payload.html_url,
|
|
370
|
+
additions: payload.additions,
|
|
371
|
+
deletions: payload.deletions,
|
|
372
|
+
base: payload.base?.ref ?? '',
|
|
373
|
+
head: payload.head?.ref ?? '',
|
|
374
|
+
ci,
|
|
375
|
+
comments: {
|
|
376
|
+
items: comments.map(comment => ({
|
|
377
|
+
id: comment.id,
|
|
378
|
+
user: comment.user?.login ?? 'unknown',
|
|
379
|
+
...comment.path !== null && comment.path !== undefined ? { path: comment.path } : {},
|
|
380
|
+
...comment.line !== null && comment.line !== undefined ? { line: comment.line } : {},
|
|
381
|
+
body: comment.body ?? '',
|
|
382
|
+
})),
|
|
383
|
+
...commentsError !== undefined ? { error: commentsError } : {},
|
|
384
|
+
},
|
|
385
|
+
findings: findings.map(finding => ({ ...finding })),
|
|
386
|
+
diff: {
|
|
387
|
+
length: diffText.length,
|
|
388
|
+
truncated: diffTruncated,
|
|
389
|
+
excerpt: diffText.slice(0, state.config.renderExcerptChars),
|
|
390
|
+
text: diffText,
|
|
391
|
+
files: diffError !== undefined ? [] : parseDiffStats(diffText, maxDiffChars),
|
|
392
|
+
...diffError !== undefined ? { error: diffError } : {},
|
|
393
|
+
},
|
|
394
|
+
rateLimit,
|
|
395
|
+
};
|
|
396
|
+
return value;
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
function renderPrSummary(value) {
|
|
401
|
+
const findings = value.findings ?? [];
|
|
402
|
+
const commentItems = value.comments?.items ?? [];
|
|
403
|
+
const files = value.diff.files ?? [];
|
|
404
|
+
const lines = [
|
|
405
|
+
`PR #${value.number} "${value.title}" (${value.state}) by ${value.author}`,
|
|
406
|
+
`${value.repo} · ${value.base} ← ${value.head} · +${value.additions} −${value.deletions} · ${value.url}`,
|
|
407
|
+
`CI: ${value.ci.summary}${value.ci.error !== undefined ? ` (unavailable: ${value.ci.error})` : ''}`,
|
|
408
|
+
`Comments (external GitHub content): ${commentItems.length} review comment(s); static findings: ${findings.length}`,
|
|
409
|
+
];
|
|
410
|
+
if (value.comments?.error !== undefined)
|
|
411
|
+
lines.push(`Comment fetch failed: ${value.comments.error}`);
|
|
412
|
+
for (const finding of findings.slice(0, 10)) {
|
|
413
|
+
lines.push(`- [${finding.severity}] ${finding.rule} ${finding.file}${finding.line !== null && finding.line !== undefined ? `:${finding.line}` : ''}: ${finding.message}`);
|
|
414
|
+
}
|
|
415
|
+
if (findings.length > 10)
|
|
416
|
+
lines.push(`… ${findings.length - 10} more findings`);
|
|
417
|
+
lines.push(`Diff: ${value.diff.length} chars${value.diff.truncated ? ' (truncated)' : ''} across ${files.length} file(s)${value.diff.error !== undefined ? ` (unavailable: ${value.diff.error})` : ''}`);
|
|
418
|
+
if (value.diff.excerpt.length > 0)
|
|
419
|
+
lines.push(`Diff excerpt:\n${value.diff.excerpt}`);
|
|
420
|
+
if (value.rateLimit?.remaining !== null && value.rateLimit?.remaining !== undefined)
|
|
421
|
+
lines.push(`GitHub rate limit remaining: ${value.rateLimit.remaining}`);
|
|
422
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
423
|
+
}
|
|
424
|
+
function summarizeChecks(runs) {
|
|
425
|
+
const normalized = runs.map(run => ({
|
|
426
|
+
name: run.name ?? 'unnamed check',
|
|
427
|
+
status: run.status ?? 'unknown',
|
|
428
|
+
...run.conclusion !== null && run.conclusion !== undefined ? { conclusion: run.conclusion } : {},
|
|
429
|
+
}));
|
|
430
|
+
if (normalized.length === 0)
|
|
431
|
+
return { summary: 'no checks reported' };
|
|
432
|
+
const pending = normalized.filter(run => run.status !== 'completed').length;
|
|
433
|
+
const failed = normalized.filter(run => run.conclusion !== undefined && run.conclusion !== 'success' && run.conclusion !== 'neutral' && run.conclusion !== 'skipped').length;
|
|
434
|
+
const status = pending > 0 ? 'in-progress' : 'completed';
|
|
435
|
+
const conclusion = pending > 0 ? undefined : failed > 0 ? 'failure' : 'success';
|
|
436
|
+
return { summary: `${normalized.length} check(s), ${pending} pending, ${failed} failed`, status, ...conclusion !== undefined ? { conclusion } : {}, runs: normalized };
|
|
437
|
+
}
|
|
438
|
+
/** `review_post`: publish a review job's drafted comment (write; approval-gated upstream). */
|
|
439
|
+
export function reviewPostTool(state) {
|
|
440
|
+
return defineTool({
|
|
441
|
+
name: 'review_post',
|
|
442
|
+
description: 'Post the review comment drafted by a completed background review job (started with /review). '
|
|
443
|
+
+ 'Requires approval. mode "summary" posts one PR issue-level comment (default); mode "inline" posts a '
|
|
444
|
+
+ 'line-anchored review against the PR head commit. Pass `body` to override the drafted comment.',
|
|
445
|
+
parameters: {
|
|
446
|
+
jobId: { type: 'string', required: true, description: 'Id of a completed review job.' },
|
|
447
|
+
mode: { type: 'string', enum: ['summary', 'inline'], description: 'Posting mode. Defaults to summary.' },
|
|
448
|
+
body: { type: 'string', description: 'Override the drafted comment body.' },
|
|
449
|
+
},
|
|
450
|
+
output: {
|
|
451
|
+
schema: {
|
|
452
|
+
oneOf: [{
|
|
453
|
+
type: 'object',
|
|
454
|
+
additionalProperties: false,
|
|
455
|
+
properties: {
|
|
456
|
+
status: { type: 'string', required: true, const: 'posted' },
|
|
457
|
+
mode: { type: 'string', required: true, enum: ['summary', 'inline'] },
|
|
458
|
+
url: { type: 'string', required: true },
|
|
459
|
+
commentId: { type: 'integer' },
|
|
460
|
+
reviewId: { type: 'integer' },
|
|
461
|
+
findings: { type: 'integer', required: true },
|
|
462
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
463
|
+
},
|
|
464
|
+
}, ERROR_SCHEMA],
|
|
465
|
+
},
|
|
466
|
+
render: (_args, value) => {
|
|
467
|
+
if (value.status === 'error')
|
|
468
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
469
|
+
return value.mode === 'inline'
|
|
470
|
+
? [{ type: 'text', text: `posted inline review with ${value.findings} finding(s): ${value.url}` }]
|
|
471
|
+
: [{ type: 'text', text: `posted review comment with ${value.findings} finding(s): ${value.url}` }];
|
|
472
|
+
},
|
|
473
|
+
presentationMeta: identityMeta,
|
|
474
|
+
},
|
|
475
|
+
isConcurrencySafe: () => false,
|
|
476
|
+
presentCall: reviewPostCall,
|
|
477
|
+
presentResult: reviewPostResult,
|
|
478
|
+
async execute(args, exec) {
|
|
479
|
+
const record = state.records.get(args.jobId);
|
|
480
|
+
if (record === undefined)
|
|
481
|
+
return errorValue('unknown-job', `no review job "${args.jobId}"`, 'List jobs with job_list and pick a github-review job id.');
|
|
482
|
+
if (record.status !== 'completed' || record.report === null) {
|
|
483
|
+
return errorValue('job-not-completed', `review job "${args.jobId}" has not completed`, 'Wait for the completion notice, then post.');
|
|
484
|
+
}
|
|
485
|
+
const mode = args.mode ?? 'summary';
|
|
486
|
+
const override = args.body !== undefined ? args.body.trim() : '';
|
|
487
|
+
if (args.body !== undefined && override.length === 0)
|
|
488
|
+
return errorValue('invalid-args', 'body must not be empty');
|
|
489
|
+
const token = await state.resolveToken(exec.signal);
|
|
490
|
+
if (!token.ok)
|
|
491
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
492
|
+
const client = state.client(token.token.value);
|
|
493
|
+
const findings = record.report.findings;
|
|
494
|
+
try {
|
|
495
|
+
if (mode === 'inline') {
|
|
496
|
+
if (record.headSha === undefined) {
|
|
497
|
+
return errorValue('no-head-sha', `review job "${args.jobId}" has no head commit captured`, 'Re-run /review for this PR; inline comments need the head-commit SHA.');
|
|
498
|
+
}
|
|
499
|
+
const inline = findings.filter(finding => finding.line !== null);
|
|
500
|
+
const rest = findings.filter(finding => finding.line === null);
|
|
501
|
+
const reviewBody = override.length > 0 ? override : formatPostBody(rest, record.report.truncated);
|
|
502
|
+
const posted = await client.requestJson('POST', `/repos/${record.repo}/pulls/${record.pr}/reviews`, {
|
|
503
|
+
signal: exec.signal,
|
|
504
|
+
body: {
|
|
505
|
+
body: reviewBody,
|
|
506
|
+
event: 'COMMENT',
|
|
507
|
+
comments: inline.map(finding => ({
|
|
508
|
+
path: finding.file,
|
|
509
|
+
line: finding.line,
|
|
510
|
+
body: `**${finding.severity}** \`${finding.rule}\`: ${finding.message}`,
|
|
511
|
+
})),
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
return { status: 'posted', mode: 'inline', url: posted.data.html_url, reviewId: posted.data.id, findings: findings.length, rateLimit: rateLimitValue(posted.rateLimit) };
|
|
515
|
+
}
|
|
516
|
+
const posted = await client.requestJson('POST', `/repos/${record.repo}/issues/${record.pr}/comments`, { signal: exec.signal, body: { body: override.length > 0 ? override : record.report.postBody } });
|
|
517
|
+
return { status: 'posted', mode: 'summary', url: posted.data.html_url, commentId: posted.data.id, findings: findings.length, rateLimit: rateLimitValue(posted.rateLimit) };
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
return githubErrorValue(error);
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
/** `gh_issue`: read issues (list / get / comments). Read-only. */
|
|
526
|
+
export function ghIssueTool(state) {
|
|
527
|
+
return defineTool({
|
|
528
|
+
name: 'gh_issue',
|
|
529
|
+
description: 'Read GitHub issues. `action`: "list" repository issues (filter by state; pull requests are '
|
|
530
|
+
+ 'marked with kind "pr"), "get" one issue by number, or "comments" for one issue\'s comments. Read-only '
|
|
531
|
+
+ 'and concurrency-safe. Use issue_open to create.',
|
|
532
|
+
parameters: {
|
|
533
|
+
action: { type: 'string', required: true, enum: ['list', 'get', 'comments'], description: 'Which read to perform.' },
|
|
534
|
+
ownerRepo: { type: 'string', description: 'Repository as owner/repo. Defaults to configured or git origin.' },
|
|
535
|
+
issueNumber: { type: 'integer', description: 'Issue number; required for get and comments.' },
|
|
536
|
+
state: { type: 'string', enum: ['open', 'closed', 'all'], description: 'Filter for list. Defaults to open.' },
|
|
537
|
+
limit: { type: 'integer', description: 'Max items for list. Defaults to 30, capped at 100.' },
|
|
538
|
+
},
|
|
539
|
+
output: {
|
|
540
|
+
schema: {
|
|
541
|
+
oneOf: [{
|
|
542
|
+
type: 'object',
|
|
543
|
+
additionalProperties: false,
|
|
544
|
+
properties: {
|
|
545
|
+
repo: { type: 'string', required: true },
|
|
546
|
+
action: { type: 'string', required: true, enum: ['list', 'get', 'comments'] },
|
|
547
|
+
total: { type: 'integer', required: true },
|
|
548
|
+
items: {
|
|
549
|
+
type: 'array',
|
|
550
|
+
items: {
|
|
551
|
+
type: 'object',
|
|
552
|
+
additionalProperties: false,
|
|
553
|
+
properties: {
|
|
554
|
+
number: { type: 'integer', required: true },
|
|
555
|
+
title: { type: 'string', required: true },
|
|
556
|
+
state: { type: 'string', required: true },
|
|
557
|
+
kind: { type: 'string', required: true, enum: ['issue', 'pr', 'comment'] },
|
|
558
|
+
author: { type: 'string', required: true },
|
|
559
|
+
url: { type: 'string', required: true },
|
|
560
|
+
comments: { type: 'integer', required: true },
|
|
561
|
+
createdAt: { type: 'string', required: true },
|
|
562
|
+
body: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
567
|
+
},
|
|
568
|
+
}, ERROR_SCHEMA],
|
|
569
|
+
},
|
|
570
|
+
render: (_args, value) => {
|
|
571
|
+
if ('status' in value) {
|
|
572
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
573
|
+
}
|
|
574
|
+
return [{
|
|
575
|
+
type: 'text',
|
|
576
|
+
text: (value.items ?? []).length === 0
|
|
577
|
+
? `no issues in ${value.repo}`
|
|
578
|
+
: (value.items ?? []).map(item => `#${item.number} ${item.title} [${item.kind}/${item.state}] by ${item.author} — ${item.url}`).join('\n'),
|
|
579
|
+
}];
|
|
580
|
+
},
|
|
581
|
+
presentationMeta: identityMeta,
|
|
582
|
+
},
|
|
583
|
+
isConcurrencySafe: () => true,
|
|
584
|
+
presentCall: ghIssueCall,
|
|
585
|
+
presentResult: ghIssueResult,
|
|
586
|
+
async execute(args, exec) {
|
|
587
|
+
if ((args.action === 'get' || args.action === 'comments') && args.issueNumber === undefined) {
|
|
588
|
+
return errorValue('invalid-args', `action "${args.action}" requires issueNumber`);
|
|
589
|
+
}
|
|
590
|
+
const repoResult = await state.resolveRepo(args.ownerRepo, exec.signal);
|
|
591
|
+
if (!repoResult.ok)
|
|
592
|
+
return errorValue(repoResult.code, repoResult.message, repoResult.guidance);
|
|
593
|
+
const token = await state.resolveToken(exec.signal);
|
|
594
|
+
if (!token.ok)
|
|
595
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
596
|
+
const client = state.client(token.token.value);
|
|
597
|
+
let raw = [];
|
|
598
|
+
let rawComments = null;
|
|
599
|
+
let rateLimit = { remaining: null, resetAt: null };
|
|
600
|
+
try {
|
|
601
|
+
if (args.action === 'list') {
|
|
602
|
+
const limit = Math.min(Math.max(args.limit ?? 30, 1), 100);
|
|
603
|
+
const listResponse = await client.requestJson('GET', `/repos/${repoResult.repo}/issues?state=${args.state ?? 'open'}&per_page=${limit}`, { signal: exec.signal });
|
|
604
|
+
raw = listResponse.data;
|
|
605
|
+
rateLimit = rateLimitValue(listResponse.rateLimit);
|
|
606
|
+
}
|
|
607
|
+
else if (args.action === 'comments') {
|
|
608
|
+
const number = args.issueNumber;
|
|
609
|
+
const commentsResponse = await client.requestJson('GET', `/repos/${repoResult.repo}/issues/${number}/comments`, { signal: exec.signal });
|
|
610
|
+
rawComments = commentsResponse.data;
|
|
611
|
+
rateLimit = rateLimitValue(commentsResponse.rateLimit);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
const number = args.issueNumber;
|
|
615
|
+
const oneResponse = await client.requestJson('GET', `/repos/${repoResult.repo}/issues/${number}`, { signal: exec.signal });
|
|
616
|
+
raw = [oneResponse.data];
|
|
617
|
+
rateLimit = rateLimitValue(oneResponse.rateLimit);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
return githubErrorValue(error);
|
|
622
|
+
}
|
|
623
|
+
const items = rawComments !== null
|
|
624
|
+
? rawComments.map(comment => ({
|
|
625
|
+
number: comment.id,
|
|
626
|
+
title: firstLineOf(comment.body ?? '').slice(0, 80),
|
|
627
|
+
state: 'comment',
|
|
628
|
+
kind: 'comment',
|
|
629
|
+
author: comment.user?.login ?? 'unknown',
|
|
630
|
+
url: comment.html_url ?? '',
|
|
631
|
+
comments: 0,
|
|
632
|
+
createdAt: comment.created_at ?? '',
|
|
633
|
+
body: comment.body ?? null,
|
|
634
|
+
}))
|
|
635
|
+
: raw.map(item => ({
|
|
636
|
+
number: item.number,
|
|
637
|
+
title: item.title,
|
|
638
|
+
state: item.state,
|
|
639
|
+
kind: item.pull_request !== undefined ? 'pr' : 'issue',
|
|
640
|
+
author: item.user?.login ?? 'unknown',
|
|
641
|
+
url: item.html_url,
|
|
642
|
+
comments: item.comments ?? 0,
|
|
643
|
+
createdAt: item.created_at ?? '',
|
|
644
|
+
body: item.body ?? null,
|
|
645
|
+
}));
|
|
646
|
+
const value = {
|
|
647
|
+
repo: repoResult.repo,
|
|
648
|
+
action: args.action,
|
|
649
|
+
total: items.length,
|
|
650
|
+
items,
|
|
651
|
+
rateLimit,
|
|
652
|
+
};
|
|
653
|
+
return value;
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
/** `issue_open`: create an issue (write; approval-gated upstream). */
|
|
658
|
+
export function issueOpenTool(state) {
|
|
659
|
+
return defineTool({
|
|
660
|
+
name: 'issue_open',
|
|
661
|
+
description: 'Create a GitHub issue. Requires approval. Returns the issue URL and number.',
|
|
662
|
+
parameters: {
|
|
663
|
+
title: { type: 'string', required: true, description: 'Issue title.' },
|
|
664
|
+
body: { type: 'string', description: 'Issue body.' },
|
|
665
|
+
labels: { type: 'array', items: { type: 'string' }, description: 'Label names to apply.' },
|
|
666
|
+
ownerRepo: { type: 'string', description: 'Target repository as owner/repo. Defaults to configured or git origin.' },
|
|
667
|
+
},
|
|
668
|
+
output: {
|
|
669
|
+
schema: {
|
|
670
|
+
oneOf: [{
|
|
671
|
+
type: 'object',
|
|
672
|
+
additionalProperties: false,
|
|
673
|
+
properties: {
|
|
674
|
+
status: { type: 'string', required: true, const: 'created' },
|
|
675
|
+
url: { type: 'string', required: true },
|
|
676
|
+
number: { type: 'integer', required: true },
|
|
677
|
+
title: { type: 'string', required: true },
|
|
678
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
679
|
+
},
|
|
680
|
+
}, ERROR_SCHEMA],
|
|
681
|
+
},
|
|
682
|
+
render: (_args, value) => {
|
|
683
|
+
if (value.status === 'error') {
|
|
684
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
685
|
+
}
|
|
686
|
+
return [{ type: 'text', text: `created issue #${value.number}: ${value.url}` }];
|
|
687
|
+
},
|
|
688
|
+
presentationMeta: identityMeta,
|
|
689
|
+
},
|
|
690
|
+
isConcurrencySafe: () => false,
|
|
691
|
+
presentCall: issueOpenCall,
|
|
692
|
+
presentResult: issueOpenResult,
|
|
693
|
+
async execute(args, exec) {
|
|
694
|
+
if (args.title.trim().length === 0)
|
|
695
|
+
return errorValue('invalid-args', 'title must not be empty');
|
|
696
|
+
const repo = await state.resolveRepo(args.ownerRepo, exec.signal);
|
|
697
|
+
if (!repo.ok)
|
|
698
|
+
return errorValue(repo.code, repo.message, repo.guidance);
|
|
699
|
+
const token = await state.resolveToken(exec.signal);
|
|
700
|
+
if (!token.ok)
|
|
701
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
702
|
+
try {
|
|
703
|
+
const created = await state.client(token.token.value).requestJson('POST', `/repos/${repo.repo}/issues`, {
|
|
704
|
+
signal: exec.signal,
|
|
705
|
+
body: {
|
|
706
|
+
title: args.title,
|
|
707
|
+
...args.body !== undefined ? { body: args.body } : {},
|
|
708
|
+
...args.labels !== undefined && args.labels.length > 0 ? { labels: args.labels } : {},
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
return { status: 'created', url: created.data.html_url, number: created.data.number, title: created.data.title, rateLimit: rateLimitValue(created.rateLimit) };
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
return githubErrorValue(error);
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
/** `issue_comment`: comment on an issue or pull request (write; approval-gated upstream). */
|
|
720
|
+
export function issueCommentTool(state) {
|
|
721
|
+
return defineTool({
|
|
722
|
+
name: 'issue_comment',
|
|
723
|
+
description: 'Comment on a GitHub issue or pull request. Requires approval. Returns the comment URL.',
|
|
724
|
+
parameters: {
|
|
725
|
+
ownerRepo: { type: 'string', description: 'Repository as owner/repo. Defaults to configured or git origin.' },
|
|
726
|
+
issueNumber: { type: 'integer', required: true, description: 'Issue or pull request number.' },
|
|
727
|
+
body: { type: 'string', required: true, description: 'Comment body.' },
|
|
728
|
+
},
|
|
729
|
+
output: {
|
|
730
|
+
schema: {
|
|
731
|
+
oneOf: [{
|
|
732
|
+
type: 'object',
|
|
733
|
+
additionalProperties: false,
|
|
734
|
+
properties: {
|
|
735
|
+
status: { type: 'string', required: true, const: 'commented' },
|
|
736
|
+
url: { type: 'string', required: true },
|
|
737
|
+
commentId: { type: 'integer', required: true },
|
|
738
|
+
issueNumber: { type: 'integer', required: true },
|
|
739
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
740
|
+
},
|
|
741
|
+
}, ERROR_SCHEMA],
|
|
742
|
+
},
|
|
743
|
+
render: (_args, value) => {
|
|
744
|
+
if (value.status === 'error') {
|
|
745
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
746
|
+
}
|
|
747
|
+
return [{ type: 'text', text: `commented on #${value.issueNumber}: ${value.url}` }];
|
|
748
|
+
},
|
|
749
|
+
presentationMeta: identityMeta,
|
|
750
|
+
},
|
|
751
|
+
isConcurrencySafe: () => false,
|
|
752
|
+
presentCall: issueCommentCall,
|
|
753
|
+
presentResult: issueCommentResult,
|
|
754
|
+
async execute(args, exec) {
|
|
755
|
+
const body = args.body.trim();
|
|
756
|
+
if (body.length === 0)
|
|
757
|
+
return errorValue('invalid-args', 'body must not be empty');
|
|
758
|
+
const repo = await state.resolveRepo(args.ownerRepo, exec.signal);
|
|
759
|
+
if (!repo.ok)
|
|
760
|
+
return errorValue(repo.code, repo.message, repo.guidance);
|
|
761
|
+
const token = await state.resolveToken(exec.signal);
|
|
762
|
+
if (!token.ok)
|
|
763
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
764
|
+
try {
|
|
765
|
+
const posted = await state.client(token.token.value).requestJson('POST', `/repos/${repo.repo}/issues/${args.issueNumber}/comments`, { signal: exec.signal, body: { body } });
|
|
766
|
+
return { status: 'commented', url: posted.data.html_url, commentId: posted.data.id, issueNumber: args.issueNumber, rateLimit: rateLimitValue(posted.rateLimit) };
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
return githubErrorValue(error);
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
/** `issue_close`: close an issue (write; approval-gated upstream). */
|
|
775
|
+
export function issueCloseTool(state) {
|
|
776
|
+
return defineTool({
|
|
777
|
+
name: 'issue_close',
|
|
778
|
+
description: 'Close a GitHub issue. Requires approval. Optionally records a close reason '
|
|
779
|
+
+ '("completed" or "not planned") for projects that surface it.',
|
|
780
|
+
parameters: {
|
|
781
|
+
ownerRepo: { type: 'string', description: 'Repository as owner/repo. Defaults to configured or git origin.' },
|
|
782
|
+
issueNumber: { type: 'integer', required: true, description: 'Issue number.' },
|
|
783
|
+
stateReason: { type: 'string', enum: ['completed', 'not_planned'], description: 'Close reason GitHub records. Omit for a plain close.' },
|
|
784
|
+
},
|
|
785
|
+
output: {
|
|
786
|
+
schema: {
|
|
787
|
+
oneOf: [{
|
|
788
|
+
type: 'object',
|
|
789
|
+
additionalProperties: false,
|
|
790
|
+
properties: {
|
|
791
|
+
status: { type: 'string', required: true, const: 'closed' },
|
|
792
|
+
url: { type: 'string', required: true },
|
|
793
|
+
number: { type: 'integer', required: true },
|
|
794
|
+
title: { type: 'string', required: true },
|
|
795
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
796
|
+
},
|
|
797
|
+
}, ERROR_SCHEMA],
|
|
798
|
+
},
|
|
799
|
+
render: (_args, value) => {
|
|
800
|
+
if (value.status === 'error') {
|
|
801
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
802
|
+
}
|
|
803
|
+
return [{ type: 'text', text: `closed issue #${value.number}: ${value.url}` }];
|
|
804
|
+
},
|
|
805
|
+
presentationMeta: identityMeta,
|
|
806
|
+
},
|
|
807
|
+
isConcurrencySafe: () => false,
|
|
808
|
+
presentCall: issueCloseCall,
|
|
809
|
+
presentResult: issueCloseResult,
|
|
810
|
+
async execute(args, exec) {
|
|
811
|
+
const repo = await state.resolveRepo(args.ownerRepo, exec.signal);
|
|
812
|
+
if (!repo.ok)
|
|
813
|
+
return errorValue(repo.code, repo.message, repo.guidance);
|
|
814
|
+
const token = await state.resolveToken(exec.signal);
|
|
815
|
+
if (!token.ok)
|
|
816
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
817
|
+
try {
|
|
818
|
+
const closed = await state.client(token.token.value).requestJson('PATCH', `/repos/${repo.repo}/issues/${args.issueNumber}`, {
|
|
819
|
+
signal: exec.signal,
|
|
820
|
+
body: {
|
|
821
|
+
state: 'closed',
|
|
822
|
+
...args.stateReason !== undefined ? { state_reason: args.stateReason } : {},
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
return { status: 'closed', url: closed.data.html_url, number: closed.data.number, title: closed.data.title, rateLimit: rateLimitValue(closed.rateLimit) };
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
return githubErrorValue(error);
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
const SEARCH_REPO_RE = /https?:\/\/[^/]+\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\/(?:issues|pull)\/\d+/;
|
|
834
|
+
/** `gh_search`: search issues and pull requests (read; uses the search quota). */
|
|
835
|
+
export function ghSearchTool(state) {
|
|
836
|
+
return defineTool({
|
|
837
|
+
name: 'gh_search',
|
|
838
|
+
description: 'Search GitHub issues and pull requests (GitHub search syntax, e.g. "repo:owner/name is:issue crash"). '
|
|
839
|
+
+ 'Read-only and concurrency-safe. Uses the separate search-API quota.',
|
|
840
|
+
parameters: {
|
|
841
|
+
q: { type: 'string', required: true, description: 'Search query in GitHub search syntax.' },
|
|
842
|
+
sort: { type: 'string', enum: ['comments', 'reactions', 'created', 'updated'], description: 'Sort key. Defaults to best match.' },
|
|
843
|
+
order: { type: 'string', enum: ['desc', 'asc'], description: 'Sort order. Defaults to desc.' },
|
|
844
|
+
perPage: { type: 'integer', description: 'Max results. Defaults to 20, capped at 100.' },
|
|
845
|
+
},
|
|
846
|
+
output: {
|
|
847
|
+
schema: {
|
|
848
|
+
oneOf: [{
|
|
849
|
+
type: 'object',
|
|
850
|
+
additionalProperties: false,
|
|
851
|
+
properties: {
|
|
852
|
+
query: { type: 'string', required: true },
|
|
853
|
+
total: { type: 'integer', required: true },
|
|
854
|
+
items: {
|
|
855
|
+
type: 'array',
|
|
856
|
+
required: true,
|
|
857
|
+
items: {
|
|
858
|
+
type: 'object',
|
|
859
|
+
additionalProperties: false,
|
|
860
|
+
properties: {
|
|
861
|
+
number: { type: 'integer', required: true },
|
|
862
|
+
title: { type: 'string', required: true },
|
|
863
|
+
state: { type: 'string', required: true },
|
|
864
|
+
kind: { type: 'string', required: true, enum: ['issue', 'pr'] },
|
|
865
|
+
author: { type: 'string', required: true },
|
|
866
|
+
url: { type: 'string', required: true },
|
|
867
|
+
repo: { type: 'string', required: true },
|
|
868
|
+
comments: { type: 'integer', required: true },
|
|
869
|
+
createdAt: { type: 'string', required: true },
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
874
|
+
},
|
|
875
|
+
}, ERROR_SCHEMA],
|
|
876
|
+
},
|
|
877
|
+
render: (_args, value) => {
|
|
878
|
+
if ('status' in value) {
|
|
879
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
880
|
+
}
|
|
881
|
+
if (value.items.length === 0)
|
|
882
|
+
return [{ type: 'text', text: `no results for "${value.query}"` }];
|
|
883
|
+
return [{
|
|
884
|
+
type: 'text',
|
|
885
|
+
text: value.items.map(item => `#${item.number} ${item.title} [${item.kind}/${item.state}] ${item.repo} by ${item.author} — ${item.url}`).join('\n'),
|
|
886
|
+
}];
|
|
887
|
+
},
|
|
888
|
+
presentationMeta: identityMeta,
|
|
889
|
+
},
|
|
890
|
+
isConcurrencySafe: () => true,
|
|
891
|
+
presentCall: ghSearchCall,
|
|
892
|
+
presentResult: ghSearchResult,
|
|
893
|
+
async execute(args, exec) {
|
|
894
|
+
const q = args.q.trim();
|
|
895
|
+
if (q.length === 0)
|
|
896
|
+
return errorValue('invalid-args', 'q must not be empty');
|
|
897
|
+
const token = await state.resolveToken(exec.signal);
|
|
898
|
+
if (!token.ok)
|
|
899
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
900
|
+
const perPage = Math.min(Math.max(args.perPage ?? 20, 1), 100);
|
|
901
|
+
const query = new URLSearchParams({ q, per_page: String(perPage) });
|
|
902
|
+
if (args.sort !== undefined)
|
|
903
|
+
query.set('sort', args.sort);
|
|
904
|
+
if (args.order !== undefined)
|
|
905
|
+
query.set('order', args.order);
|
|
906
|
+
try {
|
|
907
|
+
const response = await state.client(token.token.value).requestJson('GET', `/search/issues?${query.toString()}`, { signal: exec.signal });
|
|
908
|
+
const items = response.data.items ?? [];
|
|
909
|
+
const value = {
|
|
910
|
+
query: q,
|
|
911
|
+
total: response.data.total_count ?? items.length,
|
|
912
|
+
items: items.map(item => {
|
|
913
|
+
const match = SEARCH_REPO_RE.exec(item.html_url);
|
|
914
|
+
const isPr = item.pull_request !== undefined || item.html_url.includes('/pull/');
|
|
915
|
+
return {
|
|
916
|
+
number: item.number,
|
|
917
|
+
title: item.title,
|
|
918
|
+
state: item.state,
|
|
919
|
+
kind: isPr ? 'pr' : 'issue',
|
|
920
|
+
author: item.user?.login ?? 'unknown',
|
|
921
|
+
url: item.html_url,
|
|
922
|
+
repo: match?.[1] ?? '',
|
|
923
|
+
comments: item.comments ?? 0,
|
|
924
|
+
createdAt: item.created_at ?? '',
|
|
925
|
+
};
|
|
926
|
+
}),
|
|
927
|
+
rateLimit: rateLimitValue(response.rateLimit),
|
|
928
|
+
};
|
|
929
|
+
return value;
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
return githubErrorValue(error);
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
//# sourceMappingURL=tools.js.map
|