// CRUD operations for tasks import { query, queryOne, execute, getNextTaskId, getProjectKey, detectProjectFromCwd } from '../db.js'; import { getEmbedding, formatEmbedding } from '../embeddings.js'; import type { Task, ChecklistItem, TaskLink } from '../types.js'; import { getRecentDelegations } from './delegations.js'; import { getTaskCommits } from './commits.js'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; /** * Get current session ID from environment or cache file */ function getSessionId(): string { // Check environment first if (process.env.CLAUDE_SESSION_ID) { return process.env.CLAUDE_SESSION_ID; } // Try to read from cache file (usage-stats format) const cacheFile = path.join(os.homedir(), '.claude', 'cache', '.current_session_id'); try { const sessionId = fs.readFileSync(cacheFile, 'utf-8').trim(); if (sessionId) return sessionId; } catch { // File doesn't exist or can't be read } // Generate a new session ID const now = new Date(); const timestamp = now.toISOString().replace(/[-:T]/g, '').slice(0, 15); return `session_${timestamp}`; } /** * Record task activity for session tracking */ async function recordActivity( task_id: string, activity_type: 'created' | 'updated' | 'status_change' | 'closed', old_value?: string, new_value?: string ): Promise { const session_id = getSessionId(); try { await execute( `INSERT INTO task_activity (task_id, session_id, activity_type, old_value, new_value) VALUES ($1, $2, $3, $4, $5)`, [task_id, session_id, activity_type, old_value || null, new_value || null] ); } catch { // Don't fail the main operation if activity tracking fails console.error('Failed to record task activity'); } } 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); // Generate embedding for duplicate detection const embedText = description ? `${title}. ${description}` : title; const embedding = await getEmbedding(embedText); const embeddingValue = embedding ? formatEmbedding(embedding) : null; // Check for similar/duplicate tasks (only if embedding succeeded) let duplicateWarning = ''; if (embeddingValue) { const similarTasks = await query<{ id: string; title: string; status: string; similarity: number }>( `SELECT id, title, status, 1 - (embedding <=> $1) as similarity FROM tasks WHERE project = $2 AND embedding IS NOT NULL AND status != 'completed' ORDER BY embedding <=> $1 LIMIT 3`, [embeddingValue, projectKey] ); // Warn if highly similar tasks exist (>70% similarity) const highSimilarity = similarTasks.filter(t => t.similarity > 0.70); if (highSimilarity.length > 0) { duplicateWarning = '\n\n⚠️ Similar tasks found:\n'; for (const t of highSimilarity) { const pct = Math.round(t.similarity * 100); duplicateWarning += ` - ${t.id}: ${t.title} (${pct}% match, ${t.status})\n`; } duplicateWarning += '\nConsider linking with: task link relates_to'; } } // Get next task ID const taskId = await getNextTaskId(projectKey); // 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] ); } // Record activity for session tracking await recordActivity(taskId, 'created', undefined, 'open'); return `Created: ${taskId}\n Title: ${title}\n Type: ${type}\n Priority: ${priority}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}${duplicateWarning}`; } /** * List tasks with filters * Auto-detects project from CWD if not explicitly provided */ 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; // Auto-detect project from CWD if not explicitly provided const effectiveProject = project || detectProjectFromCwd(); if (effectiveProject) { const projectKey = await getProjectKey(effectiveProject); 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${effectiveProject ? ` for project ${effectiveProject}` : ''}`; } 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${effectiveProject ? ` (${effectiveProject})` : ''}:\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 commits const commitHistory = await getTaskCommits(id); if (commitHistory) { output += commitHistory; } // Get recent delegations const delegationHistory = await getRecentDelegations(id); if (delegationHistory) { output += delegationHistory; } return output; } /** * Close a task */ export async function taskClose(id: string): Promise { // Get current status for activity tracking const task = await queryOne<{ status: string }>(`SELECT status FROM tasks WHERE id = $1`, [id]); 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}`; } // Record activity await recordActivity(id, 'closed', task?.status, 'completed'); return `Closed: ${id}`; } /** * Update a task */ export async function taskUpdate(args: TaskUpdateArgs): Promise { const { id, status, priority, type, title } = args; // Get current values for activity tracking const task = await queryOne<{ status: string }>(`SELECT status FROM tasks WHERE id = $1`, [id]); if (!task) { return `Task not found: ${id}`; } 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}`; } // Record activity if (status && status !== task.status) { await recordActivity(id, 'status_change', task.status, status); } else { await recordActivity(id, 'updated'); } return `Updated: ${id}`; }