From d0aaa7ec2fbc422ac759e8f0b782884db4be14f1 Mon Sep 17 00:00:00 2001 From: Hikari Date: Tue, 3 Mar 2026 10:19:41 -0800 Subject: [PATCH] fix: resolve all linting issues in command files Refactored createIssue, createTask, and onboardMentee commands to extract helper functions, fix JSDoc descriptions, correct type handling, and satisfy all ESLint rules. Also fixed object-shorthand mixing in index.ts and the naming convention in anthropic.ts. --- src/commands/createIssue.ts | 71 +++++++++++------- src/commands/createTask.ts | 134 +++++++++++++++++++++------------- src/commands/onboardMentee.ts | 73 ++++++++++-------- src/index.ts | 2 +- src/utils/anthropic.ts | 1 + 5 files changed, 174 insertions(+), 107 deletions(-) diff --git a/src/commands/createIssue.ts b/src/commands/createIssue.ts index e252d66..8609ec1 100644 --- a/src/commands/createIssue.ts +++ b/src/commands/createIssue.ts @@ -4,11 +4,10 @@ * @author Naomi Carrigan */ -import { MessageFlags } from "discord.js"; +import { MessageFlags, type ChatInputCommandInteraction } from "discord.js"; import { ids } from "../config/ids.js"; import { anthropic } from "../utils/anthropic.js"; import { logger } from "../utils/logger.js"; -import type { ChatInputCommandInteraction } from "discord.js"; interface GiteaIssueResponse { // eslint-disable-next-line @typescript-eslint/naming-convention -- Gitea API field. @@ -16,8 +15,44 @@ interface GiteaIssueResponse { number: number; } +const issueSystemPrompt = "You are a helpful assistant that creates" + + " well-structured Gitea issue bodies in markdown. Include relevant" + + " sections like 'Description' and 'Acceptance Criteria'." + + " Be clear and actionable. Return only the body text."; + /** - * @param interaction + * Generates an AI-augmented Gitea issue body using Claude. + * @param description - Optional additional context for the issue. + * @param title - The subject of the Gitea issue. + * @returns The generated issue body text. + */ +const generateIssueBody = async( + description: string, + title: string, +): Promise => { + const aiResponse = await anthropic.messages.create({ + // eslint-disable-next-line @typescript-eslint/naming-convention -- Anthropic API field. + max_tokens: 1000, + messages: [ + { + content: `Create a clear, detailed issue body for a software project.\n\nIssue title: ${title}${description === "" + ? "" + : `\nAdditional context: ${description}`}`, + role: "user", + }, + ], + model: "claude-haiku-4-5-20251001", + system: issueSystemPrompt, + }); + const [ firstContent ] = aiResponse.content; + return firstContent?.type === "text" + ? firstContent.text + : description; +}; + +/** + * Creates a Gitea issue using AI-augmented body content. + * @param interaction - The Discord slash command interaction. */ export const createIssue = async( interaction: ChatInputCommandInteraction, @@ -37,34 +72,19 @@ export const createIssue = async( await interaction.deferReply({ ephemeral: true }); - const aiResponse = await anthropic.messages.create({ - max_tokens: 1000, - messages: [ - { - content: `Create a clear, detailed issue body for a software project.\n\nIssue title: ${title}${description - ? `\nAdditional context: ${description}` - : ""}`, - role: "user", - }, - ], - model: "claude-haiku-4-5-20251001", - system: "You are a helpful assistant that creates well-structured Gitea issue bodies in markdown. Include relevant sections like 'Description' and 'Acceptance Criteria'. Be clear and actionable. Return only the body text.", - }); - - const firstContent = aiResponse.content[0]; - const augmentedBody = firstContent.type === "text" - ? firstContent.text - : description; + const augmentedBody = await generateIssueBody(description, title); const response = await fetch( `https://git.nhcarrigan.com/api/v1/repos/${owner}/${repo}/issues`, { body: JSON.stringify({ - body: augmentedBody, - title, + body: augmentedBody, + title: title, }), headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name. "Authorization": `Bearer ${process.env.GITEA_KEY ?? ""}`, + // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name. "Content-Type": "application/json", }, method: "POST", @@ -79,11 +99,12 @@ export const createIssue = async( return; } - const data: GiteaIssueResponse = await response.json(); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Required to type Response.json() output. + const data = await response.json() as GiteaIssueResponse; await logger.metric("created_issue", 1, { repository: `${owner}/${repo}`, - title, + title: title, }); await interaction.editReply({ content: `✅ Issue #${data.number.toString()} created: **${title}**\n${data.html_url}`, diff --git a/src/commands/createTask.ts b/src/commands/createTask.ts index ddc8eb2..1b206ec 100644 --- a/src/commands/createTask.ts +++ b/src/commands/createTask.ts @@ -4,19 +4,94 @@ * @author Naomi Carrigan */ -import { MessageFlags } from "discord.js"; +import { MessageFlags, type ChatInputCommandInteraction } from "discord.js"; import { ids } from "../config/ids.js"; import { anthropic } from "../utils/anthropic.js"; import { logger } from "../utils/logger.js"; -import type { ChatInputCommandInteraction } from "discord.js"; interface LeantimeResponse { error?: { message: string }; result?: number; } +const taskSystemPrompt = "You are a helpful assistant that creates" + + " well-structured task descriptions. Be concise and actionable." + + " Return only the description text with no extra formatting or headers."; + /** - * @param interaction + * Generates an AI-augmented task description using Claude. + * @param description - Optional additional context for the task. + * @param title - The subject of the Leantime task. + * @returns The generated task description text. + */ +const generateTaskDescription = async( + description: string, + title: string, +): Promise => { + const aiResponse = await anthropic.messages.create({ + // eslint-disable-next-line @typescript-eslint/naming-convention -- Anthropic API field. + max_tokens: 500, + messages: [ + { + content: `Create a clear, concise task description for a personal productivity board.\n\nTask title: ${title}${description === "" + ? "" + : `\nAdditional context: ${description}`}`, + role: "user", + }, + ], + model: "claude-haiku-4-5-20251001", + system: taskSystemPrompt, + }); + const [ firstContent ] = aiResponse.content; + return firstContent.type === "text" + ? firstContent.text + : description; +}; + +/** + * Posts a task to the Leantime board via JSON-RPC. + * @param description - Body copy for the Leantime task. + * @param priority - The task priority level. + * @param title - The headline for the Leantime task. + * @returns The Leantime API response. + */ +const postLeantimeTask = async( + description: string, + priority: number, + title: string, +): Promise => { + const response = await fetch("https://board.nhcarrigan.com/api/jsonrpc", { + body: JSON.stringify({ + id: `amari-task-${Date.now().toString()}`, + jsonrpc: "2.0", + method: "leantime.rpc.tickets.addTicket", + params: { + values: { + description: description, + editorId: "1", + headline: title, + priority: priority.toString(), + projectId: "1", + type: "task", + }, + }, + }), + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name. + "Content-Type": "application/json", + // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name. + "x-api-key": process.env.LEANTIME_KEY ?? "", + }, + method: "POST", + }); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Required to type Response.json() output. + const data = await response.json() as LeantimeResponse; + return data; +}; + +/** + * Creates a Leantime task using AI-augmented description content. + * @param interaction - The Discord slash command interaction. */ export const createTask = async( interaction: ChatInputCommandInteraction, @@ -35,51 +110,8 @@ export const createTask = async( await interaction.deferReply({ ephemeral: true }); - const aiResponse = await anthropic.messages.create({ - max_tokens: 500, - messages: [ - { - content: `Create a clear, concise task description for a personal productivity board.\n\nTask title: ${title}${description - ? `\nAdditional context: ${description}` - : ""}`, - role: "user", - }, - ], - model: "claude-haiku-4-5-20251001", - system: "You are a helpful assistant that creates well-structured task descriptions. Be concise and actionable. Return only the description text with no extra formatting or headers.", - }); - - const firstContent = aiResponse.content[0]; - const augmentedDesc = firstContent.type === "text" - ? firstContent.text - : description; - - const response = await fetch("https://board.nhcarrigan.com/api/jsonrpc", { - body: JSON.stringify({ - id: `amari-task-${Date.now().toString()}`, - jsonrpc: "2.0", - method: "leantime.rpc.tickets.addTicket", - params: { - values: { - description: augmentedDesc, - - editorId: "1", - headline: title, - priority: priority.toString(), - - projectId: "1", - type: "task", - }, - }, - }), - headers: { - "Content-Type": "application/json", - "x-api-key": process.env.LEANTIME_KEY ?? "", - }, - method: "POST", - }); - - const data: LeantimeResponse = await response.json(); + const augmentedDesc = await generateTaskDescription(description, title); + const data = await postLeantimeTask(augmentedDesc, priority, title); if (data.error !== undefined) { await interaction.editReply({ @@ -89,9 +121,9 @@ export const createTask = async( } const taskId = data.result; - const taskUrl = taskId !== undefined - ? `https://board.nhcarrigan.com/dashboard/home#/tickets/showTicket/${taskId.toString()}` - : "https://board.nhcarrigan.com"; + const taskUrl = taskId === undefined + ? "https://board.nhcarrigan.com" + : `https://board.nhcarrigan.com/dashboard/home#/tickets/showTicket/${taskId.toString()}`; await logger.metric("created_task", 1, { title }); await interaction.editReply({ diff --git a/src/commands/onboardMentee.ts b/src/commands/onboardMentee.ts index 56a8732..ac62f54 100644 --- a/src/commands/onboardMentee.ts +++ b/src/commands/onboardMentee.ts @@ -4,15 +4,52 @@ * @author Naomi Carrigan */ -import { MessageFlags } from "discord.js"; +import { MessageFlags, type ChatInputCommandInteraction } from "discord.js"; import { ids } from "../config/ids.js"; import { logger } from "../utils/logger.js"; import type { Amari } from "../interfaces/amari.js"; -import type { ChatInputCommandInteraction } from "discord.js"; /** - * @param amari - * @param interaction + * Creates a mentee repository and configures collaborator access. + * @param amari - The Amari instance. + * @param githubUsername - The mentee's GitHub username. + * @returns The URL of the created or existing repository. + */ +const setupMenteeRepository = async( + amari: Amari, + githubUsername: string, +): Promise => { + const orgApps = amari.githubApp.octokit.rest.apps; + const { data: installation } = await orgApps.getOrgInstallation({ + org: "nhcarrigan-mentorship", + }); + const mentorshipOctokit + = await amari.githubApp.getInstallationOctokit(installation.id); + let repoUrl = `https://github.com/nhcarrigan-mentorship/${githubUsername}`; + try { + const { data: repoData } = await mentorshipOctokit.rest.repos.createInOrg({ + // eslint-disable-next-line @typescript-eslint/naming-convention -- Octokit API field. + auto_init: true, + name: githubUsername, + org: "nhcarrigan-mentorship", + }); + repoUrl = repoData.html_url; + } catch { + // Repo likely already exists - use the default URL. + } + await mentorshipOctokit.rest.repos.addCollaborator({ + owner: "nhcarrigan-mentorship", + permission: "maintain", + repo: githubUsername, + username: githubUsername, + }); + return repoUrl; +}; + +/** + * Onboards a new mentee by creating their GitHub repository and notifying them. + * @param amari - The Amari instance. + * @param interaction - The Discord slash command interaction. */ export const onboardMentee = async( amari: Amari, @@ -29,35 +66,11 @@ export const onboardMentee = async( const menteeName = interaction.options.getString("mentee_name", true); const githubUsername = interaction.options.getString("github_username", true); const menteeUser = interaction.options.getUser("mentee", true); - const discordId = menteeUser.id; await interaction.deferReply({ ephemeral: true }); try { - const { data: installation } = await amari.githubApp.octokit.rest.apps.getOrgInstallation({ - org: "nhcarrigan-mentorship", - }); - const mentorshipOctokit = await amari.githubApp.getInstallationOctokit(installation.id); - - let repoUrl: string; - try { - const { data: repoData } = await mentorshipOctokit.rest.repos.createInOrg({ - // eslint-disable-next-line @typescript-eslint/naming-convention -- Octokit API field. - auto_init: true, - name: githubUsername, - org: "nhcarrigan-mentorship", - }); - repoUrl = repoData.html_url; - } catch { - repoUrl = `https://github.com/nhcarrigan-mentorship/${githubUsername}`; - } - - await mentorshipOctokit.rest.repos.addCollaborator({ - owner: "nhcarrigan-mentorship", - permission: "maintain", - repo: githubUsername, - username: githubUsername, - }); + const repoUrl = await setupMenteeRepository(amari, githubUsername); const channel = amari.discord.channels.cache.get(ids.channels.menteeChat) @@ -71,7 +84,7 @@ export const onboardMentee = async( } await channel.send({ - content: `Hey <@${discordId}>! I've created your mentorship repository: ${repoUrl}\n\nYou should have received an invitation to collaborate - please accept it to get started!`, + content: `Hey <@${menteeUser.id}>! I've created your mentorship repository: ${repoUrl}\n\nYou should have received an invitation to collaborate - please accept it to get started!`, }); await logger.metric("onboarded_mentee", 1, { mentee: menteeName }); diff --git a/src/index.ts b/src/index.ts index 5aabaac..1a2d6bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,7 +63,7 @@ const amari: Amari = { partials: [ Partials.Channel ], }), github: octokit, - githubApp, + githubApp: githubApp, lastRssItems: { freeCodeCamp: null, hackerNews: null, diff --git a/src/utils/anthropic.ts b/src/utils/anthropic.ts index 7c08b45..fb73f4a 100644 --- a/src/utils/anthropic.ts +++ b/src/utils/anthropic.ts @@ -4,6 +4,7 @@ * @author Naomi Carrigan */ +// eslint-disable-next-line @typescript-eslint/naming-convention -- SDK default export uses PascalCase. import Anthropic from "@anthropic-ai/sdk"; export const anthropic = new Anthropic({