generated from nhcarrigan/template
feat: replace create-task and create-issue with unified create-ticket
Removes /create-task and /create-issue slash commands and introduces /create-ticket, which routes to LeanTime, Asana, Gitea, or GitHub based on a platform choice argument. The user provides only a description; the AI generates both the title and fleshed-out body.
This commit is contained in:
+23
-42
@@ -1,56 +1,37 @@
|
||||
[
|
||||
{
|
||||
"name": "create-issue",
|
||||
"name": "create-ticket",
|
||||
"type": 1,
|
||||
"description": "Creates a Gitea issue with an AI-generated body.",
|
||||
"description": "Creates a ticket on the chosen platform with an AI-generated title and description.",
|
||||
"options": [
|
||||
{
|
||||
"name": "owner",
|
||||
"description": "The owner of the repository.",
|
||||
"name": "platform",
|
||||
"description": "The platform to create the ticket on.",
|
||||
"type": 3,
|
||||
"required": true,
|
||||
"choices": [
|
||||
{ "name": "LeanTime", "value": "leantime" },
|
||||
{ "name": "Asana", "value": "asana" },
|
||||
{ "name": "Gitea", "value": "gitea" },
|
||||
{ "name": "GitHub", "value": "github" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Describe what you need. The AI will generate a title and full description.",
|
||||
"type": 3,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "owner",
|
||||
"description": "The repository owner (required for Gitea/GitHub).",
|
||||
"type": 3,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "repo",
|
||||
"description": "The name of the repository.",
|
||||
"description": "The repository name (required for Gitea/GitHub).",
|
||||
"type": 3,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"description": "The issue title.",
|
||||
"type": 3,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Optional additional context for the issue body.",
|
||||
"type": 3,
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "create-task",
|
||||
"type": 1,
|
||||
"description": "Creates a Leantime task with an AI-generated description.",
|
||||
"options": [
|
||||
{
|
||||
"name": "title",
|
||||
"description": "The task title.",
|
||||
"type": 3,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Optional additional context for the task description.",
|
||||
"type": 3,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "priority",
|
||||
"description": "The task priority level (1-5, default 3).",
|
||||
"type": 4,
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,4 +9,5 @@ BASEROW_TOKEN="op://Environment Variables - Naomi/Amari/baserow token"
|
||||
RA_KEY="op://Environment Variables - Naomi/Amari/retroachievements key"
|
||||
LEANTIME_KEY="op://Environment Variables - Naomi/Amari/leantime key"
|
||||
GITEA_KEY="op://Environment Variables - Naomi/Amari/gitea key"
|
||||
ASANA_KEY="op://Environment Variables - Naomi/Amari/asana key"
|
||||
ANTHROPIC_KEY="op://Environment Variables - Naomi/Hikari/anthropic_key"
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { ids } from "../config/ids.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { makeAiRequest } from "../utils/makeAiRequest.js";
|
||||
|
||||
interface GiteaIssueResponse {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Gitea API field.
|
||||
html_url: string;
|
||||
number: number;
|
||||
}
|
||||
|
||||
interface GiteaIssueOptions {
|
||||
body: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const issueSystemPrompt = "Create well-structured Gitea issue bodies in"
|
||||
+ " markdown. Include relevant sections like 'Description' and"
|
||||
+ " 'Acceptance Criteria'. Be clear and actionable."
|
||||
+ " Return only the body text.";
|
||||
|
||||
/**
|
||||
* Generates an AI-augmented Gitea issue body.
|
||||
* @param description - Optional additional context for the issue.
|
||||
* @param title - The subject of the Gitea issue.
|
||||
* @returns The generated issue body text, or the original description as fallback.
|
||||
*/
|
||||
const generateIssueBody = async(
|
||||
description: string,
|
||||
title: string,
|
||||
): Promise<string> => {
|
||||
const result = await makeAiRequest({
|
||||
maxTokens: 1000,
|
||||
systemPrompt: issueSystemPrompt,
|
||||
userMessage: `Create a clear, detailed issue body for a software project.\n\nIssue title: ${title}${description === ""
|
||||
? ""
|
||||
: `\nAdditional context: ${description}`}`,
|
||||
});
|
||||
return result ?? description;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a Gitea issue via the API.
|
||||
* @param options -- The issue fields to submit.
|
||||
* @returns The created issue data.
|
||||
*/
|
||||
const postGiteaIssue = async(
|
||||
options: GiteaIssueOptions,
|
||||
): Promise<GiteaIssueResponse> => {
|
||||
const response = await fetch(
|
||||
`https://git.nhcarrigan.com/api/v1/repos/${options.owner}/${options.repo}/issues`,
|
||||
{
|
||||
body: JSON.stringify({ body: options.body, title: options.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",
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Required to type Response.json() output.
|
||||
return await response.json() as GiteaIssueResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a Gitea issue using AI-augmented body content.
|
||||
* @param interaction - The Discord slash command interaction.
|
||||
*/
|
||||
export const createIssue = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
if (interaction.user.id !== ids.users.naomi) {
|
||||
await interaction.reply({
|
||||
content: "This command is restricted to Naomi.",
|
||||
flags: [ MessageFlags.Ephemeral ],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = interaction.options.getString("owner", true);
|
||||
const repo = interaction.options.getString("repo", true);
|
||||
const title = interaction.options.getString("title", true);
|
||||
const description = interaction.options.getString("description") ?? "";
|
||||
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
|
||||
try {
|
||||
const augmentedBody = await generateIssueBody(description, title);
|
||||
const data = await postGiteaIssue({
|
||||
body: augmentedBody,
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
title: title,
|
||||
});
|
||||
await logger.metric("created_issue", 1, {
|
||||
repository: `${owner}/${repo}`,
|
||||
title: title,
|
||||
});
|
||||
await interaction.editReply({
|
||||
content: `✅ Issue #${data.number.toString()} created: **${title}**\n${data.html_url}`,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await logger.error("createIssue command", error);
|
||||
}
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error";
|
||||
await interaction.editReply({
|
||||
content: `❌ Failed to create issue: ${errorMessage}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { ids } from "../config/ids.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { makeAiRequest } from "../utils/makeAiRequest.js";
|
||||
|
||||
interface LeantimeResponse {
|
||||
error?: { message: string };
|
||||
result?: number;
|
||||
}
|
||||
|
||||
const taskSystemPrompt = "Create well-structured task descriptions."
|
||||
+ " Be concise and actionable."
|
||||
+ " Return only the description text with no extra formatting or headers.";
|
||||
|
||||
/**
|
||||
* Generates an AI-augmented task description.
|
||||
* @param description - Optional additional context for the task.
|
||||
* @param title - The subject of the Leantime task.
|
||||
* @returns The generated task description text, or the original description as fallback.
|
||||
*/
|
||||
const generateTaskDescription = async(
|
||||
description: string,
|
||||
title: string,
|
||||
): Promise<string> => {
|
||||
const result = await makeAiRequest({
|
||||
maxTokens: 500,
|
||||
systemPrompt: taskSystemPrompt,
|
||||
userMessage: `Create a clear, concise task description for a personal productivity board.\n\nTask title: ${title}${description === ""
|
||||
? ""
|
||||
: `\nAdditional context: ${description}`}`,
|
||||
});
|
||||
return result ?? 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,
|
||||
): Promise<void> => {
|
||||
if (interaction.user.id !== ids.users.naomi) {
|
||||
await interaction.reply({
|
||||
content: "This command is restricted to Naomi.",
|
||||
flags: [ MessageFlags.Ephemeral ],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const title = interaction.options.getString("title", true);
|
||||
const description = interaction.options.getString("description") ?? "";
|
||||
const priority = interaction.options.getInteger("priority") ?? 3;
|
||||
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
|
||||
try {
|
||||
const augmentedDesc = await generateTaskDescription(description, title);
|
||||
const data = await postLeantimeTask(augmentedDesc, priority, title);
|
||||
|
||||
if (data.error !== undefined) {
|
||||
await interaction.editReply({
|
||||
content: `❌ Failed to create task: ${data.error.message}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = data.result;
|
||||
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({
|
||||
content: `✅ Task created: **${title}**\n${taskUrl}`,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await logger.error("createTask command", error);
|
||||
}
|
||||
await interaction.editReply({
|
||||
content: "❌ An unexpected error occurred while creating the task.",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { ids } from "../config/ids.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { makeAiRequest } from "../utils/makeAiRequest.js";
|
||||
import type { Amari } from "../interfaces/amari.js";
|
||||
|
||||
interface GeneratedTicket {
|
||||
body: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface LeantimeResponse {
|
||||
error?: { message: string };
|
||||
result?: number;
|
||||
}
|
||||
|
||||
interface GiteaIssueResponse {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Gitea API field.
|
||||
html_url: string;
|
||||
number: number;
|
||||
}
|
||||
|
||||
interface AsanaTaskResponse {
|
||||
data: {
|
||||
gid: string;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Asana API field.
|
||||
permalink_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PostOptions {
|
||||
body: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface RepoTicketOptions extends PostOptions {
|
||||
amari: Amari;
|
||||
}
|
||||
|
||||
interface TicketRouteOptions {
|
||||
amari: Amari;
|
||||
owner: string;
|
||||
platform: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
interface RepoValidationContext {
|
||||
interaction: ChatInputCommandInteraction;
|
||||
owner: string;
|
||||
platform: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
const ticketSystemPrompt = "Generate a well-structured ticket. Return ONLY a"
|
||||
+ " valid JSON object with exactly two keys: \"title\" (a concise title"
|
||||
+ " under 80 characters) and \"body\" (a detailed markdown description"
|
||||
+ " with relevant sections such as Description and Acceptance Criteria)."
|
||||
+ " No extra text or formatting outside the JSON object.";
|
||||
|
||||
/**
|
||||
* Generates an AI title and body for a ticket from a raw description.
|
||||
* @param description - The user's raw description of what they need.
|
||||
* @param platform - The target platform for context.
|
||||
* @returns A generated ticket with title and body, or null on failure.
|
||||
*/
|
||||
const generateTicket = async(
|
||||
description: string,
|
||||
platform: string,
|
||||
): Promise<GeneratedTicket | null> => {
|
||||
const result = await makeAiRequest({
|
||||
maxTokens: 1000,
|
||||
systemPrompt: ticketSystemPrompt,
|
||||
userMessage: `Platform: ${platform}\n\nUser description: ${description}`,
|
||||
});
|
||||
if (result === null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Parsing known AI JSON output.
|
||||
return JSON.parse(result) as GeneratedTicket;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts a task to LeanTime via JSON-RPC.
|
||||
* @param title - The task headline.
|
||||
* @param body - The task description.
|
||||
* @returns A URL to the created task.
|
||||
*/
|
||||
const postToLeantime = async(title: string, body: string): Promise<string> => {
|
||||
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: body,
|
||||
editorId: "1",
|
||||
headline: title,
|
||||
priority: "3",
|
||||
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;
|
||||
if (data.error !== undefined) {
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
return data.result === undefined
|
||||
? "https://board.nhcarrigan.com"
|
||||
: `https://board.nhcarrigan.com/dashboard/home#/tickets/showTicket/${data.result.toString()}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts a task to Asana.
|
||||
* @param title - The task name.
|
||||
* @param body - The task notes.
|
||||
* @returns A URL to the created Asana task.
|
||||
*/
|
||||
const postToAsana = async(title: string, body: string): Promise<string> => {
|
||||
const response = await fetch(
|
||||
"https://app.asana.com/api/1.0/tasks?opt_fields=gid,permalink_url",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
name: title,
|
||||
notes: body,
|
||||
projects: [ "1210018361945076" ],
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name.
|
||||
"Authorization": `Bearer ${process.env.ASANA_KEY ?? ""}`,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name.
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Required to type Response.json() output.
|
||||
const data = await response.json() as AsanaTaskResponse;
|
||||
return data.data.permalink_url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts an issue to Gitea.
|
||||
* @param options - The repository and content details.
|
||||
* @returns A URL to the created Gitea issue.
|
||||
*/
|
||||
const postToGitea = async(options: PostOptions): Promise<string> => {
|
||||
const response = await fetch(
|
||||
`https://git.nhcarrigan.com/api/v1/repos/${options.owner}/${options.repo}/issues`,
|
||||
{
|
||||
body: JSON.stringify({ body: options.body, title: options.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",
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Required to type Response.json() output.
|
||||
const data = await response.json() as GiteaIssueResponse;
|
||||
return data.html_url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts an issue to GitHub using the authenticated app octokit.
|
||||
* @param options - The Amari instance, repository, and content details.
|
||||
* @returns A URL to the created GitHub issue.
|
||||
*/
|
||||
const postToGitHub = async(options: RepoTicketOptions): Promise<string> => {
|
||||
const { data } = await options.amari.github.rest.issues.create({
|
||||
body: options.body,
|
||||
owner: options.owner,
|
||||
repo: options.repo,
|
||||
title: options.title,
|
||||
});
|
||||
return data.html_url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that owner and repo are provided when required by the platform.
|
||||
* Replies with an error if validation fails.
|
||||
* @param context - The validation context including platform, owner, repo, and interaction.
|
||||
* @returns True if validation passes, false if an error reply was sent.
|
||||
*/
|
||||
const validateRepoArguments = async(
|
||||
context: RepoValidationContext,
|
||||
): Promise<boolean> => {
|
||||
if (context.platform !== "gitea" && context.platform !== "github") {
|
||||
return true;
|
||||
}
|
||||
if (context.owner !== "" && context.repo !== "") {
|
||||
return true;
|
||||
}
|
||||
const platformLabel = context.platform === "gitea"
|
||||
? "Gitea"
|
||||
: "GitHub";
|
||||
await context.interaction.reply({
|
||||
content: `❌ The \`owner\` and \`repo\` arguments are required for ${platformLabel}.`,
|
||||
flags: [ MessageFlags.Ephemeral ],
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Routes a generated ticket to the correct platform and logs the metric.
|
||||
* @param ticket - The AI-generated ticket content.
|
||||
* @param options - Routing context including platform, owner, repo, and Amari instance.
|
||||
* @returns A URL to the created ticket.
|
||||
*/
|
||||
const routeTicket = async(
|
||||
ticket: GeneratedTicket,
|
||||
options: TicketRouteOptions,
|
||||
): Promise<string> => {
|
||||
const { amari, owner, platform, repo } = options;
|
||||
const { body, title } = ticket;
|
||||
if (platform === "leantime") {
|
||||
await logger.metric("created_ticket", 1, { platform, title });
|
||||
return await postToLeantime(title, body);
|
||||
}
|
||||
if (platform === "asana") {
|
||||
await logger.metric("created_ticket", 1, { platform, title });
|
||||
return await postToAsana(title, body);
|
||||
}
|
||||
const repository = `${owner}/${repo}`;
|
||||
if (platform === "gitea") {
|
||||
await logger.metric("created_ticket", 1, { platform, repository, title });
|
||||
return await postToGitea({ body, owner, repo, title });
|
||||
}
|
||||
await logger.metric("created_ticket", 1, { platform, repository, title });
|
||||
return await postToGitHub({ amari, body, owner, repo, title });
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a ticket on the specified platform using an AI-generated title and body.
|
||||
* @param amari - The Amari instance.
|
||||
* @param interaction - The Discord slash command interaction.
|
||||
*/
|
||||
export const createTicket = async(
|
||||
amari: Amari,
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
if (interaction.user.id !== ids.users.naomi) {
|
||||
await interaction.reply({
|
||||
content: "This command is restricted to Naomi.",
|
||||
flags: [ MessageFlags.Ephemeral ],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const platform = interaction.options.getString("platform", true);
|
||||
const description = interaction.options.getString("description", true);
|
||||
const { owner, repo } = {
|
||||
owner: interaction.options.getString("owner") ?? "",
|
||||
repo: interaction.options.getString("repo") ?? "",
|
||||
};
|
||||
|
||||
if (!await validateRepoArguments({ interaction, owner, platform, repo })) {
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
|
||||
const ticket = await generateTicket(description, platform);
|
||||
if (ticket === null) {
|
||||
await interaction.editReply({
|
||||
content: "❌ Failed to generate ticket content from AI.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = await routeTicket(ticket, { amari, owner, platform, repo });
|
||||
await interaction.editReply({
|
||||
content: `✅ Ticket created: **${ticket.title}**\n${url}`,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await logger.error("createTicket command", error);
|
||||
}
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error";
|
||||
await interaction.editReply({
|
||||
content: `❌ Failed to create ticket: ${errorMessage}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
type ChatInputCommandInteraction,
|
||||
type Interaction,
|
||||
} from "discord.js";
|
||||
import { createIssue } from "../commands/createIssue.js";
|
||||
import { createTask } from "../commands/createTask.js";
|
||||
import { createTicket } from "../commands/createTicket.js";
|
||||
import { forwardToOwner } from "../commands/forwardToOwner.js";
|
||||
import { onboardMentee } from "../commands/onboardMentee.js";
|
||||
import { ids } from "../config/ids.js";
|
||||
@@ -30,12 +29,8 @@ const handleChatInputCommand = (
|
||||
void onboardMentee(amari, interaction);
|
||||
return;
|
||||
}
|
||||
if (commandName === "create-task") {
|
||||
void createTask(interaction);
|
||||
return;
|
||||
}
|
||||
if (commandName === "create-issue") {
|
||||
void createIssue(interaction);
|
||||
if (commandName === "create-ticket") {
|
||||
void createTicket(amari, interaction);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user