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.
This commit is contained in:
2026-03-03 10:19:41 -08:00
parent 9df2d9ddc4
commit d0aaa7ec2f
5 changed files with 174 additions and 107 deletions
+45 -24
View File
@@ -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<string> => {
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,
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}`,
+83 -51
View File
@@ -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<string> => {
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<LeantimeResponse> => {
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({
+43 -30
View File
@@ -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<string> => {
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 });
+1 -1
View File
@@ -63,7 +63,7 @@ const amari: Amari = {
partials: [ Partials.Channel ],
}),
github: octokit,
githubApp,
githubApp: githubApp,
lastRssItems: {
freeCodeCamp: null,
hackerNews: null,
+1
View File
@@ -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({