diff --git a/src/cli/commands/tasks.ts b/src/cli/commands/tasks.ts new file mode 100644 index 0000000..0191ae0 --- /dev/null +++ b/src/cli/commands/tasks.ts @@ -0,0 +1,21 @@ +import { Command } from 'commander'; +import { getAllTasksWithMetrics } from '../../core/taskService.js'; +import { formatTasksOutput } from '../../lib/formatting.js'; +import { TaskStatus } from '../../types/task.js'; + +export function registerTasks(program: Command): void { + program + .command('tasks') + .description('List tasks with metrics summary') + .option('--status ', 'Filter by status (active, paused, blocked, closed)') + .action(async (options) => { + try { + const status = options.status as TaskStatus | undefined; + const entries = getAllTasksWithMetrics(status); + console.log(formatTasksOutput(entries)); + } catch (err) { + console.error('Error:', err instanceof Error ? err.message : String(err)); + process.exit(1); + } + }); +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 6168bdf..b3284d4 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -18,6 +18,7 @@ import { registerIngestArtifactUpdated } from './commands/ingestArtifactUpdated. import { registerSkill } from './commands/skill.js'; import { registerLog } from './commands/log.js'; import { registerTopics } from './commands/topics.js'; +import { registerTasks } from './commands/tasks.js'; import { registerShow } from './commands/show.js'; // Ensure ~/.worklog directory and DB exist on startup @@ -47,6 +48,7 @@ registerIngestArtifactUpdated(program); registerSkill(program); registerLog(program); registerTopics(program); +registerTasks(program); registerShow(program); program.parse(); diff --git a/src/core/taskService.ts b/src/core/taskService.ts index 8d0ad46..ff9a0d2 100644 --- a/src/core/taskService.ts +++ b/src/core/taskService.ts @@ -1,5 +1,6 @@ import { getDb } from '../db/client.js'; import { Task, TaskStatus } from '../types/task.js'; +import { TaskMetrics } from '../types/metrics.js'; import { generateId } from '../lib/ids.js'; import { nowISO } from '../lib/time.js'; @@ -55,3 +56,19 @@ export function getActiveTasks(): Task[] { "SELECT * FROM tasks WHERE status = 'active' ORDER BY importance DESC, updated_at DESC" ).all() as Task[]; } + +export function getAllTasksWithMetrics( + status?: TaskStatus +): Array<{ task: Task; metrics: TaskMetrics | null }> { + const db = getDb(); + const tasks = status + ? (db.prepare('SELECT * FROM tasks WHERE status = ? ORDER BY importance DESC, updated_at DESC').all(status) as Task[]) + : (db.prepare("SELECT * FROM tasks WHERE status != 'closed' ORDER BY importance DESC, updated_at DESC").all() as Task[]); + + return tasks.map((task) => { + const metrics = (db.prepare( + 'SELECT * FROM task_metrics WHERE task_id = ? ORDER BY calculated_at DESC LIMIT 1' + ).get(task.id) as TaskMetrics) ?? null; + return { task, metrics }; + }); +} diff --git a/src/lib/formatting.ts b/src/lib/formatting.ts index 62580c1..aa4bf7c 100644 --- a/src/lib/formatting.ts +++ b/src/lib/formatting.ts @@ -255,6 +255,42 @@ export function formatTopicsOutput( return lines.join('\n'); } +const STATUS_LABELS: Record = { + active: 'ACTIVE', + paused: 'PAUSED', + blocked: 'BLOCKED', + closed: 'CLOSED', +}; + +export function formatTasksOutput( + entries: Array<{ task: Task; metrics: import('../types/metrics.js').TaskMetrics | null }> +): string { + if (entries.length === 0) { + return 'No tasks found.'; + } + + const lines: string[] = []; + lines.push('='.repeat(72)); + lines.push(`TASKS (${entries.length} total)`); + lines.push('='.repeat(72)); + lines.push( + `${'TITLE'.padEnd(28)} ${'STATUS'.padEnd(7)} ${'IMP'.padStart(4)} ${'DRIFT'.padStart(5)} ${'LAST SEEN'.padEnd(16)}` + ); + lines.push('-'.repeat(72)); + + for (const { task, metrics } of entries) { + const title = truncate(task.title, 28).padEnd(28); + const status = (STATUS_LABELS[task.status] ?? task.status).padEnd(7); + const imp = task.importance.toFixed(1).padStart(4); + const drift = metrics ? metrics.drift_score.toFixed(2).padStart(5) : ' N/A'; + const lastSeen = metrics?.last_seen_at ? formatDate(metrics.last_seen_at) : 'never'; + lines.push(`${title} ${status} ${imp} ${drift} ${lastSeen}`); + } + + lines.push('='.repeat(72)); + return lines.join('\n'); +} + export function formatShowOutput( topic: Topic, events: Event[],