From 0438688e326731ae3dd66f59637b3412214dcddd Mon Sep 17 00:00:00 2001 From: wolfy Date: Sun, 31 May 2026 01:43:42 -0500 Subject: [PATCH] fix: issues with pagination not resolving correct url templates --- main.ts | 10 +-- mcp/checkout.ts | 51 ++++++------ mcp/comment.ts | 162 ++++++++++----------------------------- mcp/commitInfo.ts | 49 +++++------- mcp/issue.ts | 51 +++++------- mcp/issueComments.ts | 24 +++--- mcp/issueInfo.ts | 49 +++++------- mcp/labels.ts | 32 ++++---- mcp/pr.ts | 81 ++++++++------------ mcp/prInfo.ts | 41 +++++----- mcp/review.ts | 46 ++++++----- mcp/reviewComments.ts | 84 +++++++------------- mcp/selectMode.ts | 11 ++- utils/progressComment.ts | 63 +++++++-------- 14 files changed, 287 insertions(+), 467 deletions(-) diff --git a/main.ts b/main.ts index c85bad7..4a7f40c 100644 --- a/main.ts +++ b/main.ts @@ -187,11 +187,11 @@ export async function main(): Promise { let defaultBranch: string | undefined; try { - const repoData = await gitea.rest.repository.repoGet({ - owner: repoContext.owner, - repo: repoContext.name, - }); - defaultBranch = repoData.data.default_branch; + const repoData = await gitea.request( + "GET /repos/{owner}/{repo}", + { owner: repoContext.owner, repo: repoContext.name } + ); + defaultBranch = (repoData.data as { default_branch?: string }).default_branch; } catch { defaultBranch = "main"; } diff --git a/mcp/checkout.ts b/mcp/checkout.ts index 2353c49..97829f2 100644 --- a/mcp/checkout.ts +++ b/mcp/checkout.ts @@ -136,15 +136,13 @@ export async function fetchAndFormatPrDiff( ctx: ToolContext, pullNumber: number ): Promise { - const raw = await ctx.gitea.paginate(ctx.gitea.rest.repository.repoGetPullRequestFiles, { - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: pullNumber, - }); - // ChangedFile from the SDK omits `patch`; Gitea does return it, so we map here. - const files: DiffFile[] = raw.map((f) => ({ + const r = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/pulls/{index}/files", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: pullNumber, limit: 50 } + ); + const files: DiffFile[] = (r.data as Array<{ filename?: string; patch?: string }>).map((f) => ({ filename: f.filename, - patch: (f as unknown as { patch?: string }).patch, + patch: f.patch, })); return { ...formatFilesWithLineNumbers(files), files }; } @@ -190,7 +188,8 @@ type CheckoutPrBranchParams = { async function abortIfPullRequestMoved(args: { gitea: Gitea; owner: string; repo: string; pr: PrData }): Promise { try { - const data = (await args.gitea.rest.repository.repoGetPullRequest({ owner: args.owner, repo: args.repo, index: args.pr.number })).data; + const r = await args.gitea.request("GET /repos/{owner}/{repo}/pulls/{index}", { owner: args.owner, repo: args.repo, index: args.pr.number }); + const data = r.data as { state?: string; head?: { sha?: string } }; if (data.state !== "open" || data.head?.sha !== args.pr.headSha) { throw new Error(`PR #${args.pr.number} is no longer in the state it was at dispatch. Aborting.`); } @@ -203,15 +202,14 @@ async function abortIfPullRequestMoved(args: { gitea: Gitea; owner: string; repo type CreateTempBranchParams = { gitea: Gitea; owner: string; repo: string; branchName: string; sha: string }; async function createTempBranch(params: CreateTempBranchParams) { - await params.gitea.rest.repository.repoCreateBranch({ - owner: params.owner, - repo: params.repo, - body: { new_branch_name: params.branchName, old_ref_name: params.sha }, + await params.gitea.request("POST /repos/{owner}/{repo}/branches", { + owner: params.owner, repo: params.repo, + new_branch_name: params.branchName, old_ref_name: params.sha, }); return { async [Symbol.asyncDispose]() { try { - await params.gitea.rest.repository.repoDeleteBranch({ owner: params.owner, repo: params.repo, branch: params.branchName }); + await params.gitea.request("DELETE /repos/{owner}/{repo}/branches/{branch}", { owner: params.owner, repo: params.repo, branch: params.branchName }); log.debug(`» deleted temp branch ${params.branchName}`); } catch (e) { log.debug(`» failed to delete temp branch ${params.branchName}: ${e instanceof Error ? e.message : String(e)}`); @@ -300,13 +298,13 @@ export async function checkoutPrBranch( let deepenDepth = 0; try { const [prComp, beforeComp] = await Promise.all([ - gitea.rest.repository.repoCompareDiff({ owner, repo: name, basehead: `${pr.baseRef}...${toolState.checkoutSha}` }), + gitea.request("GET /repos/{owner}/{repo}/compare/{basehead}", { owner, repo: name, basehead: `${pr.baseRef}...${toolState.checkoutSha}` }), beforeSha && beforeShaReachable - ? gitea.rest.repository.repoCompareDiff({ owner, repo: name, basehead: `${pr.baseRef}...${beforeSha}` }) + ? gitea.request("GET /repos/{owner}/{repo}/compare/{basehead}", { owner, repo: name, basehead: `${pr.baseRef}...${beforeSha}` }) : undefined, ]); - const prTotal = prComp.data.total_commits ?? 0; - const beforeTotal = beforeComp?.data.total_commits ?? 0; + const prTotal = (prComp.data as { total_commits?: number }).total_commits ?? 0; + const beforeTotal = (beforeComp?.data as { total_commits?: number } | undefined)?.total_commits ?? 0; deepenDepth = Math.max(prTotal, beforeTotal) + 10; log.debug(`» compare: PR=${prTotal}, before=${beforeTotal}, deepen=${deepenDepth}`); } catch { @@ -358,12 +356,17 @@ function describeHead(h: InitialHead): string { export function CheckoutPrTool(ctx: ToolContext) { const runCheckout = async (pull_number: number): Promise => { - const prResult = await ctx.gitea.rest.repository.repoGetPullRequest({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: pull_number, - }); - const prData = prResult.data; + const prResult = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/pulls/{index}", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number } + ); + const prData = prResult.data as { + number?: number; title?: string; body?: string | null; html_url?: string; + merged?: boolean; allow_maintainer_edit?: boolean; + head?: { sha?: string; ref?: string; repo?: { full_name?: string } | null }; + base?: { ref?: string; repo?: { full_name?: string } }; + state?: string; + }; const headRepo = prData.head?.repo; if (!headRepo) throw new Error(`PR #${pull_number} source repository was deleted`); diff --git a/mcp/comment.ts b/mcp/comment.ts index 415e8a6..dcd158e 100644 --- a/mcp/comment.ts +++ b/mcp/comment.ts @@ -15,57 +15,41 @@ export { LEAPING_INTO_ACTION_PREFIX, } from "../utils/leapingComment.ts"; +interface GiteaComment { id: number; body?: string | null; html_url?: string; updated_at?: string } + function buildCommentFooter(ctx: ToolContext): string { return buildShockbotFooter({ model: ctx.toolState.model }); } export function addFooter(ctx: ToolContext, body: string): string { if (/[ \t]*\n(?!\s*\n)/i.test(body)) { - throw new Error( - "body contains
followed by a non-blank line, which breaks Gitea markdown rendering. always add a blank line after
tags." - ); + throw new Error("body contains
followed by a non-blank line — add a blank line after
tags."); } - const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body)); - const footer = buildCommentFooter(ctx); - return `${bodyWithoutFooter}${footer}`; + return `${stripExistingFooter(fixDoubleEscapedString(body))}${buildCommentFooter(ctx)}`; } export const Comment = type({ issueNumber: type.number.describe("the issue number to comment on"), body: type.string.describe("the comment body content"), - type: type - .enumerated("Plan", "Comment") - .describe("Plan: record as the plan for this run. Comment: regular comment (default).") - .optional(), + type: type.enumerated("Plan", "Comment").optional(), }); export function CreateCommentTool(ctx: ToolContext) { return tool({ name: "create_issue_comment", description: - "Create a comment on a Gitea issue or PR. " + - 'Example: `create_issue_comment({ issueNumber: 1234, body: "Thanks for the report." })`. ' + - "For progress/plan updates use report_progress instead.", + "Create a comment on a Gitea issue or PR. For progress/plan updates use report_progress instead.", parameters: Comment, execute: execute(async ({ issueNumber, body }) => { const bodyWithFooter = addFooter(ctx, body); - - const result = await ctx.gitea.rest.issue.issueCreateComment({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: issueNumber, - body: { body: bodyWithFooter }, - }); - + const r = await ctx.gitea.request( + "POST /repos/{owner}/{repo}/issues/{index}/comments", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: issueNumber, body: bodyWithFooter } + ); + const data = r.data as GiteaComment; ctx.toolState.wasUpdated = true; - log.info(`» created comment ${result.data.id}`); - - return { - success: true, - commentId: result.data.id, - url: result.data.html_url, - body: result.data.body, - }; + log.info(`» created comment ${data.id}`); + return { success: true, commentId: data.id, url: data.html_url, body: data.body }; }), }); } @@ -82,48 +66,30 @@ export function EditCommentTool(ctx: ToolContext) { parameters: EditComment, execute: execute(async ({ commentId, body }) => { const bodyWithFooter = addFooter(ctx, body); - - const result = await ctx.gitea.rest.issue.issueEditComment({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - id: commentId, - body: { body: bodyWithFooter }, - }); - log.info(`» updated comment ${result.data.id}`); - - return { - success: true, - commentId: result.data.id, - url: result.data.html_url, - body: result.data.body, - updatedAt: result.data.updated_at, - }; + const r = await ctx.gitea.request( + "PATCH /repos/{owner}/{repo}/issues/comments/{id}", + { owner: ctx.repo.owner, repo: ctx.repo.name, id: commentId, body: bodyWithFooter } + ); + const data = r.data as GiteaComment; + log.info(`» updated comment ${data.id}`); + return { success: true, commentId: data.id, url: data.html_url, body: data.body, updatedAt: data.updated_at }; }), }); } export const ReportProgress = type({ body: type.string.describe("the progress update content to share"), - "target_plan_comment?": type("boolean").describe( - "for revising an existing plan comment ONLY." - ), + "target_plan_comment?": type("boolean"), }); export async function reportProgress( ctx: ToolContext, params: { body: string; target_plan_comment?: boolean } -): Promise<{ - commentId?: number; - url?: string; - body: string; - action: "created" | "updated" | "skipped"; -}> { +): Promise<{ commentId?: number; url?: string; body: string; action: "created" | "updated" | "skipped" }> { const { body, target_plan_comment } = params; ctx.toolState.lastProgressBody = body; - if (ctx.payload.event.silent) { - return { body, action: "skipped" }; - } + if (ctx.payload.event.silent) return { body, action: "skipped" }; const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber; const apiCtx = { gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name }; @@ -145,30 +111,14 @@ export async function reportProgress( return { commentId: result.id, url: result.html_url, body: result.body || "", action: "updated" }; } - if (existingComment === null) { - return { body, action: "skipped" }; - } - - if (issueNumber === undefined) { - return { body, action: "skipped" }; - } + if (existingComment === null) return { body, action: "skipped" }; + if (issueNumber === undefined) return { body, action: "skipped" }; const initialBody = addFooter(ctx, body); - const created = await createLeapingProgressComment( - apiCtx, - { kind: "issue", issueNumber }, - initialBody - ); - + const created = await createLeapingProgressComment(apiCtx, { kind: "issue", issueNumber }, initialBody); ctx.toolState.progressComment = created.comment; ctx.toolState.wasUpdated = true; - - return { - commentId: created.comment.id, - url: created.html_url, - body: created.body || "", - action: "created", - }; + return { commentId: created.comment.id, url: created.html_url, body: created.body || "", action: "created" }; } export function ReportProgressTool(ctx: ToolContext) { @@ -180,26 +130,18 @@ export function ReportProgressTool(ctx: ToolContext) { parameters: ReportProgress, execute: execute(async (params) => { let body = params.body; - if (!params.target_plan_comment && ctx.toolState.todoTracker) { ctx.toolState.todoTracker.cancel(); await ctx.toolState.todoTracker.settled(); const collapsible = ctx.toolState.todoTracker.renderCollapsible({ completeInProgress: true }); if (collapsible) body = `${body}\n\n${collapsible}`; } - const reportParams: { body: string; target_plan_comment?: boolean } = { body }; if (params.target_plan_comment !== undefined) reportParams.target_plan_comment = params.target_plan_comment; - const result = await reportProgress(ctx, reportParams); - - if (result.action === "skipped") { - return { success: true, message: "progress recorded (no comment created)" }; - } - + if (result.action === "skipped") return { success: true, message: "progress recorded (no comment created)" }; if (result.commentId !== undefined) log.info(`» ${result.action} comment ${result.commentId}`); if (!params.target_plan_comment) ctx.toolState.finalSummaryWritten = true; - return { success: true, ...result }; }), }); @@ -208,16 +150,11 @@ export function ReportProgressTool(ctx: ToolContext) { export async function deleteProgressComment(ctx: ToolContext): Promise { const existing = ctx.toolState.progressComment; if (!existing) return false; - try { - await deleteProgressCommentApi( - { gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name }, - existing - ); + await deleteProgressCommentApi({ gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name }, existing); } catch (error) { if (!(error instanceof Error && error.message.includes("Not Found"))) throw error; } - ctx.toolState.progressComment = null; return true; } @@ -229,10 +166,7 @@ export const ReplyToReviewComment = type({ }); export interface DuplicateReplyDecision { - kind: "already-replied"; - commentId: number; - url: string | undefined; - reason: string; + kind: "already-replied"; commentId: number; url: string | undefined; reason: string; } export function duplicateReplyDecision(params: { @@ -246,47 +180,33 @@ export function duplicateReplyDecision(params: { kind: "already-replied", commentId: existing.commentId, url: existing.url, - reason: `reply ${existing.commentId} with identical body was already posted in this session; ignoring duplicate call`, + reason: `reply ${existing.commentId} with identical body was already posted in this session`, }; } export function ReplyToReviewCommentTool(ctx: ToolContext) { return tool({ name: "reply_to_review_comment", - description: - "Reply to a PR review comment. Posts an issue comment on the PR referencing the original. " + - "Keep replies extremely brief (1 sentence max).", + description: "Reply to a PR review comment. Posts an issue comment on the PR. Keep replies to 1 sentence max.", parameters: ReplyToReviewComment, execute: execute(async ({ pull_number, comment_id, body }) => { const bodyWithFooter = addFooter(ctx, body); - - const dup = duplicateReplyDecision({ - existing: ctx.toolState.reviewReplies?.get(comment_id), - bodyWithFooter, - }); + const dup = duplicateReplyDecision({ existing: ctx.toolState.reviewReplies?.get(comment_id), bodyWithFooter }); if (dup) { log.info(`skipping duplicate review reply: ${dup.reason}`); return { success: true, skipped: true, reason: dup.reason, commentId: dup.commentId, url: dup.url }; } - const replyBody = `> Reply to review comment #${comment_id}\n\n${bodyWithFooter}`; - const result = await ctx.gitea.rest.issue.issueCreateComment({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: pull_number, - body: { body: replyBody }, - }); - log.info(`» created reply comment ${result.data.id}`); - + const r = await ctx.gitea.request( + "POST /repos/{owner}/{repo}/issues/{index}/comments", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number, body: replyBody } + ); + const data = r.data as GiteaComment; + log.info(`» created reply comment ${data.id}`); ctx.toolState.wasUpdated = true; ctx.toolState.reviewReplies ??= new Map(); - ctx.toolState.reviewReplies.set(comment_id, { - commentId: result.data.id!, - url: result.data.html_url, - bodyWithFooter, - }); - - return { success: true, commentId: result.data.id, url: result.data.html_url, body: result.data.body }; + ctx.toolState.reviewReplies.set(comment_id, { commentId: data.id!, url: data.html_url, bodyWithFooter }); + return { success: true, commentId: data.id, url: data.html_url, body: data.body }; }, "reply_to_review_comment"), }); } diff --git a/mcp/commitInfo.ts b/mcp/commitInfo.ts index c30575e..e394b8e 100644 --- a/mcp/commitInfo.ts +++ b/mcp/commitInfo.ts @@ -6,52 +6,43 @@ import { formatFilesWithLineNumbers, type DiffFile } from "./checkout.ts"; import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; -export const CommitInfo = type({ - sha: type.string.describe("the commit SHA (full or abbreviated) to fetch"), -}); +interface GiteaCommit { + sha?: string; html_url?: string; parents?: Array<{ sha?: string }>; + commit?: { message?: string; author?: { name?: string; date?: string }; committer?: { name?: string; date?: string } }; + author?: { login?: string }; committer?: { login?: string }; + stats?: { additions?: number; deletions?: number; total?: number }; + files?: Array<{ filename?: string; status?: string; patch?: string }>; +} + +export const CommitInfo = type({ sha: type.string.describe("the commit SHA to fetch") }); export function CommitInfoTool(ctx: ToolContext) { return tool({ name: "get_commit_info", - description: - "Retrieve commit metadata and diff via Gitea API. Returns diffPath pointing to formatted diff file. " + - 'Example: `get_commit_info({ sha: "2a6ab5d" })`.', + description: "Retrieve commit metadata and diff via Gitea API. Returns diffPath pointing to formatted diff file.", parameters: CommitInfo, execute: execute(async ({ sha }) => { - const result = await ctx.gitea.rest.repository.repoGetSingleCommit({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - sha, - }); - const data = result.data; - - // The SDK's CommitAffectedFiles type omits `patch`, which Gitea does return - // in practice. Cast explicitly so we can pass it to formatFilesWithLineNumbers. - const files: DiffFile[] = (data.files ?? []).map((f) => ({ - filename: f.filename, - patch: (f as unknown as { patch?: string }).patch, - })); - + const r = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/git/commits/{sha}", + { owner: ctx.repo.owner, repo: ctx.repo.name, sha } + ); + const data = r.data as GiteaCommit; + const files: DiffFile[] = (data.files ?? []).map((f) => ({ filename: f.filename, patch: f.patch })); const formatResult = formatFilesWithLineNumbers(files); const tempDir = process.env.SHOCKBOT_TEMP_DIR; - if (!tempDir) { - throw new Error("SHOCKBOT_TEMP_DIR not set - get_commit_info must run in shockbot action context"); - } + if (!tempDir) throw new Error("SHOCKBOT_TEMP_DIR not set"); const diffFile = join(tempDir, `commit-${sha.slice(0, 7)}.diff`); writeFileSync(diffFile, formatResult.content); - log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`); - + log.debug(`wrote commit diff to ${diffFile}`); return { - sha: data.sha, - message: data.commit?.message, + sha: data.sha, message: data.commit?.message, author: data.author?.login ?? data.commit?.author?.name ?? null, committer: data.committer?.login ?? data.commit?.committer?.name ?? null, date: data.commit?.author?.date ?? data.commit?.committer?.date ?? "", url: data.html_url, parents: (data.parents ?? []).map((p) => p.sha), stats: data.stats ?? { additions: 0, deletions: 0, total: 0 }, - fileCount: files.length, - diffFile, + fileCount: files.length, diffFile, }; }), }); diff --git a/mcp/issue.ts b/mcp/issue.ts index 44a79dd..a3b16e4 100644 --- a/mcp/issue.ts +++ b/mcp/issue.ts @@ -4,17 +4,16 @@ import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts"; import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; +interface GiteaIssue { + number: number; html_url: string; title: string; state: string; + labels?: Array<{ name?: string }>; assignees?: Array<{ login: string }>; +} + export const Issue = type({ title: type.string.describe("the title of the issue"), body: type.string.describe("the body content of the issue"), - labels: type.string - .array() - .describe("optional array of label names to apply to the issue") - .optional(), - assignees: type.string - .array() - .describe("optional array of usernames to assign to the issue") - .optional(), + labels: type.string.array().optional(), + assignees: type.string.array().optional(), }); export function IssueTool(ctx: ToolContext) { @@ -23,30 +22,20 @@ export function IssueTool(ctx: ToolContext) { description: "Create a new Gitea issue", parameters: Issue, execute: execute(async (params) => { - const result = await ctx.gitea.rest.issue.issueCreateIssue({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - body: { - title: params.title, - body: fixDoubleEscapedString(params.body), - assignees: params.assignees, - }, - }); - - log.info(`» created issue #${result.data.number}`); - + const r = await ctx.gitea.request( + "POST /repos/{owner}/{repo}/issues", + { + owner: ctx.repo.owner, repo: ctx.repo.name, + title: params.title, body: fixDoubleEscapedString(params.body), + ...(params.assignees ? { assignees: params.assignees } : {}), + } + ); + const data = r.data as GiteaIssue; + log.info(`» created issue #${data.number}`); return { - success: true, - number: result.data.number, - url: result.data.html_url, - title: result.data.title, - state: result.data.state, - labels: result.data.labels - ?.map((l) => (typeof l === "string" ? l : l.name)) - .filter((n): n is string => n !== undefined), - assignees: result.data.assignees - ?.map((a) => a.login) - .filter((n): n is string => n !== undefined), + success: true, number: data.number, url: data.html_url, title: data.title, state: data.state, + labels: data.labels?.map((l) => l.name).filter((n): n is string => n !== undefined), + assignees: data.assignees?.map((a) => a.login).filter((n): n is string => n !== undefined), }; }), }); diff --git a/mcp/issueComments.ts b/mcp/issueComments.ts index 0af9faf..869f93a 100644 --- a/mcp/issueComments.ts +++ b/mcp/issueComments.ts @@ -2,6 +2,8 @@ import { type } from "arktype"; import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; +interface GiteaComment { id: number; body?: string | null; user?: { login?: string } } + export const GetIssueComments = type({ issue_number: type.number.describe("The issue number to get comments for"), }); @@ -9,26 +11,18 @@ export const GetIssueComments = type({ export function GetIssueCommentsTool(ctx: ToolContext) { return tool({ name: "get_issue_comments", - description: - "Get all comments for a Gitea issue or PR. " + - "Example: `get_issue_comments({ issue_number: 1234 })`.", + description: "Get all comments for a Gitea issue or PR. Example: `get_issue_comments({ issue_number: 1234 })`.", parameters: GetIssueComments, execute: execute(async ({ issue_number }) => { ctx.toolState.issueNumber = issue_number; - - const comments = await ctx.gitea.paginate(ctx.gitea.rest.issue.issueGetComments, { - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: issue_number, - }); - + const r = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/issues/{index}/comments", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: issue_number, limit: 50 } + ); + const comments = r.data as GiteaComment[]; return { issue_number, - comments: comments.map((c) => ({ - id: c.id, - body: c.body, - user: c.user?.login, - })), + comments: comments.map((c) => ({ id: c.id, body: c.body, user: c.user?.login })), count: comments.length, }; }), diff --git a/mcp/issueInfo.ts b/mcp/issueInfo.ts index 449d143..0fda7cd 100644 --- a/mcp/issueInfo.ts +++ b/mcp/issueInfo.ts @@ -2,6 +2,13 @@ import { type } from "arktype"; import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; +interface GiteaIssue { + number: number; title: string; body?: string | null; state: string; html_url: string; + user?: { login: string }; labels?: Array<{ name?: string }>; assignees?: Array<{ login: string }>; + comments: number; created_at: string; updated_at: string; closed_at?: string | null; + milestone?: { title: string } | null; pull_request?: { html_url?: string } | null; +} + export const IssueInfo = type({ issue_number: type.number.describe("The issue number to fetch"), }); @@ -9,42 +16,24 @@ export const IssueInfo = type({ export function IssueInfoTool(ctx: ToolContext) { return tool({ name: "get_issue", - description: - "Retrieve Gitea issue information by issue number. " + - "Example: `get_issue({ issue_number: 1234 })`.", + description: "Retrieve Gitea issue information by issue number. Example: `get_issue({ issue_number: 1234 })`.", parameters: IssueInfo, execute: execute(async ({ issue_number }) => { - const result = await ctx.gitea.rest.issue.issueGetIssue({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: issue_number, - }); - const data = result.data; + const r = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/issues/{index}", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: issue_number } + ); + const data = r.data as GiteaIssue; ctx.toolState.issueNumber = issue_number; - const hints: string[] = []; - if ((data.comments ?? 0) > 0) { - hints.push("use get_issue_comments to retrieve all comments for this issue"); - } - + if (data.comments > 0) hints.push("use get_issue_comments to retrieve all comments for this issue"); return { - number: data.number, - url: data.html_url, - title: data.title, - body: data.body, + number: data.number, url: data.html_url, title: data.title, body: data.body, state: data.state, - labels: data.labels - ?.map((l) => (typeof l === "string" ? l : l.name)) - .filter((n): n is string => n !== undefined), - assignees: data.assignees - ?.map((a) => a.login) - .filter((n): n is string => n !== undefined), - user: data.user?.login, - created_at: data.created_at, - updated_at: data.updated_at, - closed_at: data.closed_at, - comments: data.comments, - milestone: data.milestone?.title, + labels: data.labels?.map((l) => l.name).filter((n): n is string => n !== undefined), + assignees: data.assignees?.map((a) => a.login).filter((n): n is string => n !== undefined), + user: data.user?.login, created_at: data.created_at, updated_at: data.updated_at, + closed_at: data.closed_at, comments: data.comments, milestone: data.milestone?.title, pull_request: data.pull_request ? { html_url: data.pull_request.html_url } : null, hints, }; diff --git a/mcp/labels.ts b/mcp/labels.ts index 009c16c..6c0d451 100644 --- a/mcp/labels.ts +++ b/mcp/labels.ts @@ -3,6 +3,8 @@ import { log } from "../utils/cli.ts"; import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; +interface GiteaLabel { id: number; name?: string } + export const AddLabelsParams = type({ issue_number: type.number.describe("the issue or PR number to add labels to"), labels: type.string.array().atLeastLength(1).describe("array of label names to add"), @@ -11,15 +13,14 @@ export const AddLabelsParams = type({ export function AddLabelsTool(ctx: ToolContext) { return tool({ name: "add_labels", - description: - "Add labels to a Gitea issue or pull request. Only use labels that already exist in the repository.", + description: "Add labels to a Gitea issue or pull request. Only use labels that already exist in the repository.", parameters: AddLabelsParams, execute: execute(async ({ issue_number, labels }) => { - // Resolve label names to IDs (Gitea uses IDs, not names) - const allLabels = await ctx.gitea.paginate(ctx.gitea.rest.issue.issueListLabels, { - owner: ctx.repo.owner, - repo: ctx.repo.name, - }); + const allLabelsR = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/labels", + { owner: ctx.repo.owner, repo: ctx.repo.name, limit: 50 } + ); + const allLabels = allLabelsR.data as GiteaLabel[]; const labelIds = labels .map((name) => allLabels.find((l) => l.name === name)?.id) .filter((id): id is number => typeof id === "number"); @@ -28,18 +29,13 @@ export function AddLabelsTool(ctx: ToolContext) { return { success: true, labels: [], message: "No matching labels found in repository" }; } - const result = await ctx.gitea.rest.issue.issueAddLabel({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: issue_number, - body: { labels: labelIds }, - }); + const r = await ctx.gitea.request( + "POST /repos/{owner}/{repo}/issues/{index}/labels", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: issue_number, labels: labelIds } + ); log.info(`» added labels [${labels.join(", ")}] to issue #${issue_number}`); - - return { - success: true, - labels: result.data.map((label) => label.name).filter(Boolean), - }; + const result = r.data as GiteaLabel[]; + return { success: true, labels: result.map((l) => l.name).filter(Boolean) }; }), }); } diff --git a/mcp/pr.ts b/mcp/pr.ts index a36a50c..9c4bcb1 100644 --- a/mcp/pr.ts +++ b/mcp/pr.ts @@ -6,16 +6,10 @@ import { $ } from "../utils/shell.ts"; import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; -export const PullRequest = type({ - title: type.string.describe("the title of the pull request"), - body: type.string.describe("the body content of the pull request"), - base: type.string.describe("the base branch to merge into (e.g., 'main')"), - "draft?": type.boolean.describe("if true, create the pull request as a draft."), -}); +interface GiteaPull { number: number; html_url?: string; title?: string; head?: { ref?: string }; base?: { ref?: string } } function buildPrBodyWithFooter(ctx: ToolContext, body: string): string { - const footer = buildShockbotFooter({ model: ctx.toolState.model }); - return `${stripExistingFooter(fixDoubleEscapedString(body))}${footer}`; + return `${stripExistingFooter(fixDoubleEscapedString(body))}${buildShockbotFooter({ model: ctx.toolState.model })}`; } export const UpdatePullRequestBody = type({ @@ -29,19 +23,25 @@ export function UpdatePullRequestBodyTool(ctx: ToolContext) { description: "Update the body/description of an existing pull request", parameters: UpdatePullRequestBody, execute: execute(async (params) => { - const result = await ctx.gitea.rest.repository.repoEditPullRequest({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: params.pull_number, - body: { body: buildPrBodyWithFooter(ctx, params.body) }, - }); - log.info(`» updated pull request #${result.data.number}`); + const r = await ctx.gitea.request( + "PATCH /repos/{owner}/{repo}/pulls/{index}", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: params.pull_number, body: buildPrBodyWithFooter(ctx, params.body) } + ); + const data = r.data as GiteaPull; + log.info(`» updated pull request #${data.number}`); ctx.toolState.wasUpdated = true; - return { success: true, number: result.data.number, url: result.data.html_url }; + return { success: true, number: data.number, url: data.html_url }; }), }); } +export const PullRequest = type({ + title: type.string.describe("the title of the pull request"), + body: type.string.describe("the body content of the pull request"), + base: type.string.describe("the base branch to merge into (e.g., 'main')"), + "draft?": type.boolean, +}); + export function CreatePullRequestTool(ctx: ToolContext) { return tool({ name: "create_pull_request", @@ -49,42 +49,27 @@ export function CreatePullRequestTool(ctx: ToolContext) { parameters: PullRequest, execute: execute(async (params) => { const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim(); - - const result = await ctx.gitea.rest.repository.repoCreatePullRequest({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - body: { - title: params.title, - body: buildPrBodyWithFooter(ctx, params.body), - head: currentBranch, - base: params.base, - // draft: params.draft ?? false, // not in all Gitea versions - }, - }); - log.info(`» created pull request #${result.data.number}`); + const r = await ctx.gitea.request( + "POST /repos/{owner}/{repo}/pulls", + { + owner: ctx.repo.owner, repo: ctx.repo.name, + title: params.title, body: buildPrBodyWithFooter(ctx, params.body), + head: currentBranch, base: params.base, + } + ); + const data = r.data as GiteaPull; + log.info(`» created pull request #${data.number}`); const reviewer = ctx.payload.triggerer; - if (reviewer && result.data.number) { + if (reviewer && data.number) { try { - await ctx.gitea.rest.repository.repoCreatePullReviewRequests({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: result.data.number, - body: { reviewers: [reviewer] }, - }); - } catch { - log.debug(`failed to request review from ${reviewer}`); - } + await ctx.gitea.request( + "POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: data.number, reviewers: [reviewer] } + ); + } catch { log.debug(`failed to request review from ${reviewer}`); } } - - return { - success: true, - number: result.data.number, - url: result.data.html_url, - title: result.data.title, - head: result.data.head?.label ?? result.data.head?.ref, - base: result.data.base?.label ?? result.data.base?.ref, - }; + return { success: true, number: data.number, url: data.html_url, title: data.title, head: data.head?.ref, base: data.base?.ref }; }), }); } diff --git a/mcp/prInfo.ts b/mcp/prInfo.ts index 92a9422..5a1af9f 100644 --- a/mcp/prInfo.ts +++ b/mcp/prInfo.ts @@ -2,6 +2,15 @@ import { type } from "arktype"; import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; +interface GiteaPull { + number: number; html_url: string; title: string; body?: string | null; + state: string; draft?: boolean; merged?: boolean; allow_maintainer_edit?: boolean; + head?: { sha: string; ref: string; repo?: { full_name: string } | null }; + base?: { ref: string; repo?: { full_name: string } }; + user?: { login: string }; assignees?: Array<{ login: string }>; + labels?: Array; +} + export const PullRequestInfo = type({ pull_number: type.number.describe("The pull request number to fetch"), }); @@ -11,34 +20,22 @@ export function PullRequestInfoTool(ctx: ToolContext) { name: "get_pull_request", description: "Retrieve PR metadata (title, body, state, branches, author, labels). " + - "Example: `get_pull_request({ pull_number: 1234 })`. " + - "To checkout a PR branch locally, use checkout_pr instead.", + "Example: `get_pull_request({ pull_number: 1234 })`. To checkout a PR branch locally, use checkout_pr instead.", parameters: PullRequestInfo, execute: execute(async ({ pull_number }) => { - const result = await ctx.gitea.rest.repository.repoGetPullRequest({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: pull_number, - }); - const data = result.data; + const r = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/pulls/{index}", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number } + ); + const data = r.data as GiteaPull; const isFork = data.head?.repo?.full_name !== data.base?.repo?.full_name; - return { - number: data.number, - url: data.html_url, - title: data.title, - body: data.body, - state: data.state, - draft: data.draft, - merged: data.merged, + number: data.number, url: data.html_url, title: data.title, body: data.body, + state: data.state, draft: data.draft, merged: data.merged, maintainerCanModify: data.allow_maintainer_edit, - base: data.base?.label, - head: data.head?.label, - isFork, + base: data.base?.ref, head: data.head?.ref, isFork, author: data.user?.login, - assignees: data.assignees - ?.map((a) => a.login) - .filter((n): n is string => n !== undefined), + assignees: data.assignees?.map((a) => a.login).filter((n): n is string => n !== undefined), labels: data.labels ?.map((l) => (typeof l === "string" ? l : l.name)) .filter((n): n is string => n !== undefined), diff --git a/mcp/review.ts b/mcp/review.ts index 83184e6..244f276 100644 --- a/mcp/review.ts +++ b/mcp/review.ts @@ -62,11 +62,11 @@ export async function buildCommentableMap( const currentSha = ctx.toolState.checkoutSha; if (cached && cachedFor === pullNumber && cachedSha && cachedSha === currentSha) return cached; - const files = (await ctx.gitea.paginate(ctx.gitea.rest.repository.repoGetPullRequestFiles, { - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: pullNumber, - })) as ChangedFileWithPatch[]; + const r = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/pulls/{index}/files", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: pullNumber, limit: 50 } + ); + const files = r.data as ChangedFileWithPatch[]; const map = new Map(); for (const file of files) { @@ -244,12 +244,11 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) { let effectiveCommitId = commit_id; if (!effectiveCommitId) { try { - const pr = await ctx.gitea.rest.repository.repoGetPullRequest({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: pull_number, - }); - latestHeadSha = pr.data.head?.sha; + const pr = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/pulls/{index}", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number } + ); + latestHeadSha = (pr.data as { head?: { sha?: string } }).head?.sha; effectiveCommitId = ctx.toolState.checkoutSha ?? latestHeadSha; } catch { effectiveCommitId = ctx.toolState.checkoutSha; @@ -301,18 +300,16 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) { const footer = buildShockbotFooter({ model: ctx.toolState.model }); const fullBody = body ? `${body}${footer}` : footer.trimStart(); - const result = await retry>>( + const result = await retry( () => - ctx.gitea.rest.repository.repoCreatePullReview({ + ctx.gitea.request("POST /repos/{owner}/{repo}/pulls/{index}/reviews", { owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number, - body: { - body: fullBody, - commit_id: effectiveCommitId, - event, - comments: sdkComments, - }, + body: fullBody, + commit_id: effectiveCommitId, + event, + comments: sdkComments, }), { delaysMs: [1_000, 3_000], @@ -321,7 +318,8 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) { } ); - const reviewId = result.data.id!; + const reviewData = result.data as { id?: number; html_url?: string; state?: string }; + const reviewId = reviewData.id!; log.info(`» created review ${reviewId} on pull request #${pull_number}`); ctx.toolState.review = { @@ -344,8 +342,8 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) { return { success: true, reviewId, - html_url: result.data.html_url, - state: result.data.state, + html_url: reviewData.html_url, + state: reviewData.state, droppedComments: droppedComments.length > 0 ? droppedComments : undefined, newCommits: { from: fromSha, @@ -358,8 +356,8 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) { return { success: true, reviewId, - html_url: result.data.html_url, - state: result.data.state, + html_url: reviewData.html_url, + state: reviewData.state, droppedComments: droppedComments.length > 0 ? droppedComments : undefined, }; }), diff --git a/mcp/reviewComments.ts b/mcp/reviewComments.ts index a912097..59de1d9 100644 --- a/mcp/reviewComments.ts +++ b/mcp/reviewComments.ts @@ -1,57 +1,45 @@ import { writeFileSync } from "node:fs"; import { join } from "node:path"; import { type } from "arktype"; -import type { PullReviewComment } from "@go-gitea/sdk.js"; import { stripExistingFooter } from "../utils/buildShockbotFooter.ts"; import { log } from "../utils/log.ts"; import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; -export const GetReviewComments = type({ - pull_number: type.number.describe("The pull request number to get review comments for"), -}); +interface GiteaReview { id: number; user?: { login: string }; state?: string; body?: string; submitted_at?: string; commit_id?: string } +interface GiteaReviewComment { id: number; body?: string; path?: string; diff_hunk?: string; original_position?: number; position?: number; pull_request_review_id?: number; user?: { login: string } } + +export const GetReviewComments = type({ pull_number: type.number }); export function GetReviewCommentsTool(ctx: ToolContext) { return tool({ name: "get_review_comments", - description: - "Get all inline review comments for a pull request. " + - "Example: `get_review_comments({ pull_number: 1234 })`.", + description: "Get all inline review comments for a pull request. Example: `get_review_comments({ pull_number: 1234 })`.", parameters: GetReviewComments, execute: execute(async ({ pull_number }) => { ctx.toolState.issueNumber = pull_number; + const reviewsR = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/pulls/{index}/reviews", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number, limit: 50 } + ); + const reviews = reviewsR.data as GiteaReview[]; - // Get all reviews, then collect their comments - const reviews = await ctx.gitea.paginate(ctx.gitea.rest.repository.repoListPullReviews, { - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: pull_number, - }); - - const allComments: PullReviewComment[] = []; + const allComments: GiteaReviewComment[] = []; for (const review of reviews) { if (!review.id) continue; try { - const commentsResult = await ctx.gitea.rest.repository.repoGetPullReviewComments({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: pull_number, - id: review.id, - }); - allComments.push(...commentsResult.data); - } catch { - // best-effort - } + const cr = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number, id: review.id } + ); + allComments.push(...(cr.data as GiteaReviewComment[])); + } catch { /* best-effort */ } } const tempDir = process.env.SHOCKBOT_TEMP_DIR; let filePath: string | undefined; - const rendered = allComments.map((c) => ({ - id: c.id, - path: c.path, - // PullReviewComment uses `position` for the diff offset; Gitea doesn't - // expose separate new_position/old_position on GET — use original_position as fallback + id: c.id, path: c.path, line: c.original_position ?? c.position, side: "RIGHT" as const, body: stripExistingFooter(c.body ?? ""), @@ -65,45 +53,27 @@ export function GetReviewCommentsTool(ctx: ToolContext) { writeFileSync(filePath, JSON.stringify(rendered, null, 2)); log.debug(`wrote review comments to ${filePath}`); } - - return { - pull_number, - comments: rendered, - count: rendered.length, - ...(filePath ? { filePath } : {}), - }; + return { pull_number, comments: rendered, count: rendered.length, ...(filePath ? { filePath } : {}) }; }), }); } -export const ListPullRequestReviews = type({ - pull_number: type.number.describe("The pull request number"), -}); +export const ListPullRequestReviews = type({ pull_number: type.number }); export function ListPullRequestReviewsTool(ctx: ToolContext) { return tool({ name: "list_pull_request_reviews", - description: - "List all reviews submitted on a pull request. " + - "Example: `list_pull_request_reviews({ pull_number: 1234 })`.", + description: "List all reviews submitted on a pull request. Example: `list_pull_request_reviews({ pull_number: 1234 })`.", parameters: ListPullRequestReviews, execute: execute(async ({ pull_number }) => { - const reviews = await ctx.gitea.paginate(ctx.gitea.rest.repository.repoListPullReviews, { - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: pull_number, - }); - + const r = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/pulls/{index}/reviews", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number, limit: 50 } + ); + const reviews = r.data as GiteaReview[]; return { pull_number, - reviews: reviews.map((r) => ({ - id: r.id, - user: r.user?.login, - state: r.state, - body: r.body, - submitted_at: r.submitted_at, - commit_id: r.commit_id, - })), + reviews: reviews.map((r) => ({ id: r.id, user: r.user?.login, state: r.state, body: r.body, submitted_at: r.submitted_at, commit_id: r.commit_id })), count: reviews.length, }; }), diff --git a/mcp/selectMode.ts b/mcp/selectMode.ts index 03f06d7..e6b82d5 100644 --- a/mcp/selectMode.ts +++ b/mcp/selectMode.ts @@ -67,12 +67,11 @@ export function SelectModeTool(ctx: ToolContext) { // For Plan mode with issue_number, look up existing plan comment if (foundMode.name === "Plan" && issue_number !== undefined) { try { - const commentsResp = await ctx.gitea.paginate(ctx.gitea.rest.issue.issueGetComments, { - owner: ctx.repo.owner, - repo: ctx.repo.name, - index: issue_number, - }); - const comments = commentsResp; + const commentsR = await ctx.gitea.request( + "GET /repos/{owner}/{repo}/issues/{index}/comments", + { owner: ctx.repo.owner, repo: ctx.repo.name, index: issue_number, limit: 50 } + ); + const comments = commentsR.data as Array<{ id?: number; body?: string | null }>; // Look for a plan comment (one with our footer) const planComment = comments.find((c) => c.body?.includes("")); if (planComment) { diff --git a/utils/progressComment.ts b/utils/progressComment.ts index b5b07ab..6ea8712 100644 --- a/utils/progressComment.ts +++ b/utils/progressComment.ts @@ -1,6 +1,5 @@ /** - * Single source of truth for reading, updating, deleting, and creating "progress comments" — - * the Gitea comments shockbot uses to surface a run's status. + * Single source of truth for reading, updating, deleting, and creating "progress comments". */ import type { Gitea } from "./gitea.ts"; @@ -27,16 +26,22 @@ interface ApiCtx { repo: string; } +interface GiteaComment { + id: number; + body?: string | null; + html_url?: string; + node_id?: string; +} + export async function getProgressComment( ctx: ApiCtx, comment: ProgressComment ): Promise<{ id: number; body: string | undefined; html_url: string }> { - const result = await ctx.gitea.rest.issue.issueGetComment({ - owner: ctx.owner, - repo: ctx.repo, - id: comment.id, + const r = await ctx.gitea.request("GET /repos/{owner}/{repo}/issues/comments/{id}", { + owner: ctx.owner, repo: ctx.repo, id: comment.id, }); - return { id: result.data.id!, body: result.data.body ?? undefined, html_url: result.data.html_url! }; + const data = r.data as GiteaComment; + return { id: data.id, body: data.body ?? undefined, html_url: data.html_url ?? "" }; } export async function updateProgressComment( @@ -44,28 +49,16 @@ export async function updateProgressComment( comment: ProgressComment, body: string ): Promise<{ id: number; body: string | undefined; html_url: string; node_id: string | undefined }> { - const result = await ctx.gitea.rest.issue.issueEditComment({ - owner: ctx.owner, - repo: ctx.repo, - id: comment.id, - body: { body }, + const r = await ctx.gitea.request("PATCH /repos/{owner}/{repo}/issues/comments/{id}", { + owner: ctx.owner, repo: ctx.repo, id: comment.id, body, }); - return { - id: result.data.id!, - body: result.data.body ?? undefined, - html_url: result.data.html_url!, - node_id: undefined, - }; + const data = r.data as GiteaComment; + return { id: data.id, body: data.body ?? undefined, html_url: data.html_url ?? "", node_id: undefined }; } -export async function deleteProgressCommentApi( - ctx: ApiCtx, - comment: ProgressComment -): Promise { - await ctx.gitea.rest.issue.issueDeleteComment({ - owner: ctx.owner, - repo: ctx.repo, - id: comment.id, +export async function deleteProgressCommentApi(ctx: ApiCtx, comment: ProgressComment): Promise { + await ctx.gitea.request("DELETE /repos/{owner}/{repo}/issues/comments/{id}", { + owner: ctx.owner, repo: ctx.repo, id: comment.id, }); } @@ -84,18 +77,14 @@ export async function createLeapingProgressComment( target: CreateProgressCommentTarget, body: string ): Promise { - const issueNumber = - target.kind === "issue" ? target.issueNumber : target.pullNumber; - - const result = await ctx.gitea.rest.issue.issueCreateComment({ - owner: ctx.owner, - repo: ctx.repo, - index: issueNumber, - body: { body }, + const issueNumber = target.kind === "issue" ? target.issueNumber : target.pullNumber; + const r = await ctx.gitea.request("POST /repos/{owner}/{repo}/issues/{index}/comments", { + owner: ctx.owner, repo: ctx.repo, index: issueNumber, body, }); + const data = r.data as GiteaComment; return { - comment: { id: result.data.id!, type: "issue" }, - body: result.data.body ?? undefined, - html_url: result.data.html_url!, + comment: { id: data.id, type: "issue" }, + body: data.body ?? undefined, + html_url: data.html_url ?? "", }; }