1+ import { isRecordLike } from '@sim/utils/object'
2+ import { readGitHubErrorMessage } from '@/tools/github/response-parsers'
13import type { CreateCommentParams , CreateCommentResponse } from '@/tools/github/types'
24import { COMMENT_OUTPUT_PROPERTIES , USER_OUTPUT } from '@/tools/github/types'
35import type { ToolConfig } from '@/tools/types'
46
7+ const GITHUB_API_BASE = 'https://api.github.com'
8+
9+ function githubHeaders ( apiKey : string ) : Record < string , string > {
10+ return {
11+ Accept : 'application/vnd.github.v3+json' ,
12+ Authorization : `Bearer ${ apiKey } ` ,
13+ 'X-GitHub-Api-Version' : '2022-11-28' ,
14+ }
15+ }
16+
17+ function pullRequestUrl ( params : CreateCommentParams ) : string {
18+ return `${ GITHUB_API_BASE } /repos/${ params . owner } /${ params . repo } /pulls/${ params . pullNumber } `
19+ }
20+
21+ /**
22+ * GitHub requires `commit_id` on a pull request review comment. When the caller did
23+ * not supply one, the pull request is fetched first so its head SHA can be used —
24+ * mirroring how Jira resolves a missing `cloudId` from `domain`.
25+ */
26+ function needsCommitLookup ( params : CreateCommentParams ) : boolean {
27+ return params . commentType === 'file_comment' && ! params . commitId
28+ }
29+
30+ function fileCommentBody ( params : CreateCommentParams , commitId : string ) : Record < string , any > {
31+ return {
32+ body : params . body ,
33+ commit_id : commitId ,
34+ path : params . path ,
35+ line : params . line ,
36+ side : params . side || 'RIGHT' ,
37+ }
38+ }
39+
40+ function readHeadSha ( pullRequest : unknown ) : string | undefined {
41+ if ( ! isRecordLike ( pullRequest ) || ! isRecordLike ( pullRequest . head ) ) return undefined
42+ const sha = pullRequest . head . sha
43+ return typeof sha === 'string' && sha ? sha : undefined
44+ }
45+
46+ /**
47+ * Returns the raw GitHub comment payload. For a file comment created without an
48+ * explicit `commitId`, `response` holds the pull request lookup instead: its head
49+ * SHA is read and the comment is posted in a follow-up request.
50+ */
51+ async function readCommentPayload (
52+ response : Response ,
53+ params ?: CreateCommentParams
54+ ) : Promise < Record < string , any > > {
55+ if ( ! params || ! needsCommitLookup ( params ) ) return response . json ( )
56+
57+ const commitId = readHeadSha ( await response . json ( ) )
58+ if ( ! commitId ) {
59+ throw new Error (
60+ `GitHub returned no head commit SHA for pull request ${ params . owner } /${ params . repo } #${ params . pullNumber } . Set commitId to comment on a specific commit.`
61+ )
62+ }
63+
64+ const commentResponse = await fetch ( `${ pullRequestUrl ( params ) } /comments` , {
65+ method : 'POST' ,
66+ headers : { ...githubHeaders ( params . apiKey ) , 'Content-Type' : 'application/json' } ,
67+ body : JSON . stringify ( fileCommentBody ( params , commitId ) ) ,
68+ } )
69+
70+ if ( ! commentResponse . ok ) {
71+ throw new Error (
72+ ( await readGitHubErrorMessage ( commentResponse ) ) ??
73+ `Failed to create file comment (HTTP ${ commentResponse . status } )`
74+ )
75+ }
76+
77+ return commentResponse . json ( )
78+ }
79+
580export const commentTool : ToolConfig < CreateCommentParams , CreateCommentResponse > = {
681 id : 'github_comment' ,
782 name : 'GitHub PR Commenter' ,
@@ -39,12 +114,6 @@ export const commentTool: ToolConfig<CreateCommentParams, CreateCommentResponse>
39114 visibility : 'user-or-llm' ,
40115 description : 'File path for review comment' ,
41116 } ,
42- position : {
43- type : 'number' ,
44- required : false ,
45- visibility : 'hidden' ,
46- description : 'Line number for review comment' ,
47- } ,
48117 commentType : {
49118 type : 'string' ,
50119 required : false ,
@@ -68,7 +137,7 @@ export const commentTool: ToolConfig<CreateCommentParams, CreateCommentResponse>
68137 type : 'string' ,
69138 required : false ,
70139 visibility : 'hidden' ,
71- description : 'The SHA of the commit to comment on' ,
140+ description : 'The SHA of the commit to comment on. Defaults to the pull request head commit. ' ,
72141 } ,
73142 apiKey : {
74143 type : 'string' ,
@@ -80,26 +149,22 @@ export const commentTool: ToolConfig<CreateCommentParams, CreateCommentResponse>
80149
81150 request : {
82151 url : ( params ) => {
152+ if ( needsCommitLookup ( params ) ) {
153+ return pullRequestUrl ( params )
154+ }
83155 if ( params . path ) {
84- return `https://api.github.com/repos/ ${ params . owner } / ${ params . repo } /pulls/ ${ params . pullNumber } /comments`
156+ return `${ pullRequestUrl ( params ) } /comments`
85157 }
86- return `https://api.github.com/repos/ ${ params . owner } / ${ params . repo } /pulls/ ${ params . pullNumber } /reviews`
158+ return `${ pullRequestUrl ( params ) } /reviews`
87159 } ,
88- method : 'POST' ,
89- headers : ( params ) => ( {
90- Accept : 'application/vnd.github.v3+json' ,
91- Authorization : `Bearer ${ params . apiKey } ` ,
92- 'X-GitHub-Api-Version' : '2022-11-28' ,
93- } ) ,
160+ method : ( params ) => ( needsCommitLookup ( params ) ? 'GET' : 'POST' ) ,
161+ headers : ( params ) => githubHeaders ( params . apiKey ) ,
94162 body : ( params ) => {
163+ if ( needsCommitLookup ( params ) ) {
164+ return undefined
165+ }
95166 if ( params . commentType === 'file_comment' ) {
96- return {
97- body : params . body ,
98- commit_id : params . commitId ,
99- path : params . path ,
100- line : params . line || params . position ,
101- side : params . side || 'RIGHT' ,
102- }
167+ return fileCommentBody ( params , params . commitId as string )
103168 }
104169 return {
105170 body : params . body ,
@@ -108,8 +173,8 @@ export const commentTool: ToolConfig<CreateCommentParams, CreateCommentResponse>
108173 } ,
109174 } ,
110175
111- transformResponse : async ( response ) => {
112- const data = await response . json ( )
176+ transformResponse : async ( response , params ) => {
177+ const data = await readCommentPayload ( response , params )
113178
114179 // Create a human-readable content string
115180 const content = `Comment created: "${ data . body } "`
@@ -141,15 +206,15 @@ export const commentTool: ToolConfig<CreateCommentParams, CreateCommentResponse>
141206 } ,
142207}
143208
144- export const commentV2Tool : ToolConfig = {
209+ export const commentV2Tool : ToolConfig < CreateCommentParams > = {
145210 id : 'github_comment_v2' ,
146211 name : commentTool . name ,
147212 description : commentTool . description ,
148213 version : '2.0.0' ,
149214 params : commentTool . params ,
150215 request : commentTool . request ,
151- transformResponse : async ( response : Response ) => {
152- const data = await response . json ( )
216+ transformResponse : async ( response : Response , params ?: CreateCommentParams ) => {
217+ const data = await readCommentPayload ( response , params )
153218 return {
154219 success : true ,
155220 output : {
0 commit comments