import { z } from "zod";
import { AttachFileToTaskInput, AttachFileToTaskOutput } from "../../../mcp/tools/schemas/taskCrud.js";
import { Result, ok, err } from "../../../shared/Result.js";
import { mapHttpError } from "../../../shared/httpErrors.js";
import { characterLimit, maxAttachmentBytes } from "../../../config/runtime.js";
import type { ToolContext } from "../../../mcp/tools/registerTools.js";
type InputType = z.infer<typeof AttachFileToTaskInput>;
type OutputType = z.infer<typeof AttachFileToTaskOutput>;
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 ("attachment" in record) {
nestedSources.push(record.attachment);
}
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 extractAttachmentId(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.attachmentId) ??
toId(record.attachment_id) ??
toId(record.id) ??
toId(record.ID) ??
toId(record.Id);
if (direct) {
return direct;
}
const nestedSources: unknown[] = [];
if ("attachment" in record) {
nestedSources.push(record.attachment);
}
if ("data" in record) {
nestedSources.push(record.data);
}
if ("result" in record) {
nestedSources.push(record.result);
}
const attachments = record.attachments;
if (Array.isArray(attachments)) {
nestedSources.push(...attachments);
}
for (const source of nestedSources) {
const nested = extractAttachmentId(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";
}
}
function parseDataUri(dataUri: string): { size: number } | null {
if (!dataUri.startsWith("data:")) {
return null;
}
const commaIndex = dataUri.indexOf(",");
if (commaIndex === -1) {
return null;
}
const meta = dataUri.slice(5, commaIndex);
if (!meta.includes("base64")) {
return null;
}
const base64 = dataUri.slice(commaIndex + 1);
try {
const buffer = Buffer.from(base64, "base64");
return { size: buffer.byteLength };
} catch (error) {
void error;
return null;
}
}
export class AttachFileToTask {
constructor() {}
async execute(ctx: unknown, input: InputType): Promise<Result<z.infer<typeof AttachFileToTaskOutput>>> {
const toolCtx = ctx as ToolContext;
if (!toolCtx?.createGateway) {
return err("INTERNAL_ERROR", "Tool context not available");
}
const parsed = AttachFileToTaskInput.safeParse(input ?? {});
if (!parsed.success) {
return err("INVALID_PARAMETER", "Invalid parameters", parsed.error.flatten());
}
const data = parsed.data;
const parsedDataUri = parseDataUri(data.dataUri);
if (!parsedDataUri) {
return err("INVALID_PARAMETER", "Attachment must be provided as base64 data URI");
}
const limitBytes = maxAttachmentBytes();
if (parsedDataUri.size > limitBytes) {
const limitMb = Math.max(1, Math.round(limitBytes / (1024 * 1024)));
return err("LIMIT_EXCEEDED", `Attachment exceeds ${limitMb} MB`);
}
if (data.dryRun === true) {
return ok({
dryRun: true as const,
preview: { taskId: data.taskId, name: data.name, sizeBytes: parsedDataUri.size }
});
}
try {
const gateway = toolCtx.createGateway();
const response = await gateway.attach_file_to_task(data.taskId, data.dataUri, data.name);
const ref = extractTaskRef(response);
const attachmentId = extractAttachmentId(response);
const taskId = ref.taskId ?? data.taskId;
const task = ref.url ? { taskId, url: ref.url } : { taskId };
const out: OutputType = attachmentId
? { task, attachmentId, sizeBytes: parsedDataUri.size }
: { task, sizeBytes: parsedDataUri.size };
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);
}
}
}