/** * Parse Claude Agent SDK TodoWrite tool input into a stable checklist model * for Feishu card progress rendering (Manus-style live todos). */ export type AgentTodoStatus = "pending" | "in_progress" | "completed"; export interface AgentTodoItem { readonly content: string; readonly status: AgentTodoStatus; /** Present-tense label while the item is active, when the model supplies it. */ readonly activeForm: string | undefined; } const STATUSES = new Set(["pending", "in_progress", "completed"]); /** True when the tool name is the SDK TodoWrite dispatcher. */ export function isTodoWriteTool(toolName: string): boolean { return toolName === "TodoWrite" || toolName.endsWith("__TodoWrite"); } /** * Extract the full todo list from a TodoWrite tool_use input. * Returns null when the payload is not a usable TodoWrite body. */ export function parseTodoWriteInput(input: unknown): readonly AgentTodoItem[] | null { if (typeof input !== "object" || input === null || Array.isArray(input)) return null; if (!("todos" in input)) return null; const rawTodos = (input as { todos?: unknown }).todos; if (!Array.isArray(rawTodos) || rawTodos.length === 0) return null; const todos: AgentTodoItem[] = []; for (const raw of rawTodos) { if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue; const record = raw as Record; const content = typeof record.content === "string" ? record.content.trim() : ""; if (content === "") continue; const statusRaw = typeof record.status === "string" ? record.status : "pending"; const status: AgentTodoStatus = STATUSES.has(statusRaw as AgentTodoStatus) ? (statusRaw as AgentTodoStatus) : "pending"; const activeForm = typeof record.activeForm === "string" && record.activeForm.trim() !== "" ? record.activeForm.trim() : undefined; todos.push({ content, status, activeForm }); } return todos.length === 0 ? null : todos; } export function todoProgressSummary(todos: readonly AgentTodoItem[]): { readonly completed: number; readonly total: number; readonly inProgress: number; } { let completed = 0; let inProgress = 0; for (const todo of todos) { if (todo.status === "completed") completed += 1; else if (todo.status === "in_progress") inProgress += 1; } return { completed, total: todos.length, inProgress }; }