/**
* Task Manager — action-based CRUD task tracking with IDs and dependencies
*
* Extracted from local-tools.ts for single-responsibility.
* All consumers should import from local-tools.ts (re-export facade).
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
import { join } from "path";
import { homedir } from "os";
// ============================================================================
// TYPES
// ============================================================================
interface TaskItem {
id: string;
subject: string;
description: string;
status: "pending" | "in_progress" | "completed";
activeForm?: string;
owner?: string;
metadata?: Record<string, unknown>;
blocks: string[]; // task IDs this task blocks
blockedBy: string[]; // task IDs that must complete before this one
createdAt: string;
}
import { ToolResult } from "../../../shared/types.js";
// ============================================================================
// STATE
// ============================================================================
let taskState: TaskItem[] = [];
let taskCounter = 0;
let todoSessionId: string | null = null;
const TODOS_DIR = join(homedir(), ".swagmanager", "todos");
// ============================================================================
// TOOL IMPLEMENTATION
// ============================================================================
export function tasksTool(input: Record<string, unknown>): ToolResult {
const action = input.action as string;
if (!action) return { success: false, output: "action is required (create/update/list/get)" };
switch (action) {
case "create": {
const subject = input.subject as string;
const description = input.description as string;
if (!subject || !description) return { success: false, output: "subject and description required for create" };
taskCounter++;
const task: TaskItem = {
id: String(taskCounter),
subject,
description,
status: "pending",
activeForm: input.activeForm as string | undefined,
owner: input.owner as string | undefined,
metadata: input.metadata as Record<string, unknown> | undefined,
blocks: [],
blockedBy: [],
createdAt: new Date().toISOString(),
};
taskState.push(task);
persistTasks();
return { success: true, output: `Created task #${task.id}: ${subject}` };
}
case "update": {
const taskId = input.taskId as string;
if (!taskId) return { success: false, output: "taskId required for update" };
const task = taskState.find((t) => t.id === taskId);
if (!task) return { success: false, output: `Task #${taskId} not found` };
const newStatus = input.status as string | undefined;
// Handle deletion
if (newStatus === "deleted") {
// Remove from other tasks' blocks/blockedBy
for (const t of taskState) {
t.blocks = t.blocks.filter((id) => id !== taskId);
t.blockedBy = t.blockedBy.filter((id) => id !== taskId);
}
taskState = taskState.filter((t) => t.id !== taskId);
persistTasks();
return { success: true, output: `Deleted task #${taskId}` };
}
if (newStatus && ["pending", "in_progress", "completed"].includes(newStatus)) {
task.status = newStatus as TaskItem["status"];
}
if (input.subject_update) task.subject = input.subject_update as string;
if (input.description !== undefined) task.description = input.description as string;
if (input.activeForm !== undefined) task.activeForm = input.activeForm as string;
if (input.owner !== undefined) task.owner = input.owner as string;
if (input.metadata) {
task.metadata = { ...(task.metadata || {}), ...(input.metadata as Record<string, unknown>) };
// Remove null keys
for (const [k, v] of Object.entries(task.metadata!)) {
if (v === null) delete task.metadata![k];
}
}
// Dependency management
const addBlocks = input.addBlocks as string[] | undefined;
const addBlockedBy = input.addBlockedBy as string[] | undefined;
if (addBlocks) {
for (const id of addBlocks) {
if (!task.blocks.includes(id)) task.blocks.push(id);
const target = taskState.find((t) => t.id === id);
if (target && !target.blockedBy.includes(taskId)) target.blockedBy.push(taskId);
}
}
if (addBlockedBy) {
for (const id of addBlockedBy) {
if (!task.blockedBy.includes(id)) task.blockedBy.push(id);
const target = taskState.find((t) => t.id === id);
if (target && !target.blocks.includes(taskId)) target.blocks.push(taskId);
}
}
persistTasks();
return { success: true, output: `Updated task #${taskId}: ${task.subject} [${task.status}]` };
}
case "list": {
if (taskState.length === 0) return { success: true, output: "No tasks." };
const icon: Record<string, string> = { pending: "[ ]", in_progress: "[~]", completed: "[x]" };
const lines = taskState.map((t) => {
let line = `#${t.id}. ${icon[t.status] || "[ ]"} ${t.subject}`;
if (t.owner) line += ` (${t.owner})`;
// Show only open blockers
const openBlockers = t.blockedBy.filter((id) => {
const blocker = taskState.find((b) => b.id === id);
return blocker && blocker.status !== "completed";
});
if (openBlockers.length > 0) line += ` <- blocked by #${openBlockers.join(", #")}`;
return line;
});
const counts = {
pending: taskState.filter((t) => t.status === "pending").length,
in_progress: taskState.filter((t) => t.status === "in_progress").length,
completed: taskState.filter((t) => t.status === "completed").length,
};
return {
success: true,
output: `Tasks (${taskState.length}: ${counts.completed} done, ${counts.in_progress} active, ${counts.pending} pending):\n${lines.join("\n")}`,
};
}
case "get": {
const taskId = input.taskId as string;
if (!taskId) return { success: false, output: "taskId required for get" };
const task = taskState.find((t) => t.id === taskId);
if (!task) return { success: false, output: `Task #${taskId} not found` };
const details = [
`# Task #${task.id}: ${task.subject}`,
`Status: ${task.status}`,
task.owner ? `Owner: ${task.owner}` : null,
task.activeForm ? `Active form: ${task.activeForm}` : null,
`Created: ${task.createdAt}`,
task.blocks.length ? `Blocks: #${task.blocks.join(", #")}` : null,
task.blockedBy.length ? `Blocked by: #${task.blockedBy.join(", #")}` : null,
task.metadata ? `Metadata: ${JSON.stringify(task.metadata)}` : null,
"",
task.description,
].filter(Boolean).join("\n");
return { success: true, output: details };
}
default:
return { success: false, output: `Unknown action: ${action}. Use create/update/list/get.` };
}
}
/** Persist tasks to disk (fire-and-forget) */
function persistTasks(): void {
if (!todoSessionId) return;
try {
if (!existsSync(TODOS_DIR)) mkdirSync(TODOS_DIR, { recursive: true });
writeFileSync(
join(TODOS_DIR, `${todoSessionId}.json`),
JSON.stringify({ tasks: taskState, counter: taskCounter }, null, 2),
"utf-8"
);
} catch { /* best effort */ }
}
/** Load tasks from disk for a session */
export function loadTodos(sessionId: string): void {
const path = join(TODOS_DIR, `${sessionId}.json`);
if (!existsSync(path)) return;
try {
const raw = JSON.parse(readFileSync(path, "utf-8"));
// Support both old format (array) and new format ({tasks, counter})
if (Array.isArray(raw)) {
// Migrate old todo format
taskState = raw.map((t: any, i: number) => ({
id: t.id || String(i + 1),
subject: t.content || t.subject || "Untitled",
description: t.content || t.description || "",
status: t.status || "pending",
activeForm: t.activeForm,
blocks: t.blocks || [],
blockedBy: t.blockedBy || [],
createdAt: t.createdAt || new Date().toISOString(),
}));
taskCounter = taskState.length;
} else if (raw.tasks) {
taskState = raw.tasks;
taskCounter = raw.counter || taskState.length;
}
todoSessionId = sessionId;
} catch { /* skip corrupted */ }
}
/** Link tasks to a session for persistence */
export function setTodoSessionId(id: string): void {
todoSessionId = id;
}
/** Get current task state (for UI display) */
export function getTodoState(): typeof taskState {
return taskState;
}