From 879a415281a81c2b982a167b092676c415049615 Mon Sep 17 00:00:00 2001 From: Hugo Nijhuis Date: Tue, 12 May 2026 13:11:12 +0200 Subject: [PATCH] feat: add dedicated issue close/reopen and PR review tools --- .opencode/tools/tea-issue-close.ts | 82 +++++++++++++ .opencode/tools/tea-issue-update.ts | 155 ++++++++++++++++++++++++ .opencode/tools/tea-label-update.ts | 100 +++++++++++++++ .opencode/tools/tea-milestone-update.ts | 99 +++++++++++++++ .opencode/tools/tea-pr-review.ts | 110 +++++++++++++++++ 5 files changed, 546 insertions(+) create mode 100644 .opencode/tools/tea-issue-close.ts create mode 100644 .opencode/tools/tea-issue-update.ts create mode 100644 .opencode/tools/tea-label-update.ts create mode 100644 .opencode/tools/tea-milestone-update.ts create mode 100644 .opencode/tools/tea-pr-review.ts diff --git a/.opencode/tools/tea-issue-close.ts b/.opencode/tools/tea-issue-close.ts new file mode 100644 index 0000000..c17a6be --- /dev/null +++ b/.opencode/tools/tea-issue-close.ts @@ -0,0 +1,82 @@ +import { tool } from "@opencode-ai/plugin"; +import { $ } from "bun"; + +export const issueClose = tool({ + description: "Close one or more issues", + args: { + issueNumbers: tool.schema + .array(tool.schema.number()) + .describe("Issue number(s) to close"), + comment: tool.schema + .string() + .optional() + .describe("Optional comment explaining why the issue is being closed"), + }, + async execute(args) { + try { + const issueListStr = args.issueNumbers.join(" "); + + let cmd = $`tea issues close --output json ${issueListStr}`; + + if (args.comment) { + cmd = cmd`--comment ${args.comment}`; + } + + const result = await cmd.text(); + + return { + success: true, + message: `Successfully closed issue(s) #${args.issueNumbers.join(", ")}`, + issues: args.issueNumbers, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to close issue(s)", + message: error.message, + stderr: error.stderr?.toString(), + issues: args.issueNumbers, + }; + } + }, +}); + +export const issueReopen = tool({ + description: "Reopen one or more closed issues", + args: { + issueNumbers: tool.schema + .array(tool.schema.number()) + .describe("Issue number(s) to reopen"), + comment: tool.schema + .string() + .optional() + .describe("Optional comment explaining why the issue is being reopened"), + }, + async execute(args) { + try { + const issueListStr = args.issueNumbers.join(" "); + + let cmd = $`tea issues reopen --output json ${issueListStr}`; + + if (args.comment) { + cmd = cmd`--comment ${args.comment}`; + } + + const result = await cmd.text(); + + return { + success: true, + message: `Successfully reopened issue(s) #${args.issueNumbers.join(", ")}`, + issues: args.issueNumbers, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to reopen issue(s)", + message: error.message, + stderr: error.stderr?.toString(), + issues: args.issueNumbers, + }; + } + }, +}); \ No newline at end of file diff --git a/.opencode/tools/tea-issue-update.ts b/.opencode/tools/tea-issue-update.ts new file mode 100644 index 0000000..ad35326 --- /dev/null +++ b/.opencode/tools/tea-issue-update.ts @@ -0,0 +1,155 @@ +import { tool } from "@opencode-ai/plugin"; +import { $ } from "bun"; + +export const issueUpdateTitle = tool({ + description: "Update the title of an issue", + args: { + issueNumber: tool.schema.number().describe("Issue number"), + title: tool.schema.string().describe("New title"), + }, + async execute(args) { + try { + const result = await $`tea issues edit --output json --title ${args.title} ${args.issueNumber}`.text(); + + return { + success: true, + message: `Successfully updated title for issue #${args.issueNumber}`, + issueNumber: args.issueNumber, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to update issue title", + message: error.message, + stderr: error.stderr?.toString(), + issueNumber: args.issueNumber, + }; + } + }, +}); + +export const issueUpdateDescription = tool({ + description: "Update the description/body of an issue", + args: { + issueNumber: tool.schema.number().describe("Issue number"), + description: tool.schema.string().describe("New description/body"), + }, + async execute(args) { + try { + const result = await $`tea issues edit --output json --description ${args.description} ${args.issueNumber}`.text(); + + return { + success: true, + message: `Successfully updated description for issue #${args.issueNumber}`, + issueNumber: args.issueNumber, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to update issue description", + message: error.message, + stderr: error.stderr?.toString(), + issueNumber: args.issueNumber, + }; + } + }, +}); + +export const issueAddLabels = tool({ + description: "Add labels to one or more issues", + args: { + issueNumbers: tool.schema + .array(tool.schema.number()) + .describe("Issue number(s) to add labels to"), + labels: tool.schema + .array(tool.schema.string()) + .describe("Label names to add"), + }, + async execute(args) { + try { + const issueListStr = args.issueNumbers.join(" "); + const labelsStr = args.labels.join(","); + + const result = await $`tea issues edit --output json --add-labels ${labelsStr} ${issueListStr}`.text(); + + return { + success: true, + message: `Successfully added label(s) "${labelsStr}" to issue(s) #${args.issueNumbers.join(", ")}`, + issueNumbers: args.issueNumbers, + labels: args.labels, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to add labels to issue(s)", + message: error.message, + stderr: error.stderr?.toString(), + issueNumbers: args.issueNumbers, + labels: args.labels, + }; + } + }, +}); + +export const issueRemoveLabels = tool({ + description: "Remove labels from one or more issues", + args: { + issueNumbers: tool.schema + .array(tool.schema.number()) + .describe("Issue number(s) to remove labels from"), + labels: tool.schema + .array(tool.schema.string()) + .describe("Label names to remove"), + }, + async execute(args) { + try { + const issueListStr = args.issueNumbers.join(" "); + const labelsStr = args.labels.join(","); + + const result = await $`tea issues edit --output json --remove-labels ${labelsStr} ${issueListStr}`.text(); + + return { + success: true, + message: `Successfully removed label(s) "${labelsStr}" from issue(s) #${args.issueNumbers.join(", ")}`, + issueNumbers: args.issueNumbers, + labels: args.labels, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to remove labels from issue(s)", + message: error.message, + stderr: error.stderr?.toString(), + issueNumbers: args.issueNumbers, + labels: args.labels, + }; + } + }, +}); + +export const issueAddComment = tool({ + description: "Add a comment to an issue", + args: { + issueNumber: tool.schema.number().describe("Issue number"), + comment: tool.schema.string().describe("Comment text"), + }, + async execute(args) { + try { + const result = await $`tea issue comment --output json --issue ${args.issueNumber} --body ${args.comment}`.text(); + + return { + success: true, + message: `Successfully added comment to issue #${args.issueNumber}`, + issueNumber: args.issueNumber, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to add comment to issue", + message: error.message, + stderr: error.stderr?.toString(), + issueNumber: args.issueNumber, + }; + } + }, +}); \ No newline at end of file diff --git a/.opencode/tools/tea-label-update.ts b/.opencode/tools/tea-label-update.ts new file mode 100644 index 0000000..3f77295 --- /dev/null +++ b/.opencode/tools/tea-label-update.ts @@ -0,0 +1,100 @@ +import { tool } from "@opencode-ai/plugin"; +import { $ } from "bun"; + +export const labelCreate = tool({ + description: "Create a new label with name, color, and optional description", + args: { + name: tool.schema.string().describe("Label name"), + color: tool.schema.string().describe("Hex color code (e.g., #e99695)"), + description: tool.schema + .string() + .optional() + .describe("Label description"), + }, + async execute(args) { + try { + let cmd = $`tea label create --output json --name ${args.name} --color ${args.color}`; + + if (args.description) { + cmd = cmd`--description ${args.description}`; + } + + const result = await cmd.text(); + return result; + } catch (error: any) { + return { + error: "Failed to create label", + message: error.message, + stderr: error.stderr?.toString(), + }; + } + }, +}); + +export const labelUpdate = tool({ + description: "Update an existing label's name, color, or description", + args: { + labelName: tool.schema.string().describe("Current label name"), + newName: tool.schema + .string() + .optional() + .describe("New label name"), + newColor: tool.schema + .string() + .optional() + .describe("New hex color code"), + newDescription: tool.schema + .string() + .optional() + .describe("New description"), + }, + async execute(args) { + try { + let cmd = $`tea label update --output json --name ${args.labelName}`; + + if (args.newName) { + cmd = cmd`--new-name ${args.newName}`; + } + + if (args.newColor) { + cmd = cmd`--new-color ${args.newColor}`; + } + + if (args.newDescription) { + cmd = cmd`--new-description ${args.newDescription}`; + } + + const result = await cmd.text(); + return result; + } catch (error: any) { + return { + error: "Failed to update label", + message: error.message, + stderr: error.stderr?.toString(), + }; + } + }, +}); + +export const labelDelete = tool({ + description: "Delete a label from the repository", + args: { + labelName: tool.schema.string().describe("Label name to delete"), + }, + async execute(args) { + try { + const result = await $`tea label delete --output json --name ${args.labelName}`.text(); + return { + success: true, + message: `Successfully deleted label "${args.labelName}"`, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to delete label", + message: error.message, + stderr: error.stderr?.toString(), + }; + } + }, +}); \ No newline at end of file diff --git a/.opencode/tools/tea-milestone-update.ts b/.opencode/tools/tea-milestone-update.ts new file mode 100644 index 0000000..95a0cdc --- /dev/null +++ b/.opencode/tools/tea-milestone-update.ts @@ -0,0 +1,99 @@ +import { tool } from "@opencode-ai/plugin"; +import { $ } from "bun"; + +export const milestoneUpdate = tool({ + description: "Update an existing milestone with new title, description, due date, or state", + args: { + milestoneNumber: tool.schema.number().describe("Milestone number"), + title: tool.schema + .string() + .optional() + .describe("New milestone title"), + description: tool.schema + .string() + .optional() + .describe("New milestone description"), + dueDate: tool.schema + .string() + .optional() + .describe("New due date (format: YYYY-MM-DD)"), + state: tool.schema + .enum(["open", "closed"]) + .optional() + .describe("New milestone state"), + }, + async execute(args) { + try { + let cmd = $`tea milestone edit --output json ${args.milestoneNumber}`; + + if (args.title) { + cmd = cmd`--title ${args.title}`; + } + + if (args.description) { + cmd = cmd`--description ${args.description}`; + } + + if (args.dueDate) { + cmd = cmd`--due-date ${args.dueDate}`; + } + + if (args.state) { + cmd = cmd`--state ${args.state}`; + } + + const result = await cmd.text(); + + return { + success: true, + message: `Successfully updated milestone #${args.milestoneNumber}`, + milestoneNumber: args.milestoneNumber, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to update milestone", + message: error.message, + stderr: error.stderr?.toString(), + milestoneNumber: args.milestoneNumber, + }; + } + }, +}); + +export const milestoneDelete = tool({ + description: "Delete a milestone from the repository", + args: { + milestoneNumber: tool.schema.number().describe("Milestone number to delete"), + confirm: tool.schema + .boolean() + .optional() + .describe("Confirmation flag (set to true to confirm deletion)"), + }, + async execute(args) { + try { + if (args.confirm !== true) { + return { + error: "Deletion not confirmed", + message: "Please set confirm: true to confirm deletion", + }; + } + + const result = await $`tea milestone delete --output json ${args.milestoneNumber}`.text(); + + return { + success: true, + message: `Successfully deleted milestone #${args.milestoneNumber}`, + milestoneNumber: args.milestoneNumber, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to delete milestone", + message: error.message, + stderr: error.stderr?.toString(), + milestoneNumber: args.milestoneNumber, + }; + } + }, +}); \ No newline at end of file diff --git a/.opencode/tools/tea-pr-review.ts b/.opencode/tools/tea-pr-review.ts new file mode 100644 index 0000000..79a7469 --- /dev/null +++ b/.opencode/tools/tea-pr-review.ts @@ -0,0 +1,110 @@ +import { tool } from "@opencode-ai/plugin"; +import { $ } from "bun"; + +export const prApprove = tool({ + description: "Approve a pull request with an optional comment", + args: { + prNumber: tool.schema.number().describe("Pull request number"), + comment: tool.schema + .string() + .optional() + .describe("Optional review comment"), + }, + async execute(args) { + try { + let cmd = $`tea pr review --output json --approve ${args.prNumber}`; + + if (args.comment) { + cmd = cmd`--message ${args.comment}`; + } + + const result = await cmd.text(); + + return { + success: true, + message: `Successfully approved PR #${args.prNumber}`, + prNumber: args.prNumber, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to approve PR", + message: error.message, + stderr: error.stderr?.toString(), + prNumber: args.prNumber, + }; + } + }, +}); + +export const prRequestChanges = tool({ + description: "Request changes on a pull request with a comment", + args: { + prNumber: tool.schema.number().describe("Pull request number"), + comment: tool.schema.string().describe("Review comment explaining requested changes"), + }, + async execute(args) { + try { + const cmd = $`tea pr review --output json --request-changes ${args.prNumber} --message ${args.comment}`; + + const result = await cmd.text(); + + return { + success: true, + message: `Successfully requested changes on PR #${args.prNumber}`, + prNumber: args.prNumber, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to request changes on PR", + message: error.message, + stderr: error.stderr?.toString(), + prNumber: args.prNumber, + }; + } + }, +}); + +export const prRequestChangesWithFeedback = tool({ + description: "Request changes on a pull request with detailed feedback", + args: { + prNumber: tool.schema.number().describe("Pull request number"), + feedback: tool.schema.string().describe("Detailed feedback explaining requested changes"), + sections: tool.schema + .array(tool.schema.string()) + .optional() + .describe("Specific sections requiring changes (e.g., ['authentication', 'tests'])"), + }, + async execute(args) { + try { + let feedbackText = args.feedback; + + if (args.sections && args.sections.length > 0) { + feedbackText += "\n\nKey areas requiring attention:\n"; + for (const section of args.sections) { + feedbackText += `- ${section}\n`; + } + } + + const cmd = $`tea pr review --output json --request-changes ${args.prNumber} --message ${feedbackText}`; + + const result = await cmd.text(); + + return { + success: true, + message: `Successfully requested changes on PR #${args.prNumber}`, + prNumber: args.prNumber, + feedback: feedbackText, + output: result, + }; + } catch (error: any) { + return { + error: "Failed to request changes on PR", + message: error.message, + stderr: error.stderr?.toString(), + prNumber: args.prNumber, + }; + } + }, +}); \ No newline at end of file