// CRUD operations for tasks import { query, queryOne, execute, getNextTaskId, getProjectKey } from '../db.js'; import { getEmbedding, formatEmbedding } from '../embeddings.js'; import type { Task, ChecklistItem, TaskLink } from '../types.js'; import { getRecentDelegations } from './delegations.js'; interface TaskAddArgs { title: string; project?: string; type?: string; priority?: string; description?: string; } interface TaskListArgs { project?: string; status?: string; type?: string; priority?: string; limit?: number; } interface TaskUpdateArgs { id: string; status?: string; priority?: string; type?: string; title?: string; } /** * Create a new task */ export async function taskAdd(args: TaskAddArgs): Promise { const { title, project = 'Unknown', type = 'task', priority = 'P2', description = '' } = args; // Get project key const projectKey = await getProjectKey(project); // Get next task ID const taskId = await getNextTaskId(projectKey); // Generate embedding const embedText = description ? `${title}. ${description}` : title; const embedding = await getEmbedding(embedText); const embeddingValue = embedding ? formatEmbedding(embedding) : null; // Insert task if (embeddingValue) { await execute( `INSERT INTO tasks (id, project, title, description, type, status, priority, embedding) VALUES ($1, $2, $3, $4, $5, 'open', $6, $7)`, [taskId, projectKey, title, description, type, priority, embeddingValue] ); } else { await execute( `INSERT INTO tasks (id, project, title, description, type, status, priority) VALUES ($1, $2, $3, $4, $5, 'open', $6)`, [taskId, projectKey, title, description, type, priority] ); } return `Created: ${taskId}\n Title: ${title}\n Type: ${type}\n Priority: ${priority}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}`; } /** * List tasks with filters */ export async function taskList(args: TaskListArgs): Promise { const { project, status, type, priority, limit = 20 } = args; let whereClause = 'WHERE 1=1'; const params: unknown[] = []; let paramIndex = 1; if (project) { const projectKey = await getProjectKey(project); whereClause += ` AND project = $${paramIndex++}`; params.push(projectKey); } if (status) { whereClause += ` AND status = $${paramIndex++}`; params.push(status); } if (type) { whereClause += ` AND type = $${paramIndex++}`; params.push(type); } if (priority) { whereClause += ` AND priority = $${paramIndex++}`; params.push(priority); } params.push(limit); const tasks = await query( `SELECT id, title, type, status, priority, project FROM tasks ${whereClause} ORDER BY CASE priority WHEN 'P0' THEN 0 WHEN 'P1' THEN 1 WHEN 'P2' THEN 2 ELSE 3 END, created_at DESC LIMIT $${paramIndex}`, params ); if (tasks.length === 0) { return `No tasks found${project ? ` for project ${project}` : ''}`; } const lines = tasks.map(t => { const statusIcon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[>]' : t.status === 'blocked' ? '[!]' : '[ ]'; const typeLabel = t.type !== 'task' ? ` [${t.type}]` : ''; return `${statusIcon} ${t.priority} ${t.id}: ${t.title}${typeLabel}`; }); return `Tasks${project ? ` (${project})` : ''}:\n\n${lines.join('\n')}`; } /** * Show task details */ export async function taskShow(id: string): Promise { const task = await queryOne( `SELECT id, project, title, description, type, status, priority, to_char(created_at, 'YYYY-MM-DD HH24:MI') as created, to_char(updated_at, 'YYYY-MM-DD HH24:MI') as updated, to_char(completed_at, 'YYYY-MM-DD HH24:MI') as completed FROM tasks WHERE id = $1`, [id] ); if (!task) { return `Task not found: ${id}`; } let output = `# ${task.id}\n\n`; output += `**Title:** ${task.title}\n`; output += `**Project:** ${task.project}\n`; output += `**Type:** ${task.type}\n`; output += `**Status:** ${task.status}\n`; output += `**Priority:** ${task.priority}\n`; output += `**Created:** ${(task as unknown as { created: string }).created}\n`; output += `**Updated:** ${(task as unknown as { updated: string }).updated}\n`; if ((task as unknown as { completed: string }).completed) { output += `**Completed:** ${(task as unknown as { completed: string }).completed}\n`; } if (task.description) { output += `\n**Description:**\n${task.description}\n`; } // Get checklist const checklist = await query( `SELECT id, item, checked FROM task_checklist WHERE task_id = $1 ORDER BY position, id`, [id] ); if (checklist.length > 0) { const done = checklist.filter(c => c.checked).length; output += `\n**Checklist:** (${done}/${checklist.length})\n`; for (const item of checklist) { output += ` ${item.checked ? '[x]' : '[ ]'} ${item.item} (#${item.id})\n`; } } // Get dependencies const blockedBy = await query<{ id: string; title: string }>( `SELECT t.id, t.title FROM task_links l JOIN tasks t ON t.id = l.from_task_id WHERE l.to_task_id = $1 AND l.link_type = 'blocks'`, [id] ); const blocks = await query<{ id: string; title: string }>( `SELECT t.id, t.title FROM task_links l JOIN tasks t ON t.id = l.to_task_id WHERE l.from_task_id = $1 AND l.link_type = 'blocks'`, [id] ); if (blockedBy.length > 0) { output += `\n**Blocked by:**\n`; for (const t of blockedBy) { output += ` - ${t.id}: ${t.title}\n`; } } if (blocks.length > 0) { output += `\n**Blocks:**\n`; for (const t of blocks) { output += ` - ${t.id}: ${t.title}\n`; } } // Get related tasks (bidirectional - only need to query one direction since links are symmetric) const relatesTo = await query<{ id: string; title: string }>( `SELECT t.id, t.title FROM task_links l JOIN tasks t ON t.id = l.to_task_id WHERE l.from_task_id = $1 AND l.link_type = 'relates_to'`, [id] ); if (relatesTo.length > 0) { output += `\n**Related:**\n`; for (const t of relatesTo) { output += ` - ${t.id}: ${t.title}\n`; } } // Get duplicates (bidirectional) const duplicates = await query<{ id: string; title: string }>( `SELECT t.id, t.title FROM task_links l JOIN tasks t ON t.id = l.to_task_id WHERE l.from_task_id = $1 AND l.link_type = 'duplicates'`, [id] ); if (duplicates.length > 0) { output += `\n**Duplicates:**\n`; for (const t of duplicates) { output += ` - ${t.id}: ${t.title}\n`; } } // Get recent delegations const delegationHistory = await getRecentDelegations(id); if (delegationHistory) { output += delegationHistory; } return output; } /** * Close a task */ export async function taskClose(id: string): Promise { const result = await execute( `UPDATE tasks SET status = 'completed', completed_at = NOW(), updated_at = NOW() WHERE id = $1`, [id] ); if (result === 0) { return `Task not found: ${id}`; } return `Closed: ${id}`; } /** * Update a task */ export async function taskUpdate(args: TaskUpdateArgs): Promise { const { id, status, priority, type, title } = args; const updates: string[] = []; const params: unknown[] = []; let paramIndex = 1; if (status) { updates.push(`status = $${paramIndex++}`); params.push(status); if (status === 'completed') { updates.push(`completed_at = NOW()`); } } if (priority) { updates.push(`priority = $${paramIndex++}`); params.push(priority); } if (type) { updates.push(`type = $${paramIndex++}`); params.push(type); } if (title) { updates.push(`title = $${paramIndex++}`); params.push(title); } if (updates.length === 0) { return 'No updates specified'; } updates.push('updated_at = NOW()'); params.push(id); const result = await execute( `UPDATE tasks SET ${updates.join(', ')} WHERE id = $${paramIndex}`, params ); if (result === 0) { return `Task not found: ${id}`; } return `Updated: ${id}`; }