@perrylink/dsh-github 0.4.1 → 0.6.1
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.es.md +30 -13
- package/README.hi.md +30 -13
- package/README.md +53 -13
- package/README.pt.md +31 -14
- package/README.zh-CN.md +53 -13
- package/action.yml +161 -0
- package/lib/approval-gate.d.ts +14 -9
- package/lib/approval-gate.d.ts.map +1 -1
- package/lib/approval-gate.js +39 -1
- package/lib/approval-gate.js.map +1 -1
- package/lib/ci/bot.d.ts +50 -0
- package/lib/ci/bot.d.ts.map +1 -0
- package/lib/ci/bot.js +168 -0
- package/lib/ci/bot.js.map +1 -0
- package/lib/ci/pipeline.d.ts +84 -0
- package/lib/ci/pipeline.d.ts.map +1 -0
- package/lib/ci/pipeline.js +380 -0
- package/lib/ci/pipeline.js.map +1 -0
- package/lib/ci/review-rules.d.ts +41 -0
- package/lib/ci/review-rules.d.ts.map +1 -0
- package/lib/ci/review-rules.js +108 -0
- package/lib/ci/review-rules.js.map +1 -0
- package/lib/ci/tool.d.ts +4 -0
- package/lib/ci/tool.d.ts.map +1 -0
- package/lib/ci/tool.js +155 -0
- package/lib/ci/tool.js.map +1 -0
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +5 -2
- package/lib/commands.js.map +1 -1
- package/lib/config.d.ts +65 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +75 -2
- package/lib/config.js.map +1 -1
- package/lib/github.d.ts +11 -7
- package/lib/github.d.ts.map +1 -1
- package/lib/github.js +28 -7
- package/lib/github.js.map +1 -1
- package/lib/index.d.ts +15 -9
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +13 -1
- package/lib/index.js.map +1 -1
- package/lib/jobs.js +4 -3
- package/lib/jobs.js.map +1 -1
- package/lib/present.d.ts +146 -0
- package/lib/present.d.ts.map +1 -1
- package/lib/present.js +140 -1
- package/lib/present.js.map +1 -1
- package/lib/review.d.ts +11 -1
- package/lib/review.d.ts.map +1 -1
- package/lib/review.js +12 -9
- package/lib/review.js.map +1 -1
- package/lib/state.d.ts +3 -1
- package/lib/state.d.ts.map +1 -1
- package/lib/state.js +11 -6
- package/lib/state.js.map +1 -1
- package/lib/tools.d.ts +71 -0
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +379 -16
- package/lib/tools.js.map +1 -1
- package/package.json +6 -2
- package/scripts/action-patch.mjs +152 -0
- package/scripts/action-post.mjs +61 -0
- package/scripts/check-readmes.mjs +113 -0
- package/scripts/local-test.mjs +259 -0
- package/scripts/prepare.mjs +34 -0
package/lib/tools.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The model-facing tool surface of dsh-github.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* issue_close (writes, approval-gated by the tools/pre-execute
|
|
6
|
-
* approval-gate.ts) and gh_review / gh_issue / gh_search
|
|
7
|
-
* reads). Every execute returns only the canonical
|
|
8
|
-
* output schema; infrastructure failures throw so
|
|
9
|
-
* isError. Tokens never appear in canonical values,
|
|
10
|
-
* thrown messages. Rate-limit facts ride every result,
|
|
4
|
+
* Twelve tools: pr_create / pr_merge / pr_update / review_post / issue_open /
|
|
5
|
+
* issue_comment / issue_close (writes, approval-gated by the tools/pre-execute
|
|
6
|
+
* listener in approval-gate.ts) and gh_review / gh_issue / gh_search / gh_repo /
|
|
7
|
+
* gh_file (concurrency-safe reads). Every execute returns only the canonical
|
|
8
|
+
* JSON value declared by its output schema; infrastructure failures throw so
|
|
9
|
+
* the registry marks them isError. Tokens never appear in canonical values,
|
|
10
|
+
* rendered content, or thrown messages. Rate-limit facts ride every result,
|
|
11
|
+
* including errors.
|
|
11
12
|
* @module dsh-github/tools
|
|
12
13
|
*/
|
|
13
14
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
@@ -15,18 +16,18 @@ import { GithubError } from "./github.js";
|
|
|
15
16
|
import { readGitState } from "./git.js";
|
|
16
17
|
import { analyzeDiff, formatPostBody, parseDiffStats } from "./review.js";
|
|
17
18
|
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
|
+
import { ghFileCall, ghFileResult, ghIssueCall, ghIssueResult, ghRepoCall, ghRepoResult, ghReviewCall, ghReviewResult, ghSearchCall, ghSearchResult, identityMeta, issueCloseCall, issueCloseResult, issueCommentCall, issueCommentResult, issueOpenCall, issueOpenResult, prCreateCall, prCreateResult, prMergeCall, prMergeResult, prUpdateCall, prUpdateResult, reviewPostCall, reviewPostResult, } from "./present.js";
|
|
19
20
|
const RATE_LIMIT_PROPERTIES = {
|
|
20
21
|
remaining: { oneOf: [{ type: 'number' }, { type: 'null' }] },
|
|
21
22
|
resetAt: { oneOf: [{ type: 'number' }, { type: 'null' }] },
|
|
22
23
|
};
|
|
23
|
-
const RATE_LIMIT_SCHEMA = {
|
|
24
|
+
export const RATE_LIMIT_SCHEMA = {
|
|
24
25
|
type: 'object',
|
|
25
26
|
additionalProperties: false,
|
|
26
27
|
properties: RATE_LIMIT_PROPERTIES,
|
|
27
28
|
required: true,
|
|
28
29
|
};
|
|
29
|
-
const ERROR_SCHEMA = {
|
|
30
|
+
export const ERROR_SCHEMA = {
|
|
30
31
|
type: 'object',
|
|
31
32
|
additionalProperties: false,
|
|
32
33
|
properties: {
|
|
@@ -127,6 +128,9 @@ export function prCreateTool(state) {
|
|
|
127
128
|
const git = await readGitState(state.workspaceDir, state.runGit, exec.signal, state.apiHost);
|
|
128
129
|
if (git.branch === null)
|
|
129
130
|
return errorValue('no-head', 'could not determine the head branch', 'Pass `head` explicitly or run inside a git checkout.');
|
|
131
|
+
if (git.branch === 'HEAD') {
|
|
132
|
+
return errorValue('no-head', 'the checkout is in detached HEAD state', 'Check out a branch (or pass `head` explicitly) before creating a pull request.');
|
|
133
|
+
}
|
|
130
134
|
head = git.branch;
|
|
131
135
|
}
|
|
132
136
|
const token = await state.resolveToken(exec.signal);
|
|
@@ -359,7 +363,9 @@ export function ghReviewTool(state) {
|
|
|
359
363
|
ci = { summary: 'unknown', error: sectionErrorCode(error) };
|
|
360
364
|
}
|
|
361
365
|
}
|
|
362
|
-
const findings = fields.has('findings')
|
|
366
|
+
const findings = fields.has('findings')
|
|
367
|
+
? analyzeDiff(diffText, maxDiffChars, { maxFindings: state.config.maxFindings, maxLineLength: state.config.maxLineLength }).findings
|
|
368
|
+
: [];
|
|
363
369
|
const value = {
|
|
364
370
|
repo: repoResult.repo,
|
|
365
371
|
number: payload.number,
|
|
@@ -504,11 +510,15 @@ export function reviewPostTool(state) {
|
|
|
504
510
|
body: {
|
|
505
511
|
body: reviewBody,
|
|
506
512
|
event: 'COMMENT',
|
|
507
|
-
comments:
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
513
|
+
// A body-only review omits `comments` entirely: GitHub rejects
|
|
514
|
+
// an empty array edge more strictly than a missing key.
|
|
515
|
+
...inline.length > 0 ? {
|
|
516
|
+
comments: inline.map(finding => ({
|
|
517
|
+
path: finding.file,
|
|
518
|
+
line: finding.line,
|
|
519
|
+
body: `**${finding.severity}** \`${finding.rule}\`: ${finding.message}`,
|
|
520
|
+
})),
|
|
521
|
+
} : {},
|
|
512
522
|
},
|
|
513
523
|
});
|
|
514
524
|
return { status: 'posted', mode: 'inline', url: posted.data.html_url, reviewId: posted.data.id, findings: findings.length, rateLimit: rateLimitValue(posted.rateLimit) };
|
|
@@ -934,4 +944,357 @@ export function ghSearchTool(state) {
|
|
|
934
944
|
},
|
|
935
945
|
});
|
|
936
946
|
}
|
|
947
|
+
/** `pr_merge`: merge a pull request (write; approval-gated upstream). */
|
|
948
|
+
export function prMergeTool(state) {
|
|
949
|
+
return defineTool({
|
|
950
|
+
name: 'pr_merge',
|
|
951
|
+
description: 'Merge a GitHub pull request. Requires approval. `pr` accepts a number, "#number", '
|
|
952
|
+
+ '"owner/repo#number", or a pull-request URL. Optionally deletes the head branch after the merge.',
|
|
953
|
+
parameters: {
|
|
954
|
+
pr: { type: 'string', required: true, description: 'PR number, #number, owner/repo#number, or pull URL.' },
|
|
955
|
+
mergeMethod: { type: 'string', enum: ['merge', 'squash', 'rebase'], description: 'Merge method. Defaults to merge.' },
|
|
956
|
+
commitTitle: { type: 'string', description: 'Merge commit title (squash/rebase).' },
|
|
957
|
+
commitMessage: { type: 'string', description: 'Merge commit message (squash/rebase).' },
|
|
958
|
+
deleteBranch: { type: 'boolean', description: 'Delete the head branch after merging. Defaults to false.' },
|
|
959
|
+
},
|
|
960
|
+
output: {
|
|
961
|
+
schema: {
|
|
962
|
+
oneOf: [{
|
|
963
|
+
type: 'object',
|
|
964
|
+
additionalProperties: false,
|
|
965
|
+
properties: {
|
|
966
|
+
status: { type: 'string', required: true, const: 'merged' },
|
|
967
|
+
merged: { type: 'boolean', required: true },
|
|
968
|
+
sha: { type: 'string' },
|
|
969
|
+
message: { type: 'string', required: true },
|
|
970
|
+
url: { type: 'string', required: true },
|
|
971
|
+
branchDeleted: { type: 'boolean', required: true },
|
|
972
|
+
branchDeleteNote: { type: 'string' },
|
|
973
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
974
|
+
},
|
|
975
|
+
}, ERROR_SCHEMA],
|
|
976
|
+
},
|
|
977
|
+
render: (_args, value) => {
|
|
978
|
+
if (value.status === 'error') {
|
|
979
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
980
|
+
}
|
|
981
|
+
const deleted = value.branchDeleted ? ' (head branch deleted)' : value.branchDeleteNote !== undefined ? ` (branch delete failed: ${value.branchDeleteNote})` : '';
|
|
982
|
+
return [{ type: 'text', text: `merged pull request: ${value.url}${deleted} — ${value.message}` }];
|
|
983
|
+
},
|
|
984
|
+
presentationMeta: identityMeta,
|
|
985
|
+
},
|
|
986
|
+
isConcurrencySafe: () => false,
|
|
987
|
+
presentCall: prMergeCall,
|
|
988
|
+
presentResult: prMergeResult,
|
|
989
|
+
async execute(args, exec) {
|
|
990
|
+
const ref = state.parsePrRef(args.pr);
|
|
991
|
+
if (ref === null)
|
|
992
|
+
return errorValue('invalid-pr', `"${args.pr}" is not a PR reference`, 'Use a number, "#number", "owner/repo#number", or a pull URL.');
|
|
993
|
+
const repoResult = ref.repo !== undefined ? { ok: true, repo: ref.repo } : await state.resolveRepo(undefined, exec.signal);
|
|
994
|
+
if (!repoResult.ok)
|
|
995
|
+
return errorValue(repoResult.code, repoResult.message, repoResult.guidance);
|
|
996
|
+
const token = await state.resolveToken(exec.signal);
|
|
997
|
+
if (!token.ok)
|
|
998
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
999
|
+
const client = state.client(token.token.value);
|
|
1000
|
+
let headRef;
|
|
1001
|
+
let headSha;
|
|
1002
|
+
let prUrl;
|
|
1003
|
+
try {
|
|
1004
|
+
const metadata = await client.requestJson('GET', `/repos/${repoResult.repo}/pulls/${ref.number}`, { signal: exec.signal });
|
|
1005
|
+
headRef = metadata.data.head?.ref;
|
|
1006
|
+
headSha = metadata.data.head?.sha;
|
|
1007
|
+
prUrl = metadata.data.html_url;
|
|
1008
|
+
}
|
|
1009
|
+
catch (error) {
|
|
1010
|
+
return githubErrorValue(error);
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
const merged = await client.requestJson('PUT', `/repos/${repoResult.repo}/pulls/${ref.number}/merge`, {
|
|
1014
|
+
signal: exec.signal,
|
|
1015
|
+
body: {
|
|
1016
|
+
merge_method: args.mergeMethod ?? 'merge',
|
|
1017
|
+
...headSha !== undefined ? { sha: headSha } : {},
|
|
1018
|
+
...args.commitTitle !== undefined ? { commit_title: args.commitTitle } : {},
|
|
1019
|
+
...args.commitMessage !== undefined ? { commit_message: args.commitMessage } : {},
|
|
1020
|
+
},
|
|
1021
|
+
});
|
|
1022
|
+
let branchDeleted = false;
|
|
1023
|
+
let branchDeleteNote;
|
|
1024
|
+
if (args.deleteBranch === true && headRef !== undefined && headRef.length > 0) {
|
|
1025
|
+
try {
|
|
1026
|
+
await client.requestJson('DELETE', `/repos/${repoResult.repo}/git/refs/${encodeURIComponent(`heads/${headRef}`)}`, { signal: exec.signal });
|
|
1027
|
+
branchDeleted = true;
|
|
1028
|
+
}
|
|
1029
|
+
catch (error) {
|
|
1030
|
+
branchDeleteNote = error instanceof GithubError ? `GitHub API ${error.status}` : 'delete failed';
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
status: 'merged',
|
|
1035
|
+
merged: merged.data.merged === true,
|
|
1036
|
+
...merged.data.sha !== undefined ? { sha: merged.data.sha } : {},
|
|
1037
|
+
message: merged.data.message ?? 'merged',
|
|
1038
|
+
url: prUrl,
|
|
1039
|
+
branchDeleted,
|
|
1040
|
+
...branchDeleteNote !== undefined ? { branchDeleteNote } : {},
|
|
1041
|
+
rateLimit: rateLimitValue(merged.rateLimit),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
return githubErrorValue(error);
|
|
1046
|
+
}
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
/** `pr_update`: edit a pull request (write; approval-gated upstream). */
|
|
1051
|
+
export function prUpdateTool(state) {
|
|
1052
|
+
return defineTool({
|
|
1053
|
+
name: 'pr_update',
|
|
1054
|
+
description: 'Update a GitHub pull request: title, body, state (open/closed), or target branch. '
|
|
1055
|
+
+ 'Requires approval. At least one field must be provided.',
|
|
1056
|
+
parameters: {
|
|
1057
|
+
pr: { type: 'string', required: true, description: 'PR number, #number, owner/repo#number, or pull URL.' },
|
|
1058
|
+
title: { type: 'string', description: 'New PR title.' },
|
|
1059
|
+
body: { type: 'string', description: 'New PR description body.' },
|
|
1060
|
+
state: { type: 'string', enum: ['open', 'closed'], description: 'New PR state.' },
|
|
1061
|
+
base: { type: 'string', description: 'New target branch.' },
|
|
1062
|
+
},
|
|
1063
|
+
output: {
|
|
1064
|
+
schema: {
|
|
1065
|
+
oneOf: [{
|
|
1066
|
+
type: 'object',
|
|
1067
|
+
additionalProperties: false,
|
|
1068
|
+
properties: {
|
|
1069
|
+
status: { type: 'string', required: true, const: 'updated' },
|
|
1070
|
+
url: { type: 'string', required: true },
|
|
1071
|
+
number: { type: 'integer', required: true },
|
|
1072
|
+
title: { type: 'string', required: true },
|
|
1073
|
+
state: { type: 'string', required: true },
|
|
1074
|
+
base: { type: 'string', required: true },
|
|
1075
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
1076
|
+
},
|
|
1077
|
+
}, ERROR_SCHEMA],
|
|
1078
|
+
},
|
|
1079
|
+
render: (_args, value) => {
|
|
1080
|
+
if (value.status === 'error') {
|
|
1081
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
1082
|
+
}
|
|
1083
|
+
return [{ type: 'text', text: `updated pull request #${value.number} (${value.state}, base ${value.base}): ${value.url}` }];
|
|
1084
|
+
},
|
|
1085
|
+
presentationMeta: identityMeta,
|
|
1086
|
+
},
|
|
1087
|
+
isConcurrencySafe: () => false,
|
|
1088
|
+
presentCall: prUpdateCall,
|
|
1089
|
+
presentResult: prUpdateResult,
|
|
1090
|
+
async execute(args, exec) {
|
|
1091
|
+
if (args.title !== undefined && args.title.trim().length === 0)
|
|
1092
|
+
return errorValue('invalid-args', 'title must not be empty');
|
|
1093
|
+
if (args.base !== undefined && args.base.trim().length === 0)
|
|
1094
|
+
return errorValue('invalid-args', 'base must not be empty');
|
|
1095
|
+
if (args.title === undefined && args.body === undefined && args.state === undefined && args.base === undefined) {
|
|
1096
|
+
return errorValue('invalid-args', 'pass at least one of title, body, state, or base');
|
|
1097
|
+
}
|
|
1098
|
+
const ref = state.parsePrRef(args.pr);
|
|
1099
|
+
if (ref === null)
|
|
1100
|
+
return errorValue('invalid-pr', `"${args.pr}" is not a PR reference`, 'Use a number, "#number", "owner/repo#number", or a pull URL.');
|
|
1101
|
+
const repoResult = ref.repo !== undefined ? { ok: true, repo: ref.repo } : await state.resolveRepo(undefined, exec.signal);
|
|
1102
|
+
if (!repoResult.ok)
|
|
1103
|
+
return errorValue(repoResult.code, repoResult.message, repoResult.guidance);
|
|
1104
|
+
const token = await state.resolveToken(exec.signal);
|
|
1105
|
+
if (!token.ok)
|
|
1106
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
1107
|
+
try {
|
|
1108
|
+
const updated = await state.client(token.token.value).requestJson('PATCH', `/repos/${repoResult.repo}/pulls/${ref.number}`, {
|
|
1109
|
+
signal: exec.signal,
|
|
1110
|
+
body: {
|
|
1111
|
+
...args.title !== undefined ? { title: args.title } : {},
|
|
1112
|
+
...args.body !== undefined ? { body: args.body } : {},
|
|
1113
|
+
...args.state !== undefined ? { state: args.state } : {},
|
|
1114
|
+
...args.base !== undefined ? { base: args.base } : {},
|
|
1115
|
+
},
|
|
1116
|
+
});
|
|
1117
|
+
const data = updated.data;
|
|
1118
|
+
return {
|
|
1119
|
+
status: 'updated',
|
|
1120
|
+
url: data.html_url,
|
|
1121
|
+
number: data.number,
|
|
1122
|
+
title: data.title,
|
|
1123
|
+
state: data.state,
|
|
1124
|
+
base: data.base?.ref ?? '',
|
|
1125
|
+
rateLimit: rateLimitValue(updated.rateLimit),
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
catch (error) {
|
|
1129
|
+
return githubErrorValue(error);
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
/** `gh_repo`: read one repository's metadata (read; concurrency-safe). */
|
|
1135
|
+
export function ghRepoTool(state) {
|
|
1136
|
+
return defineTool({
|
|
1137
|
+
name: 'gh_repo',
|
|
1138
|
+
description: 'Read a GitHub repository\'s metadata: description, default branch, visibility, stars, forks, '
|
|
1139
|
+
+ 'open issues, language, license, topics, and last update. Read-only and concurrency-safe.',
|
|
1140
|
+
parameters: {
|
|
1141
|
+
ownerRepo: { type: 'string', description: 'Repository as owner/repo. Defaults to configured or git origin.' },
|
|
1142
|
+
},
|
|
1143
|
+
output: {
|
|
1144
|
+
schema: {
|
|
1145
|
+
oneOf: [{
|
|
1146
|
+
type: 'object',
|
|
1147
|
+
additionalProperties: false,
|
|
1148
|
+
properties: {
|
|
1149
|
+
repo: { type: 'string', required: true },
|
|
1150
|
+
description: { type: 'string', required: true },
|
|
1151
|
+
defaultBranch: { type: 'string', required: true },
|
|
1152
|
+
visibility: { type: 'string', required: true },
|
|
1153
|
+
stars: { type: 'integer', required: true },
|
|
1154
|
+
forks: { type: 'integer', required: true },
|
|
1155
|
+
openIssues: { type: 'integer', required: true },
|
|
1156
|
+
language: { type: 'string', required: true },
|
|
1157
|
+
license: { type: 'string', required: true },
|
|
1158
|
+
topics: { type: 'array', required: true, items: { type: 'string' } },
|
|
1159
|
+
url: { type: 'string', required: true },
|
|
1160
|
+
updatedAt: { type: 'string', required: true },
|
|
1161
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
1162
|
+
},
|
|
1163
|
+
}, ERROR_SCHEMA],
|
|
1164
|
+
},
|
|
1165
|
+
render: (_args, value) => {
|
|
1166
|
+
if ('status' in value) {
|
|
1167
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
1168
|
+
}
|
|
1169
|
+
return [{
|
|
1170
|
+
type: 'text',
|
|
1171
|
+
text: `${value.repo}: ${value.description}\n`
|
|
1172
|
+
+ `default branch ${value.defaultBranch} · ${value.language} · ${value.license}\n`
|
|
1173
|
+
+ `stars ${value.stars} · forks ${value.forks} · open issues ${value.openIssues} · ${value.visibility}\n${value.url}`,
|
|
1174
|
+
}];
|
|
1175
|
+
},
|
|
1176
|
+
presentationMeta: identityMeta,
|
|
1177
|
+
},
|
|
1178
|
+
isConcurrencySafe: () => true,
|
|
1179
|
+
presentCall: ghRepoCall,
|
|
1180
|
+
presentResult: ghRepoResult,
|
|
1181
|
+
async execute(args, exec) {
|
|
1182
|
+
const repoResult = await state.resolveRepo(args.ownerRepo, exec.signal);
|
|
1183
|
+
if (!repoResult.ok)
|
|
1184
|
+
return errorValue(repoResult.code, repoResult.message, repoResult.guidance);
|
|
1185
|
+
const token = await state.resolveToken(exec.signal);
|
|
1186
|
+
if (!token.ok)
|
|
1187
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
1188
|
+
try {
|
|
1189
|
+
const response = await state.client(token.token.value).requestJson('GET', `/repos/${repoResult.repo}`, { signal: exec.signal });
|
|
1190
|
+
const data = response.data;
|
|
1191
|
+
const value = {
|
|
1192
|
+
repo: repoResult.repo,
|
|
1193
|
+
description: data.description ?? '',
|
|
1194
|
+
defaultBranch: data.default_branch ?? '',
|
|
1195
|
+
visibility: data.visibility ?? 'unknown',
|
|
1196
|
+
stars: data.stargazers_count ?? 0,
|
|
1197
|
+
forks: data.forks_count ?? 0,
|
|
1198
|
+
openIssues: data.open_issues_count ?? 0,
|
|
1199
|
+
language: data.language ?? '',
|
|
1200
|
+
license: data.license?.spdx_id ?? '',
|
|
1201
|
+
topics: data.topics ?? [],
|
|
1202
|
+
url: data.html_url ?? '',
|
|
1203
|
+
updatedAt: data.updated_at ?? '',
|
|
1204
|
+
rateLimit: rateLimitValue(response.rateLimit),
|
|
1205
|
+
};
|
|
1206
|
+
return value;
|
|
1207
|
+
}
|
|
1208
|
+
catch (error) {
|
|
1209
|
+
return githubErrorValue(error);
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
/** `gh_file`: read one file from a repository (read; concurrency-safe). */
|
|
1215
|
+
export function ghFileTool(state) {
|
|
1216
|
+
return defineTool({
|
|
1217
|
+
name: 'gh_file',
|
|
1218
|
+
description: 'Read one file from a GitHub repository at a branch, tag, or commit (defaults to the default '
|
|
1219
|
+
+ 'branch). File contents are base64-decoded and capped; directories and oversized blobs are reported '
|
|
1220
|
+
+ 'as structured errors. Read-only and concurrency-safe.',
|
|
1221
|
+
parameters: {
|
|
1222
|
+
ownerRepo: { type: 'string', description: 'Repository as owner/repo. Defaults to configured or git origin.' },
|
|
1223
|
+
path: { type: 'string', required: true, description: 'Repository file path, e.g. "README.md" or "src/index.ts".' },
|
|
1224
|
+
ref: { type: 'string', description: 'Branch, tag, or commit SHA. Defaults to the default branch.' },
|
|
1225
|
+
maxChars: { type: 'number', description: 'Cap for the file contents. Defaults to the plugin config.' },
|
|
1226
|
+
},
|
|
1227
|
+
output: {
|
|
1228
|
+
schema: {
|
|
1229
|
+
oneOf: [{
|
|
1230
|
+
type: 'object',
|
|
1231
|
+
additionalProperties: false,
|
|
1232
|
+
properties: {
|
|
1233
|
+
repo: { type: 'string', required: true },
|
|
1234
|
+
path: { type: 'string', required: true },
|
|
1235
|
+
ref: { type: 'string', required: true },
|
|
1236
|
+
size: { type: 'integer', required: true },
|
|
1237
|
+
truncated: { type: 'boolean', required: true },
|
|
1238
|
+
content: { type: 'string', required: true },
|
|
1239
|
+
sha: { type: 'string', required: true },
|
|
1240
|
+
url: { type: 'string', required: true },
|
|
1241
|
+
rateLimit: RATE_LIMIT_SCHEMA,
|
|
1242
|
+
},
|
|
1243
|
+
}, ERROR_SCHEMA],
|
|
1244
|
+
},
|
|
1245
|
+
render: (_args, value) => {
|
|
1246
|
+
if ('status' in value) {
|
|
1247
|
+
return [{ type: 'text', text: `${value.message}${value.guidance !== undefined ? `\n${value.guidance}` : ''}` }];
|
|
1248
|
+
}
|
|
1249
|
+
return [{
|
|
1250
|
+
type: 'text',
|
|
1251
|
+
text: `${value.repo}/${value.path} @ ${value.ref} (${value.size} bytes${value.truncated ? ', truncated' : ''}):\n${value.content}`,
|
|
1252
|
+
}];
|
|
1253
|
+
},
|
|
1254
|
+
presentationMeta: identityMeta,
|
|
1255
|
+
},
|
|
1256
|
+
isConcurrencySafe: () => true,
|
|
1257
|
+
presentCall: ghFileCall,
|
|
1258
|
+
presentResult: ghFileResult,
|
|
1259
|
+
async execute(args, exec) {
|
|
1260
|
+
const path = args.path.trim();
|
|
1261
|
+
if (path.length === 0 || path.startsWith('/'))
|
|
1262
|
+
return errorValue('invalid-args', 'path must be a non-empty repository-relative file path');
|
|
1263
|
+
const repoResult = await state.resolveRepo(args.ownerRepo, exec.signal);
|
|
1264
|
+
if (!repoResult.ok)
|
|
1265
|
+
return errorValue(repoResult.code, repoResult.message, repoResult.guidance);
|
|
1266
|
+
const token = await state.resolveToken(exec.signal);
|
|
1267
|
+
if (!token.ok)
|
|
1268
|
+
return errorValue(token.error.code, token.error.message, token.error.guidance);
|
|
1269
|
+
const maxChars = args.maxChars !== undefined && args.maxChars > 0 ? Math.floor(args.maxChars) : state.config.maxFileChars;
|
|
1270
|
+
const query = args.ref !== undefined ? `?ref=${encodeURIComponent(args.ref)}` : '';
|
|
1271
|
+
const encodedPath = path.split('/').map(segment => encodeURIComponent(segment)).join('/');
|
|
1272
|
+
try {
|
|
1273
|
+
const response = await state.client(token.token.value).requestJson('GET', `/repos/${repoResult.repo}/contents/${encodedPath}${query}`, { signal: exec.signal });
|
|
1274
|
+
const data = response.data;
|
|
1275
|
+
if (Array.isArray(data)) {
|
|
1276
|
+
return errorValue('is-directory', `"${path}" is a directory; pass a file path`, 'List the directory with gh_file on its entries or use a narrower path.');
|
|
1277
|
+
}
|
|
1278
|
+
const raw = data.content ?? '';
|
|
1279
|
+
const text = data.encoding === 'base64' && raw.length > 0 ? Buffer.from(raw, 'base64').toString('utf8') : raw;
|
|
1280
|
+
const capped = capText(text, maxChars);
|
|
1281
|
+
const value = {
|
|
1282
|
+
repo: repoResult.repo,
|
|
1283
|
+
path: data.path ?? path,
|
|
1284
|
+
ref: args.ref ?? 'default',
|
|
1285
|
+
size: data.size ?? Buffer.byteLength(text),
|
|
1286
|
+
truncated: capped.truncated,
|
|
1287
|
+
content: capped.text,
|
|
1288
|
+
sha: data.sha ?? '',
|
|
1289
|
+
url: data.html_url ?? '',
|
|
1290
|
+
rateLimit: rateLimitValue(response.rateLimit),
|
|
1291
|
+
};
|
|
1292
|
+
return value;
|
|
1293
|
+
}
|
|
1294
|
+
catch (error) {
|
|
1295
|
+
return githubErrorValue(error);
|
|
1296
|
+
}
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
937
1300
|
//# sourceMappingURL=tools.js.map
|