generated from nhcarrigan/template
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:
+46
-25
@@ -4,11 +4,10 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MessageFlags } from "discord.js";
|
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||||
import { ids } from "../config/ids.js";
|
import { ids } from "../config/ids.js";
|
||||||
import { anthropic } from "../utils/anthropic.js";
|
import { anthropic } from "../utils/anthropic.js";
|
||||||
import { logger } from "../utils/logger.js";
|
import { logger } from "../utils/logger.js";
|
||||||
import type { ChatInputCommandInteraction } from "discord.js";
|
|
||||||
|
|
||||||
interface GiteaIssueResponse {
|
interface GiteaIssueResponse {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Gitea API field.
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- Gitea API field.
|
||||||
@@ -16,8 +15,44 @@ interface GiteaIssueResponse {
|
|||||||
number: number;
|
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(
|
export const createIssue = async(
|
||||||
interaction: ChatInputCommandInteraction,
|
interaction: ChatInputCommandInteraction,
|
||||||
@@ -37,34 +72,19 @@ export const createIssue = async(
|
|||||||
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
const aiResponse = await anthropic.messages.create({
|
const augmentedBody = await generateIssueBody(description, title);
|
||||||
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 response = await fetch(
|
const response = await fetch(
|
||||||
`https://git.nhcarrigan.com/api/v1/repos/${owner}/${repo}/issues`,
|
`https://git.nhcarrigan.com/api/v1/repos/${owner}/${repo}/issues`,
|
||||||
{
|
{
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
body: augmentedBody,
|
body: augmentedBody,
|
||||||
title,
|
title: title,
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name.
|
||||||
"Authorization": `Bearer ${process.env.GITEA_KEY ?? ""}`,
|
"Authorization": `Bearer ${process.env.GITEA_KEY ?? ""}`,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name.
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -79,11 +99,12 @@ export const createIssue = async(
|
|||||||
return;
|
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, {
|
await logger.metric("created_issue", 1, {
|
||||||
repository: `${owner}/${repo}`,
|
repository: `${owner}/${repo}`,
|
||||||
title,
|
title: title,
|
||||||
});
|
});
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: `✅ Issue #${data.number.toString()} created: **${title}**\n${data.html_url}`,
|
content: `✅ Issue #${data.number.toString()} created: **${title}**\n${data.html_url}`,
|
||||||
|
|||||||
+83
-51
@@ -4,19 +4,94 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MessageFlags } from "discord.js";
|
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||||
import { ids } from "../config/ids.js";
|
import { ids } from "../config/ids.js";
|
||||||
import { anthropic } from "../utils/anthropic.js";
|
import { anthropic } from "../utils/anthropic.js";
|
||||||
import { logger } from "../utils/logger.js";
|
import { logger } from "../utils/logger.js";
|
||||||
import type { ChatInputCommandInteraction } from "discord.js";
|
|
||||||
|
|
||||||
interface LeantimeResponse {
|
interface LeantimeResponse {
|
||||||
error?: { message: string };
|
error?: { message: string };
|
||||||
result?: number;
|
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(
|
export const createTask = async(
|
||||||
interaction: ChatInputCommandInteraction,
|
interaction: ChatInputCommandInteraction,
|
||||||
@@ -35,51 +110,8 @@ export const createTask = async(
|
|||||||
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
const aiResponse = await anthropic.messages.create({
|
const augmentedDesc = await generateTaskDescription(description, title);
|
||||||
max_tokens: 500,
|
const data = await postLeantimeTask(augmentedDesc, priority, title);
|
||||||
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();
|
|
||||||
|
|
||||||
if (data.error !== undefined) {
|
if (data.error !== undefined) {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
@@ -89,9 +121,9 @@ export const createTask = async(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const taskId = data.result;
|
const taskId = data.result;
|
||||||
const taskUrl = taskId !== undefined
|
const taskUrl = taskId === undefined
|
||||||
? `https://board.nhcarrigan.com/dashboard/home#/tickets/showTicket/${taskId.toString()}`
|
? "https://board.nhcarrigan.com"
|
||||||
: "https://board.nhcarrigan.com";
|
: `https://board.nhcarrigan.com/dashboard/home#/tickets/showTicket/${taskId.toString()}`;
|
||||||
|
|
||||||
await logger.metric("created_task", 1, { title });
|
await logger.metric("created_task", 1, { title });
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
|
|||||||
@@ -4,15 +4,52 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MessageFlags } from "discord.js";
|
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||||
import { ids } from "../config/ids.js";
|
import { ids } from "../config/ids.js";
|
||||||
import { logger } from "../utils/logger.js";
|
import { logger } from "../utils/logger.js";
|
||||||
import type { Amari } from "../interfaces/amari.js";
|
import type { Amari } from "../interfaces/amari.js";
|
||||||
import type { ChatInputCommandInteraction } from "discord.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param amari
|
* Creates a mentee repository and configures collaborator access.
|
||||||
* @param interaction
|
* @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(
|
export const onboardMentee = async(
|
||||||
amari: Amari,
|
amari: Amari,
|
||||||
@@ -29,35 +66,11 @@ export const onboardMentee = async(
|
|||||||
const menteeName = interaction.options.getString("mentee_name", true);
|
const menteeName = interaction.options.getString("mentee_name", true);
|
||||||
const githubUsername = interaction.options.getString("github_username", true);
|
const githubUsername = interaction.options.getString("github_username", true);
|
||||||
const menteeUser = interaction.options.getUser("mentee", true);
|
const menteeUser = interaction.options.getUser("mentee", true);
|
||||||
const discordId = menteeUser.id;
|
|
||||||
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: installation } = await amari.githubApp.octokit.rest.apps.getOrgInstallation({
|
const repoUrl = await setupMenteeRepository(amari, githubUsername);
|
||||||
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 channel
|
const channel
|
||||||
= amari.discord.channels.cache.get(ids.channels.menteeChat)
|
= amari.discord.channels.cache.get(ids.channels.menteeChat)
|
||||||
@@ -71,7 +84,7 @@ export const onboardMentee = async(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await channel.send({
|
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 });
|
await logger.metric("onboarded_mentee", 1, { mentee: menteeName });
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ const amari: Amari = {
|
|||||||
partials: [ Partials.Channel ],
|
partials: [ Partials.Channel ],
|
||||||
}),
|
}),
|
||||||
github: octokit,
|
github: octokit,
|
||||||
githubApp,
|
githubApp: githubApp,
|
||||||
lastRssItems: {
|
lastRssItems: {
|
||||||
freeCodeCamp: null,
|
freeCodeCamp: null,
|
||||||
hackerNews: null,
|
hackerNews: null,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- SDK default export uses PascalCase.
|
||||||
import Anthropic from "@anthropic-ai/sdk";
|
import Anthropic from "@anthropic-ai/sdk";
|
||||||
|
|
||||||
export const anthropic = new Anthropic({
|
export const anthropic = new Anthropic({
|
||||||
|
|||||||
Reference in New Issue
Block a user