@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.
Files changed (65) hide show
  1. package/README.es.md +30 -13
  2. package/README.hi.md +30 -13
  3. package/README.md +53 -13
  4. package/README.pt.md +31 -14
  5. package/README.zh-CN.md +53 -13
  6. package/action.yml +161 -0
  7. package/lib/approval-gate.d.ts +14 -9
  8. package/lib/approval-gate.d.ts.map +1 -1
  9. package/lib/approval-gate.js +39 -1
  10. package/lib/approval-gate.js.map +1 -1
  11. package/lib/ci/bot.d.ts +50 -0
  12. package/lib/ci/bot.d.ts.map +1 -0
  13. package/lib/ci/bot.js +168 -0
  14. package/lib/ci/bot.js.map +1 -0
  15. package/lib/ci/pipeline.d.ts +84 -0
  16. package/lib/ci/pipeline.d.ts.map +1 -0
  17. package/lib/ci/pipeline.js +380 -0
  18. package/lib/ci/pipeline.js.map +1 -0
  19. package/lib/ci/review-rules.d.ts +41 -0
  20. package/lib/ci/review-rules.d.ts.map +1 -0
  21. package/lib/ci/review-rules.js +108 -0
  22. package/lib/ci/review-rules.js.map +1 -0
  23. package/lib/ci/tool.d.ts +4 -0
  24. package/lib/ci/tool.d.ts.map +1 -0
  25. package/lib/ci/tool.js +155 -0
  26. package/lib/ci/tool.js.map +1 -0
  27. package/lib/commands.d.ts.map +1 -1
  28. package/lib/commands.js +5 -2
  29. package/lib/commands.js.map +1 -1
  30. package/lib/config.d.ts +65 -1
  31. package/lib/config.d.ts.map +1 -1
  32. package/lib/config.js +75 -2
  33. package/lib/config.js.map +1 -1
  34. package/lib/github.d.ts +11 -7
  35. package/lib/github.d.ts.map +1 -1
  36. package/lib/github.js +28 -7
  37. package/lib/github.js.map +1 -1
  38. package/lib/index.d.ts +15 -9
  39. package/lib/index.d.ts.map +1 -1
  40. package/lib/index.js +13 -1
  41. package/lib/index.js.map +1 -1
  42. package/lib/jobs.js +4 -3
  43. package/lib/jobs.js.map +1 -1
  44. package/lib/present.d.ts +146 -0
  45. package/lib/present.d.ts.map +1 -1
  46. package/lib/present.js +140 -1
  47. package/lib/present.js.map +1 -1
  48. package/lib/review.d.ts +11 -1
  49. package/lib/review.d.ts.map +1 -1
  50. package/lib/review.js +12 -9
  51. package/lib/review.js.map +1 -1
  52. package/lib/state.d.ts +3 -1
  53. package/lib/state.d.ts.map +1 -1
  54. package/lib/state.js +11 -6
  55. package/lib/state.js.map +1 -1
  56. package/lib/tools.d.ts +71 -0
  57. package/lib/tools.d.ts.map +1 -1
  58. package/lib/tools.js +379 -16
  59. package/lib/tools.js.map +1 -1
  60. package/package.json +6 -2
  61. package/scripts/action-patch.mjs +152 -0
  62. package/scripts/action-post.mjs +61 -0
  63. package/scripts/check-readmes.mjs +113 -0
  64. package/scripts/local-test.mjs +259 -0
  65. 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
- * 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.
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') ? analyzeDiff(diffText, maxDiffChars).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: inline.map(finding => ({
508
- path: finding.file,
509
- line: finding.line,
510
- body: `**${finding.severity}** \`${finding.rule}\`: ${finding.message}`,
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