diff --git a/src/cli/commands/close.ts b/src/cli/commands/close.ts index f7619d3..f0ec4c6 100644 --- a/src/cli/commands/close.ts +++ b/src/cli/commands/close.ts @@ -1,54 +1,65 @@ import { Command } from 'commander'; import { createEvent } from '../../core/eventService.js'; -import { updateTaskStatus, getTask } from '../../core/taskService.js'; +import { updateTaskStatus, getTask, findTaskByTitle, getActiveTasks, getLastStartedActiveTask } from '../../core/taskService.js'; export function registerClose(program: Command): void { program .command('close') - .description('Close a task') + .description('Close a task (auto-selects when only one active task exists)') .option('--task ', 'Task ID to close') .option('--summary ', 'Closing summary') .action(async (options) => { try { - let taskId = options.task; + let taskId: string | undefined = options.task; - // If no task specified, we still create the event - const summary = options.summary ?? (taskId ? `Closed task` : 'Closed work session'); - - if (taskId) { - const task = getTask(taskId); - if (!task) { - console.error(`Task not found: ${taskId}`); + if (!taskId) { + const activeTasks = getActiveTasks(); + if (activeTasks.length === 0) { + console.error('No active tasks found. Nothing to close.'); + process.exit(1); + } + if (activeTasks.length === 1) { + taskId = activeTasks[0].id; + } else { + // Multiple active tasks — show list with default suggestion + const suggested = getLastStartedActiveTask(); + console.error(`Multiple active tasks found. Specify --task :`); + for (const t of activeTasks) { + const marker = suggested && t.id === suggested.id ? ' *' : ''; + console.error(` ${t.id} ${t.title}${marker}`); + } + if (suggested) { + console.error(`\n* Most recently started (suggested default)`); + console.error(` Run: ingest close --task ${suggested.id}`); + } process.exit(1); } + } - const event = createEvent({ - event_type: 'task_closed', - task_id: taskId, - project_id: task.project_id ?? undefined, - actor: 'human', - origin: 'manual', - summary, - }); + const task = getTask(taskId) ?? findTaskByTitle(taskId); + if (!task) { + console.error(`Task not found: ${taskId}`); + process.exit(1); + } + taskId = task.id; - updateTaskStatus(taskId, 'closed'); + const summary = options.summary ?? `Closed task: ${task.title}`; - console.log(`Task closed: ${event.id}`); - console.log(`Task: ${task.title}`); - console.log(`Summary: ${event.summary}`); - console.log(`Time: ${event.occurred_at}`); - } else { - const event = createEvent({ - event_type: 'task_closed', - actor: 'human', - origin: 'manual', - summary, - }); + const event = createEvent({ + event_type: 'task_closed', + task_id: taskId, + project_id: task.project_id ?? undefined, + actor: 'human', + origin: 'manual', + summary, + }); - console.log(`Session closed: ${event.id}`); - console.log(`Summary: ${event.summary}`); - console.log(`Time: ${event.occurred_at}`); - } + updateTaskStatus(taskId, 'closed'); + + console.log(`Task closed: ${event.id}`); + console.log(`Task: ${task.title}`); + console.log(`Summary: ${event.summary}`); + console.log(`Time: ${event.occurred_at}`); } catch (err) { console.error('Error:', err instanceof Error ? err.message : String(err)); process.exit(1); diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index 2fde8ee..a7fad73 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -4,12 +4,19 @@ import { findOrCreateProject } from '../../core/projectService.js'; import { findOrCreateTopic } from '../../core/topicService.js'; import { createEvent } from '../../core/eventService.js'; +function defaultTaskTitle(summary?: string): string { + if (summary) return summary; + const now = new Date(); + const pad = (n: number) => String(n).padStart(2, '0'); + return `Session ${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`; +} + export function registerStart(program: Command): void { program .command('start') .description('Start a new task session') .option('--project ', 'Project name') - .option('--task ', 'Task title') + .option('--task <title>', 'Task title (auto-generated from --summary or timestamp if omitted)') .option('--topic <name>', 'Topic name') .option('--summary <text>', 'Summary') .action(async (options) => { @@ -21,12 +28,11 @@ export function registerStart(program: Command): void { console.log(`Project: ${project.name} (${project.id})`); } - let taskId: string | undefined; - if (options.task) { - const task = findOrCreateTask(options.task, projectId); - taskId = task.id; - console.log(`Task: ${task.title} (${task.id})`); - } + // Always create/find a task so that subsequent events are linked + const taskTitle = options.task ?? defaultTaskTitle(options.summary); + const task = findOrCreateTask(taskTitle, projectId); + const taskId = task.id; + console.log(`Task: ${task.title} (${task.id})`); let topicId: string | undefined; if (options.topic) { @@ -35,7 +41,7 @@ export function registerStart(program: Command): void { console.log(`Topic: ${topic.name} (${topic.id})`); } - const summary = options.summary ?? (options.task ? `Started task: ${options.task}` : 'Started work session'); + const summary = options.summary ?? `Started task: ${taskTitle}`; const event = createEvent({ event_type: 'task_started', diff --git a/src/core/taskService.ts b/src/core/taskService.ts index ff9a0d2..d3ab5e3 100644 --- a/src/core/taskService.ts +++ b/src/core/taskService.ts @@ -57,6 +57,23 @@ export function getActiveTasks(): Task[] { ).all() as Task[]; } +export function findTaskByTitle(title: string): Task | null { + const db = getDb(); + return (db.prepare('SELECT * FROM tasks WHERE title = ? LIMIT 1').get(title) as Task) ?? null; +} + +export function getLastStartedActiveTask(): Task | null { + const db = getDb(); + const row = db.prepare(` + SELECT t.* FROM tasks t + INNER JOIN events e ON e.task_id = t.id + WHERE t.status = 'active' AND e.event_type = 'task_started' + ORDER BY e.occurred_at DESC + LIMIT 1 + `).get() as Task | undefined; + return row ?? null; +} + export function getAllTasksWithMetrics( status?: TaskStatus ): Array<{ task: Task; metrics: TaskMetrics | null }> {