import { z } from "zod";
import { CreateTaskInput, CreateTaskOutput } from "../../../mcp/tools/schemas/taskCrud.js";
import type { CreateTaskSuccessOutput } 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 CreateTaskInput>;
type OutputType = z.infer<typeof CreateTaskOutput>;
type CreateTaskSuccessOutputType = CreateTaskSuccessOutput;
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);
}
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 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: CreateTaskSuccessOutputType): 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 CreateTask {
constructor() {}
async execute(ctx: unknown, input: InputType): Promise<Result<z.infer<typeof CreateTaskOutput>>> {
// Extract ToolContext from ctx parameter
const toolCtx = ctx as ToolContext;
if (!toolCtx?.createGateway) {
return err("INTERNAL_ERROR", "Tool context not available");
}
const parsed = CreateTaskInput.safeParse(input ?? {});
if (!parsed.success) {
return err("INVALID_PARAMETER", "Invalid parameters", parsed.error.flatten());
}
const data = parsed.data;
const body: Record<string, unknown> = { name: data.name };
if (typeof data.description === "string") {
body.description = data.description;
}
if (Array.isArray(data.assigneeIds)) {
body.assignees = data.assigneeIds;
}
if (typeof data.status === "string") {
body.status = data.status;
}
if (typeof data.priority !== "undefined") {
body.priority = data.priority;
}
if (typeof data.dueDateMs === "number") {
body.due_date = String(data.dueDateMs);
}
if (typeof data.timeEstimateMs === "number") {
body.time_estimate = String(data.timeEstimateMs);
}
if (Array.isArray(data.tags)) {
body.tags = data.tags;
}
if (data.dryRun === true) {
const preview = { listId: data.listId, body };
return ok({ dryRun: true as const, preview });
}
try {
// Create gateway on-demand from current session
const gateway = toolCtx.createGateway();
const response = await gateway.create_task(data.listId, body);
const ref = extractTaskRef(response);
const taskId = ref.taskId ?? data.name;
const task = ref.url ? { taskId, url: ref.url } : { taskId };
const out: CreateTaskSuccessOutputType = { 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);
}
}
}