import { z } from "zod";
import { CommentTaskInput, CommentTaskOutput } from "../../../mcp/tools/schemas/taskCrud.js";
import { Result, ok, err } from "../../../shared/Result.js";
import { mapHttpError } from "../../../shared/httpErrors.js";
import { characterLimit } from "../../../config/runtime.js";
import type { ToolContext } from "../../../mcp/tools/registerTools.js";
type InputType = z.infer<typeof CommentTaskInput>;
type OutputType = z.infer<typeof CommentTaskOutput>;
type HttpErrorLike = { status?: number; data?: unknown };
type TaskRefLike = { taskId?: string; url?: string };
function toId(value: unknown): string | undefined {
if (typeof value === "string" && value.length > 0) {
return value;
}
if (typeof value === "number" && Number.isFinite(value)) {
return String(Math.trunc(value));
}
if (typeof value === "bigint") {
return value.toString();
}
return undefined;
}
function toUrl(value: unknown): string | undefined {
if (typeof value === "string" && value.length > 0) {
return value;
}
return undefined;
}
function extractTaskRef(payload: unknown, visited = new Set<unknown>()): TaskRefLike {
if (typeof payload === "string" && payload.length > 0) {
return { taskId: payload };
}
if (!payload || typeof payload !== "object") {
return {};
}
if (visited.has(payload)) {
return {};
}
visited.add(payload);
const record = payload as Record<string, unknown>;
let taskId =
toId(record.taskId) ??
toId(record.task_id) ??
toId(record.id) ??
toId(record.ID) ??
toId(record.Id);
let url = toUrl(record.url) ?? toUrl(record.permalink) ?? toUrl(record.link) ?? toUrl(record.web_url) ?? toUrl(record.app_url);
const nestedSources: unknown[] = [];
if ("task" in record) {
nestedSources.push(record.task);
}
if ("data" in record) {
nestedSources.push(record.data);
}
if ("result" in record) {
nestedSources.push(record.result);
}
if ("comment" in record) {
nestedSources.push(record.comment);
}
const tasks = record.tasks;
if (Array.isArray(tasks)) {
nestedSources.push(...tasks);
}
for (const source of nestedSources) {
const nested = extractTaskRef(source, visited);
if (!taskId && nested.taskId) {
taskId = nested.taskId;
}
if (!url && nested.url) {
url = nested.url;
}
if (taskId && url) {
break;
}
}
return { taskId, url };
}
function extractCommentId(payload: unknown, visited = new Set<unknown>()): string | undefined {
if (typeof payload === "string" && payload.length > 0) {
return payload;
}
if (!payload || typeof payload !== "object") {
return undefined;
}
if (visited.has(payload)) {
return undefined;
}
visited.add(payload);
const record = payload as Record<string, unknown>;
const direct =
toId(record.commentId) ??
toId(record.comment_id) ??
toId(record.id) ??
toId(record.ID) ??
toId(record.Id);
if (direct) {
return direct;
}
const nestedSources: unknown[] = [];
if ("comment" in record) {
nestedSources.push(record.comment);
}
if ("data" in record) {
nestedSources.push(record.data);
}
if ("result" in record) {
nestedSources.push(record.result);
}
const comments = record.comments;
if (Array.isArray(comments)) {
nestedSources.push(...comments);
}
for (const source of nestedSources) {
const nested = extractCommentId(source, visited);
if (nested) {
return nested;
}
}
return undefined;
}
function shortenField(target: unknown, path: string[]): boolean {
if (!target || typeof target !== "object") {
return false;
}
let current: unknown = target;
for (let i = 0; i < path.length - 1; i += 1) {
if (!current || typeof current !== "object") {
return false;
}
current = (current as Record<string, unknown>)[path[i]];
}
if (!current || typeof current !== "object") {
return false;
}
const container = current as Record<string, unknown>;
const key = path[path.length - 1];
const value = container[key];
if (typeof value !== "string" || value.length === 0) {
return false;
}
const nextLength = Math.floor(value.length / 2);
container[key] = nextLength > 0 ? value.slice(0, nextLength) : "";
return true;
}
function enforceLimit(out: OutputType): void {
const limit = characterLimit();
const paths: string[][] = [["description"], ["commentMarkdown"], ["attachment", "name"]];
let payload = JSON.stringify(out);
let truncated = false;
while (payload.length > limit) {
let trimmed = false;
for (const path of paths) {
if (shortenField(out as unknown, path)) {
trimmed = true;
truncated = true;
payload = JSON.stringify(out);
if (payload.length <= limit) {
break;
}
}
}
if (!trimmed) {
break;
}
}
if (payload.length > limit) {
truncated = true;
}
if (truncated) {
out.truncated = true;
out.guidance = "Output trimmed to character_limit";
}
}
export class CommentTask {
constructor() {}
async execute(ctx: unknown, input: InputType): Promise<Result<z.infer<typeof CommentTaskOutput>>> {
const toolCtx = ctx as ToolContext;
if (!toolCtx?.createGateway) {
return err("INTERNAL_ERROR", "Tool context not available");
}
const parsed = CommentTaskInput.safeParse(input ?? {});
if (!parsed.success) {
return err("INVALID_PARAMETER", "Invalid parameters", parsed.error.flatten());
}
const data = parsed.data;
if (data.dryRun === true) {
return ok({ dryRun: true as const, preview: { taskId: data.taskId, markdown: data.commentMarkdown } });
}
try {
const gateway = toolCtx.createGateway();
const response = await gateway.comment_task(data.taskId, data.commentMarkdown);
const ref = extractTaskRef(response);
const commentId = extractCommentId(response);
const taskId = ref.taskId ?? data.taskId;
const task = ref.url ? { taskId, url: ref.url } : { taskId };
const out: OutputType = commentId ? { task, commentId } : { task };
enforceLimit(out);
return ok(out, out.truncated === true, out.guidance);
} catch (error) {
const httpError = error as HttpErrorLike;
if (httpError && typeof httpError.status === "number") {
const mapped = mapHttpError(httpError.status, httpError.data);
return err(mapped.code, mapped.message, mapped.details);
}
const message = error instanceof Error ? error.message : String(error);
return err("UNKNOWN", message);
}
}
}