@task-mcp/cli 1.0.28 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1148 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -31,7 +31,17 @@ import {
31
31
  priorityColors,
32
32
  formatPriority,
33
33
  formatDependencies,
34
- banner
34
+ banner,
35
+ STATUS_SYMBOLS,
36
+ formatStatusSymbol,
37
+ PRIORITY_LEVELS,
38
+ formatPriorityBadge,
39
+ renderSection,
40
+ renderSections,
41
+ renderAlert,
42
+ renderStats,
43
+ formatDueDate,
44
+ formatTaskLine
35
45
  } from "@task-mcp/shared";
36
46
  import { truncateStr as truncateStr2 } from "@task-mcp/shared";
37
47
 
@@ -42,10 +52,11 @@ import {
42
52
  } from "@task-mcp/shared";
43
53
 
44
54
  // src/storage.ts
45
- import { TaskStore, InboxStore, StateStore } from "@task-mcp/mcp-server/storage";
55
+ import { TaskStore, InboxStore, StateStore, SessionStore, getDefaultTasksDir } from "@task-mcp/mcp-server/storage";
46
56
  var _taskStore = null;
47
57
  var _inboxStore = null;
48
58
  var _stateStore = null;
59
+ var _sessionStore = null;
49
60
  function getTaskStore() {
50
61
  if (!_taskStore) {
51
62
  _taskStore = new TaskStore;
@@ -64,6 +75,13 @@ function getStateStore() {
64
75
  }
65
76
  return _stateStore;
66
77
  }
78
+ function getSessionStore() {
79
+ if (!_sessionStore) {
80
+ const taskStore = getTaskStore();
81
+ _sessionStore = new SessionStore(getDefaultTasksDir(), taskStore.currentWorkspace);
82
+ }
83
+ return _sessionStore;
84
+ }
67
85
  function getCurrentWorkspace() {
68
86
  return getTaskStore().currentWorkspace;
69
87
  }
@@ -93,6 +111,149 @@ async function listInboxItems(status) {
93
111
  async function getActiveTag() {
94
112
  return getStateStore().getActiveTag();
95
113
  }
114
+ function calculateStats(tasks) {
115
+ const stats = {
116
+ total: tasks.length,
117
+ completed: 0,
118
+ inProgress: 0,
119
+ pending: 0,
120
+ blocked: 0,
121
+ cancelled: 0,
122
+ byPriority: { critical: 0, high: 0, medium: 0, low: 0 },
123
+ completionPercent: 0
124
+ };
125
+ for (const task of tasks) {
126
+ switch (task.status) {
127
+ case "completed":
128
+ stats.completed++;
129
+ break;
130
+ case "in_progress":
131
+ stats.inProgress++;
132
+ break;
133
+ case "pending":
134
+ stats.pending++;
135
+ break;
136
+ case "blocked":
137
+ stats.blocked++;
138
+ break;
139
+ case "cancelled":
140
+ stats.cancelled++;
141
+ break;
142
+ }
143
+ stats.byPriority[task.priority]++;
144
+ }
145
+ const nonCancelled = stats.total - stats.cancelled;
146
+ stats.completionPercent = nonCancelled > 0 ? Math.round(stats.completed / nonCancelled * 100) : 0;
147
+ return stats;
148
+ }
149
+ function calculateDependencyMetrics(tasks) {
150
+ const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
151
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
152
+ const dependentCounts = {};
153
+ for (const task of activeTasks) {
154
+ for (const dep of task.dependencies ?? []) {
155
+ dependentCounts[dep.taskId] = (dependentCounts[dep.taskId] ?? 0) + 1;
156
+ }
157
+ }
158
+ let mostDependedOn = null;
159
+ let maxCount = 0;
160
+ for (const [id, count] of Object.entries(dependentCounts)) {
161
+ if (count > maxCount) {
162
+ maxCount = count;
163
+ const task = tasks.find((t) => t.id === id);
164
+ if (task) {
165
+ mostDependedOn = { id, title: task.title, count };
166
+ }
167
+ }
168
+ }
169
+ const noDependencies = activeTasks.filter((t) => (t.dependencies?.length ?? 0) === 0).length;
170
+ const readyToWork = activeTasks.filter((t) => {
171
+ const deps = t.dependencies ?? [];
172
+ if (deps.length === 0)
173
+ return true;
174
+ return deps.every((d) => completedIds.has(d.taskId));
175
+ }).length;
176
+ const blockedByDependencies = activeTasks.length - readyToWork;
177
+ const totalDeps = activeTasks.reduce((sum, t) => sum + (t.dependencies?.length ?? 0), 0);
178
+ const avgDependencies = activeTasks.length > 0 ? totalDeps / activeTasks.length : 0;
179
+ return {
180
+ noDependencies,
181
+ readyToWork,
182
+ blockedByDependencies,
183
+ mostDependedOn,
184
+ avgDependencies: Math.round(avgDependencies * 10) / 10
185
+ };
186
+ }
187
+ function suggestNextTask(tasks) {
188
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
189
+ const actionable = tasks.filter((t) => {
190
+ if (t.status !== "pending" && t.status !== "in_progress")
191
+ return false;
192
+ const deps = t.dependencies ?? [];
193
+ return deps.every((d) => completedIds.has(d.taskId));
194
+ });
195
+ if (actionable.length === 0)
196
+ return null;
197
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
198
+ actionable.sort((a, b) => {
199
+ const pDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
200
+ if (pDiff !== 0)
201
+ return pDiff;
202
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
203
+ });
204
+ return actionable[0] ?? null;
205
+ }
206
+ function formatTimeSince(isoTimestamp) {
207
+ const then = new Date(isoTimestamp).getTime();
208
+ const nowMs = Date.now();
209
+ const diffMs = nowMs - then;
210
+ const minutes = Math.floor(diffMs / (1000 * 60));
211
+ const hours = Math.floor(minutes / 60);
212
+ const days = Math.floor(hours / 24);
213
+ if (days > 0) {
214
+ return `${days}d ago`;
215
+ }
216
+ if (hours > 0) {
217
+ return `${hours}h ago`;
218
+ }
219
+ if (minutes > 0) {
220
+ return `${minutes}m ago`;
221
+ }
222
+ return "just now";
223
+ }
224
+ async function getLastSessionInfo() {
225
+ try {
226
+ const sessionStore = getSessionStore();
227
+ const lastSession = await sessionStore.getLastSession();
228
+ if (!lastSession || !lastSession.endedAt) {
229
+ return null;
230
+ }
231
+ const tasks = await listTasks();
232
+ const inProgressTasks = tasks.filter((t) => t.status === "in_progress");
233
+ const summary = lastSession.tasksSummary;
234
+ let currentInProgressTask;
235
+ if (inProgressTasks.length > 0) {
236
+ const sessionTaskIds = new Set(lastSession.tasksWorked);
237
+ const sessionInProgress = inProgressTasks.find((t) => sessionTaskIds.has(t.id));
238
+ const taskToShow = sessionInProgress ?? inProgressTasks[0];
239
+ if (taskToShow) {
240
+ currentInProgressTask = {
241
+ id: taskToShow.id,
242
+ title: taskToShow.title
243
+ };
244
+ }
245
+ }
246
+ return {
247
+ session: lastSession,
248
+ timeSince: formatTimeSince(lastSession.endedAt),
249
+ completed: summary?.completed ?? 0,
250
+ inProgress: inProgressTasks.length,
251
+ currentInProgressTask
252
+ };
253
+ } catch {
254
+ return null;
255
+ }
256
+ }
96
257
 
97
258
  // src/commands/dashboard.ts
98
259
  async function dashboard(workspaceName) {
@@ -141,6 +302,529 @@ async function dashboard(workspaceName) {
141
302
  console.log(output);
142
303
  }
143
304
 
305
+ // src/commands/smart-dashboard.ts
306
+ function getOverdueTasks(tasks) {
307
+ const today = new Date;
308
+ today.setHours(0, 0, 0, 0);
309
+ return tasks.filter((t) => {
310
+ if (t.status === "completed" || t.status === "cancelled")
311
+ return false;
312
+ if (!t.dueDate)
313
+ return false;
314
+ const due = new Date(t.dueDate);
315
+ due.setHours(0, 0, 0, 0);
316
+ return due < today;
317
+ });
318
+ }
319
+ function getStaleBlockedTasks(tasks, daysThreshold = 3) {
320
+ const now = Date.now();
321
+ const thresholdMs = daysThreshold * 24 * 60 * 60 * 1000;
322
+ return tasks.filter((t) => {
323
+ if (t.status !== "blocked")
324
+ return false;
325
+ const createdAt = new Date(t.createdAt).getTime();
326
+ return now - createdAt > thresholdMs;
327
+ });
328
+ }
329
+ function getDueSoonTasks(tasks) {
330
+ const today = new Date;
331
+ today.setHours(0, 0, 0, 0);
332
+ const tomorrow = new Date(today);
333
+ tomorrow.setDate(tomorrow.getDate() + 1);
334
+ const dayAfter = new Date(today);
335
+ dayAfter.setDate(dayAfter.getDate() + 2);
336
+ return tasks.filter((t) => {
337
+ if (t.status === "completed" || t.status === "cancelled")
338
+ return false;
339
+ if (!t.dueDate)
340
+ return false;
341
+ const due = new Date(t.dueDate);
342
+ due.setHours(0, 0, 0, 0);
343
+ return due >= today && due < dayAfter;
344
+ }).sort((a, b) => {
345
+ const dateA = new Date(a.dueDate).getTime();
346
+ const dateB = new Date(b.dueDate).getTime();
347
+ return dateA - dateB;
348
+ });
349
+ }
350
+ function getSuggestionReason(task, tasks) {
351
+ const reasons = [];
352
+ const blockingCount = tasks.filter((t) => t.dependencies?.some((d) => d.taskId === task.id)).length;
353
+ if (blockingCount > 0) {
354
+ reasons.push(`Unblocks ${blockingCount} task${blockingCount > 1 ? "s" : ""}`);
355
+ }
356
+ if (task.priority === "critical") {
357
+ reasons.push("Critical priority");
358
+ } else if (task.priority === "high") {
359
+ reasons.push("High priority");
360
+ }
361
+ if (task.dueDate) {
362
+ const today = new Date;
363
+ today.setHours(0, 0, 0, 0);
364
+ const due = new Date(task.dueDate);
365
+ due.setHours(0, 0, 0, 0);
366
+ const diffDays = Math.floor((due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
367
+ if (diffDays < 0) {
368
+ reasons.push("Overdue");
369
+ } else if (diffDays === 0) {
370
+ reasons.push("Due today");
371
+ } else if (diffDays === 1) {
372
+ reasons.push("Due tomorrow");
373
+ }
374
+ }
375
+ if (task.status === "in_progress") {
376
+ reasons.push("Already started");
377
+ }
378
+ return reasons.length > 0 ? reasons.join(", ") : "Ready to start";
379
+ }
380
+ function taskToSectionItem(task, options = {}) {
381
+ const prefix = formatStatusSymbol(task.status);
382
+ const idStr = c.gray(task.id.slice(-4));
383
+ const title = truncateStr(task.title, 35);
384
+ const primary = `${idStr} ${title}`;
385
+ const suffixParts = [];
386
+ if (task.priority) {
387
+ suffixParts.push(formatPriorityBadge(task.priority));
388
+ }
389
+ if (task.dueDate) {
390
+ suffixParts.push(formatDueDate(task.dueDate));
391
+ }
392
+ const details = [];
393
+ if (options.showReason) {
394
+ details.push(options.showReason);
395
+ }
396
+ return {
397
+ primary,
398
+ prefix,
399
+ suffix: suffixParts.join(" "),
400
+ details
401
+ };
402
+ }
403
+ function renderHeader(workspace, stats) {
404
+ const lines = [];
405
+ const title = `task-mcp`;
406
+ const wsName = c.cyan(workspace);
407
+ const bar = progressBar(stats.completed, stats.total - stats.cancelled, {
408
+ width: 10,
409
+ showPercent: false
410
+ });
411
+ lines.push(c.gray(BOX.rTopLeft + hline(60, BOX.horizontal) + BOX.rTopRight));
412
+ lines.push(c.gray(BOX.vertical) + ` ${c.bold(title)} ${c.gray("@")} ${wsName}` + " ".repeat(20) + `Health: ${bar} ${stats.completionPercent}%` + " " + c.gray(BOX.vertical));
413
+ lines.push(c.gray(BOX.rBottomLeft + hline(60, BOX.horizontal) + BOX.rBottomRight));
414
+ return lines;
415
+ }
416
+ function renderNextAction(tasks) {
417
+ const nextTask = suggestNextTask(tasks);
418
+ if (!nextTask) {
419
+ return renderSection("Next Action", [], {
420
+ icon: "\uD83C\uDFAF",
421
+ emptyMessage: "No actionable tasks"
422
+ });
423
+ }
424
+ const reason = getSuggestionReason(nextTask, tasks);
425
+ const item = taskToSectionItem(nextTask, { showReason: reason });
426
+ return renderSection("Next Action", [item], { icon: "\uD83C\uDFAF" });
427
+ }
428
+ function renderAttention(tasks) {
429
+ const alerts = [];
430
+ const overdue = getOverdueTasks(tasks);
431
+ if (overdue.length > 0) {
432
+ alerts.push(c.red(`${overdue.length} task${overdue.length > 1 ? "s" : ""} overdue`));
433
+ }
434
+ const staleBlocked = getStaleBlockedTasks(tasks, 3);
435
+ if (staleBlocked.length > 0) {
436
+ alerts.push(c.yellow(`${staleBlocked.length} task${staleBlocked.length > 1 ? "s" : ""} blocked > 3 days`));
437
+ }
438
+ const blockedCount = tasks.filter((t) => t.status === "blocked").length;
439
+ if (blockedCount > 0 && staleBlocked.length === 0) {
440
+ alerts.push(c.yellow(`${blockedCount} task${blockedCount > 1 ? "s" : ""} blocked`));
441
+ }
442
+ const inProgressCount = tasks.filter((t) => t.status === "in_progress").length;
443
+ if (inProgressCount > 0) {
444
+ alerts.push(c.cyan(`${inProgressCount} task${inProgressCount > 1 ? "s" : ""} in progress`));
445
+ }
446
+ if (alerts.length === 0) {
447
+ return renderSection("Attention", [], {
448
+ icon: "\u26A0",
449
+ emptyMessage: c.green("All clear!"),
450
+ headerColor: "yellow"
451
+ });
452
+ }
453
+ const items = alerts.map((alert) => ({
454
+ primary: alert,
455
+ prefix: c.gray("\u2022")
456
+ }));
457
+ return renderSection("Attention", items, {
458
+ icon: "\u26A0",
459
+ headerColor: "yellow"
460
+ });
461
+ }
462
+ function renderDueSoon(tasks) {
463
+ const dueSoon = getDueSoonTasks(tasks);
464
+ if (dueSoon.length === 0) {
465
+ return renderSection("Due Soon", [], {
466
+ icon: "\uD83D\uDCC5",
467
+ emptyMessage: "No upcoming deadlines"
468
+ });
469
+ }
470
+ const items = dueSoon.slice(0, 5).map((task) => taskToSectionItem(task));
471
+ return renderSection("Due Soon", items, {
472
+ icon: "\uD83D\uDCC5",
473
+ showCount: dueSoon.length > 5
474
+ });
475
+ }
476
+ function renderQuickStats(stats) {
477
+ const statsRow = renderStats([
478
+ { label: "Total", value: stats.total },
479
+ { label: "Done", value: `${stats.completed} (${stats.completionPercent}%)`, color: "green" },
480
+ { label: "In Progress", value: stats.inProgress, color: "cyan" },
481
+ { label: "Blocked", value: stats.blocked, color: stats.blocked > 0 ? "red" : "gray" }
482
+ ]);
483
+ return ["", c.gray(statsRow)];
484
+ }
485
+ function renderLastSession(sessionInfo) {
486
+ if (!sessionInfo) {
487
+ return [];
488
+ }
489
+ const lines = [];
490
+ const header = `${c.gray("Last Session:")} ${c.cyan(sessionInfo.timeSince)}`;
491
+ lines.push(` ${header}`);
492
+ const summaryParts = [];
493
+ if (sessionInfo.completed > 0) {
494
+ summaryParts.push(`${sessionInfo.completed} completed`);
495
+ }
496
+ if (sessionInfo.inProgress > 0) {
497
+ summaryParts.push(`${sessionInfo.inProgress} in progress`);
498
+ }
499
+ if (summaryParts.length > 0) {
500
+ lines.push(` ${c.gray("->")} ${summaryParts.join(", ")}`);
501
+ }
502
+ if (sessionInfo.currentInProgressTask) {
503
+ const taskId = c.gray(sessionInfo.currentInProgressTask.id.slice(-8));
504
+ const title = truncateStr(sessionInfo.currentInProgressTask.title, 40);
505
+ lines.push(` ${c.gray("->")} ${taskId}: ${title} ${c.cyan("(in_progress)")}`);
506
+ }
507
+ return lines;
508
+ }
509
+ async function smartDashboard() {
510
+ const workspaces = await listWorkspaces();
511
+ if (workspaces.length === 0) {
512
+ console.log(c.yellow("No tasks found. Create a task first using the MCP server."));
513
+ return;
514
+ }
515
+ const workspace = getCurrentWorkspace();
516
+ const tasks = await listTasks();
517
+ const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
518
+ const stats = calculateStats(tasks);
519
+ const lastSessionInfo = await getLastSessionInfo();
520
+ const lines = [];
521
+ lines.push(...renderHeader(workspace, stats));
522
+ lines.push("");
523
+ const lastSessionLines = renderLastSession(lastSessionInfo);
524
+ if (lastSessionLines.length > 0) {
525
+ lines.push(...lastSessionLines);
526
+ lines.push("");
527
+ }
528
+ lines.push(...renderNextAction(tasks));
529
+ lines.push("");
530
+ const attentionLines = renderAttention(tasks);
531
+ if (attentionLines.length > 2) {
532
+ lines.push(...attentionLines);
533
+ lines.push("");
534
+ }
535
+ lines.push(...renderDueSoon(tasks));
536
+ lines.push(...renderQuickStats(stats));
537
+ lines.push("");
538
+ lines.push(c.gray(" Run: task next | task health | task blocked | task tree"));
539
+ console.log(lines.join(`
540
+ `));
541
+ }
542
+
543
+ // src/commands/next.ts
544
+ var STRATEGY_DESCRIPTIONS = {
545
+ balanced: "Balance priority, dependencies, and due dates",
546
+ quick_wins: "Focus on quick, low-effort tasks",
547
+ unblock: "Prioritize tasks that unblock others",
548
+ critical_path: "Focus on critical path tasks"
549
+ };
550
+ function getDependentTasks(task, tasks) {
551
+ return tasks.filter((t) => t.dependencies?.some((d) => d.taskId === task.id));
552
+ }
553
+ function suggestByStrategy(tasks, strategy) {
554
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
555
+ const actionable = tasks.filter((t) => {
556
+ if (t.status !== "pending" && t.status !== "in_progress")
557
+ return false;
558
+ const deps = t.dependencies ?? [];
559
+ return deps.every((d) => completedIds.has(d.taskId));
560
+ });
561
+ if (actionable.length === 0)
562
+ return null;
563
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
564
+ switch (strategy) {
565
+ case "quick_wins":
566
+ actionable.sort((a, b) => {
567
+ const estA = a.estimate?.expected ?? 60;
568
+ const estB = b.estimate?.expected ?? 60;
569
+ if (estA !== estB)
570
+ return estA - estB;
571
+ return priorityOrder[a.priority] - priorityOrder[b.priority];
572
+ });
573
+ break;
574
+ case "unblock":
575
+ actionable.sort((a, b) => {
576
+ const depsA = getDependentTasks(a, tasks).length;
577
+ const depsB = getDependentTasks(b, tasks).length;
578
+ if (depsA !== depsB)
579
+ return depsB - depsA;
580
+ return priorityOrder[a.priority] - priorityOrder[b.priority];
581
+ });
582
+ break;
583
+ case "critical_path":
584
+ actionable.sort((a, b) => {
585
+ const pDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
586
+ if (pDiff !== 0)
587
+ return pDiff;
588
+ const depsA = getDependentTasks(a, tasks).length;
589
+ const depsB = getDependentTasks(b, tasks).length;
590
+ return depsB - depsA;
591
+ });
592
+ break;
593
+ case "balanced":
594
+ default:
595
+ actionable.sort((a, b) => {
596
+ const pDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
597
+ if (pDiff !== 0)
598
+ return pDiff;
599
+ const dueA = a.dueDate ? new Date(a.dueDate).getTime() : Infinity;
600
+ const dueB = b.dueDate ? new Date(b.dueDate).getTime() : Infinity;
601
+ if (dueA !== dueB)
602
+ return dueA - dueB;
603
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
604
+ });
605
+ break;
606
+ }
607
+ return actionable[0] ?? null;
608
+ }
609
+ function buildReasons(task, tasks) {
610
+ const reasons = [];
611
+ if (task.priority === "critical") {
612
+ reasons.push("\uD83D\uDD34 Critical priority");
613
+ } else if (task.priority === "high") {
614
+ reasons.push("\uD83D\uDFE0 High priority");
615
+ }
616
+ if (task.dueDate) {
617
+ const today = new Date;
618
+ today.setHours(0, 0, 0, 0);
619
+ const due = new Date(task.dueDate);
620
+ due.setHours(0, 0, 0, 0);
621
+ const diffDays = Math.floor((due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
622
+ if (diffDays < 0) {
623
+ reasons.push(`\u26A0\uFE0F Overdue by ${Math.abs(diffDays)} day${Math.abs(diffDays) > 1 ? "s" : ""}`);
624
+ } else if (diffDays === 0) {
625
+ reasons.push("\uD83D\uDCC5 Due today");
626
+ } else if (diffDays === 1) {
627
+ reasons.push("\uD83D\uDCC5 Due tomorrow");
628
+ } else if (diffDays <= 3) {
629
+ reasons.push(`\uD83D\uDCC5 Due in ${diffDays} days`);
630
+ }
631
+ }
632
+ const dependents = getDependentTasks(task, tasks);
633
+ if (dependents.length > 0) {
634
+ reasons.push(`\uD83D\uDD13 Unblocks ${dependents.length} task${dependents.length > 1 ? "s" : ""}`);
635
+ }
636
+ if (task.status === "in_progress") {
637
+ reasons.push("\u25B6\uFE0F Already in progress");
638
+ }
639
+ if (!task.dependencies || task.dependencies.length === 0) {
640
+ reasons.push("\u2705 No blockers");
641
+ }
642
+ if (reasons.length === 0) {
643
+ reasons.push("Ready to start");
644
+ }
645
+ return reasons;
646
+ }
647
+ function renderTaskDetail(task, tasks) {
648
+ const lines = [];
649
+ const status = formatStatusSymbol(task.status);
650
+ const priority = formatPriorityBadge(task.priority);
651
+ lines.push("");
652
+ lines.push(` ${status} ${c.bold(task.title)} ${priority}`);
653
+ lines.push("");
654
+ const details = [
655
+ ["ID", c.gray(task.id)],
656
+ ["Status", task.status],
657
+ ["Priority", task.priority]
658
+ ];
659
+ if (task.dueDate) {
660
+ details.push(["Due", formatDueDate(task.dueDate)]);
661
+ }
662
+ if (task.estimate?.expected) {
663
+ const hours = Math.floor(task.estimate.expected / 60);
664
+ const mins = task.estimate.expected % 60;
665
+ const timeStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
666
+ details.push(["Estimate", timeStr]);
667
+ }
668
+ if (task.tags && task.tags.length > 0) {
669
+ details.push(["Tags", task.tags.map((t) => c.cyan(`#${t}`)).join(" ")]);
670
+ }
671
+ for (const [label, value] of details) {
672
+ lines.push(` ${c.gray(label + ":")} ${value}`);
673
+ }
674
+ if (task.description) {
675
+ lines.push("");
676
+ lines.push(` ${c.gray("Description:")}`);
677
+ const descLines = task.description.split(`
678
+ `).slice(0, 3);
679
+ for (const line of descLines) {
680
+ lines.push(` ${c.dim(line)}`);
681
+ }
682
+ }
683
+ return lines;
684
+ }
685
+ function renderImpact(task, tasks) {
686
+ const dependents = getDependentTasks(task, tasks);
687
+ if (dependents.length === 0) {
688
+ return [];
689
+ }
690
+ const items = dependents.slice(0, 5).map((t) => ({
691
+ primary: `${c.gray(t.id.slice(-4))} ${t.title}`,
692
+ prefix: formatStatusSymbol(t.status)
693
+ }));
694
+ return [
695
+ "",
696
+ ...renderSection("Will Unblock", items, {
697
+ icon: "\uD83D\uDD13",
698
+ showCount: dependents.length > 5
699
+ })
700
+ ];
701
+ }
702
+ function renderAlternatives(currentStrategy) {
703
+ const others = Object.keys(STRATEGY_DESCRIPTIONS).filter((s) => s !== currentStrategy);
704
+ const lines = [
705
+ "",
706
+ c.gray(" Alternative strategies:")
707
+ ];
708
+ for (const strategy of others) {
709
+ lines.push(` ${c.cyan(`task next --strategy ${strategy}`)}`);
710
+ }
711
+ return lines;
712
+ }
713
+ async function nextCmd(options = {}) {
714
+ const strategy = options.strategy ?? "balanced";
715
+ const tasks = await listTasks();
716
+ const nextTask = suggestByStrategy(tasks, strategy);
717
+ if (!nextTask) {
718
+ console.log(c.yellow("No actionable tasks found."));
719
+ console.log(c.gray("All tasks may be completed, blocked, or have unmet dependencies."));
720
+ return;
721
+ }
722
+ const reasons = buildReasons(nextTask, tasks);
723
+ const lines = [];
724
+ lines.push(c.cyan(c.bold("\uD83C\uDFAF Suggested Next Task")));
725
+ lines.push(c.gray(hline(50)));
726
+ lines.push(...renderTaskDetail(nextTask, tasks));
727
+ lines.push("");
728
+ lines.push(c.gray(" Why this task:"));
729
+ for (const reason of reasons) {
730
+ lines.push(` ${reason}`);
731
+ }
732
+ lines.push(...renderImpact(nextTask, tasks));
733
+ lines.push("");
734
+ lines.push(c.gray(` Strategy: ${strategy} - ${STRATEGY_DESCRIPTIONS[strategy]}`));
735
+ lines.push(...renderAlternatives(strategy));
736
+ console.log(lines.join(`
737
+ `));
738
+ }
739
+
740
+ // src/commands/tree.ts
741
+ function buildTree(tasks) {
742
+ const taskMap = new Map;
743
+ for (const task of tasks) {
744
+ taskMap.set(task.id, task);
745
+ }
746
+ const nodeMap = new Map;
747
+ for (const task of tasks) {
748
+ nodeMap.set(task.id, { task, children: [] });
749
+ }
750
+ const roots = [];
751
+ for (const task of tasks) {
752
+ const node = nodeMap.get(task.id);
753
+ if (task.parentId && nodeMap.has(task.parentId)) {
754
+ const parent = nodeMap.get(task.parentId);
755
+ parent.children.push(node);
756
+ } else {
757
+ roots.push(node);
758
+ }
759
+ }
760
+ const sortNodes = (nodes) => {
761
+ nodes.sort((a, b) => {
762
+ const orderA = a.task.sortOrder ?? Infinity;
763
+ const orderB = b.task.sortOrder ?? Infinity;
764
+ if (orderA !== orderB)
765
+ return orderA - orderB;
766
+ return new Date(a.task.createdAt).getTime() - new Date(b.task.createdAt).getTime();
767
+ });
768
+ for (const node of nodes) {
769
+ sortNodes(node.children);
770
+ }
771
+ };
772
+ sortNodes(roots);
773
+ return roots;
774
+ }
775
+ function formatTaskLine2(task) {
776
+ const status = formatStatusSymbol(task.status);
777
+ const shortId = c.gray(task.id.slice(-4));
778
+ const title = task.title;
779
+ const priority = task.priority !== "medium" ? formatPriorityBadge(task.priority) : "";
780
+ return `${status} ${shortId} ${title}${priority ? " " + priority : ""}`;
781
+ }
782
+ function renderNode(node, prefix, isLast, isRoot, lines) {
783
+ const branch = isRoot ? "" : isLast ? "\\-- " : "|-- ";
784
+ const continuation = isLast ? " " : "| ";
785
+ const taskLine = formatTaskLine2(node.task);
786
+ lines.push(`${prefix}${branch}${taskLine}`);
787
+ const childPrefix = isRoot ? prefix : prefix + continuation;
788
+ const childCount = node.children.length;
789
+ for (let i = 0;i < childCount; i++) {
790
+ const child = node.children[i];
791
+ const childIsLast = i === childCount - 1;
792
+ renderNode(child, childPrefix, childIsLast, false, lines);
793
+ }
794
+ }
795
+ function renderTree(roots) {
796
+ const lines = [];
797
+ for (const root of roots) {
798
+ renderNode(root, "", true, true, lines);
799
+ if (roots.indexOf(root) < roots.length - 1) {
800
+ lines.push("");
801
+ }
802
+ }
803
+ return lines;
804
+ }
805
+ async function treeCmd(options = {}) {
806
+ const allTasks = await listTasks();
807
+ const tasks = options.all ? allTasks : allTasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
808
+ if (tasks.length === 0) {
809
+ console.log(c.yellow("No tasks found."));
810
+ if (!options.all) {
811
+ console.log(c.gray("Use --all to include completed/cancelled tasks."));
812
+ }
813
+ return;
814
+ }
815
+ const roots = buildTree(tasks);
816
+ const lines = [];
817
+ lines.push(c.cyan(c.bold("Task Hierarchy")));
818
+ lines.push(c.gray(hline(50)));
819
+ lines.push("");
820
+ lines.push(...renderTree(roots));
821
+ lines.push("");
822
+ lines.push(c.gray(hline(50)));
823
+ lines.push(c.gray("Legend: [ ] pending [~] progress [x] done [!] blocked"));
824
+ console.log(lines.join(`
825
+ `));
826
+ }
827
+
144
828
  // src/constants.ts
145
829
  var MENU_SEPARATOR_WIDTH = 40;
146
830
  var LIST_SEPARATOR_WIDTH = 50;
@@ -250,14 +934,26 @@ Workspaces (${workspaces.length})
250
934
 
251
935
  // src/interactive.ts
252
936
  import * as readline from "readline";
253
- async function prompt(question) {
937
+ async function prompt(question, options) {
254
938
  const rl = readline.createInterface({
255
939
  input: process.stdin,
256
940
  output: process.stdout
257
941
  });
258
942
  return new Promise((resolve) => {
259
- rl.question(question, (answer) => {
943
+ let timeoutId;
944
+ const cleanup = () => {
945
+ if (timeoutId)
946
+ clearTimeout(timeoutId);
260
947
  rl.close();
948
+ };
949
+ if (options?.timeoutMs !== undefined) {
950
+ timeoutId = setTimeout(() => {
951
+ cleanup();
952
+ resolve(options.defaultValue ?? "");
953
+ }, options.timeoutMs);
954
+ }
955
+ rl.question(question, (answer) => {
956
+ cleanup();
261
957
  resolve(answer.trim());
262
958
  });
263
959
  });
@@ -585,10 +1281,416 @@ async function inboxCountCmd() {
585
1281
  }
586
1282
  console.log();
587
1283
  }
1284
+
1285
+ // src/commands/blocked.ts
1286
+ function calculateDaysBlocked(task) {
1287
+ const blockedSince = task.updatedAt ?? task.createdAt;
1288
+ const blockedDate = new Date(blockedSince);
1289
+ const now = new Date;
1290
+ const diffMs = now.getTime() - blockedDate.getTime();
1291
+ return Math.floor(diffMs / (1000 * 60 * 60 * 24));
1292
+ }
1293
+ function getBlockerTasks(task, allTasks) {
1294
+ if (!task.dependencies || task.dependencies.length === 0)
1295
+ return [];
1296
+ const blockers = [];
1297
+ const completedStatuses = new Set(["completed", "cancelled"]);
1298
+ for (const dep of task.dependencies) {
1299
+ const blockerTask = allTasks.find((t) => t.id === dep.taskId);
1300
+ if (blockerTask && !completedStatuses.has(blockerTask.status)) {
1301
+ blockers.push(blockerTask);
1302
+ }
1303
+ }
1304
+ return blockers;
1305
+ }
1306
+ function getBlockReason(blockers) {
1307
+ if (blockers.length === 0) {
1308
+ return "Unknown (no active blockers found)";
1309
+ }
1310
+ const inProgress = blockers.filter((t) => t.status === "in_progress");
1311
+ const pending = blockers.filter((t) => t.status === "pending");
1312
+ const blocked = blockers.filter((t) => t.status === "blocked");
1313
+ const reasons = [];
1314
+ if (inProgress.length > 0) {
1315
+ reasons.push(`${inProgress.length} in progress`);
1316
+ }
1317
+ if (pending.length > 0) {
1318
+ reasons.push(`${pending.length} pending`);
1319
+ }
1320
+ if (blocked.length > 0) {
1321
+ reasons.push(`${blocked.length} blocked (chain)`);
1322
+ }
1323
+ return `Dependency tasks: ${reasons.join(", ")}`;
1324
+ }
1325
+ function analyzeBlockedTasks(tasks) {
1326
+ const blockedTasks = tasks.filter((t) => t.status === "blocked");
1327
+ const result = [];
1328
+ for (const task of blockedTasks) {
1329
+ const blockers = getBlockerTasks(task, tasks);
1330
+ const daysBlocked = calculateDaysBlocked(task);
1331
+ const isStale = daysBlocked >= 3;
1332
+ result.push({
1333
+ task,
1334
+ blockers,
1335
+ reason: getBlockReason(blockers),
1336
+ daysBlocked,
1337
+ isStale
1338
+ });
1339
+ }
1340
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
1341
+ result.sort((a, b) => {
1342
+ const pDiff = priorityOrder[a.task.priority] - priorityOrder[b.task.priority];
1343
+ if (pDiff !== 0)
1344
+ return pDiff;
1345
+ return b.daysBlocked - a.daysBlocked;
1346
+ });
1347
+ return result;
1348
+ }
1349
+ function renderBlockedTask(info) {
1350
+ const lines = [];
1351
+ const { task, blockers, reason, daysBlocked, isStale } = info;
1352
+ const staleIndicator = isStale ? c.red("[!]") : c.yellow("[B]");
1353
+ const taskId = c.gray(task.id.slice(-6));
1354
+ const priority = formatPriorityBadge(task.priority);
1355
+ const title = isStale ? c.red(task.title) : task.title;
1356
+ lines.push(` ${staleIndicator} ${taskId} ${title} ${priority}`);
1357
+ if (blockers.length > 0) {
1358
+ const blockerIds = blockers.map((b) => c.cyan(b.id.slice(-6))).join(", ");
1359
+ lines.push(` ${c.gray("blocked_by:")} ${blockerIds}`);
1360
+ }
1361
+ lines.push(` ${c.dim("->")} ${c.dim("Reason:")} ${c.dim(reason)}`);
1362
+ if (isStale) {
1363
+ lines.push(` ${c.dim("->")} ${c.red(`Blocked for ${daysBlocked} days`)}`);
1364
+ }
1365
+ return lines;
1366
+ }
1367
+ function renderStaleWarnings(staleItems) {
1368
+ if (staleItems.length === 0)
1369
+ return [];
1370
+ const message = `${staleItems.length} task${staleItems.length > 1 ? "s" : ""} blocked for 3+ days`;
1371
+ return [
1372
+ "",
1373
+ ...renderAlert(message, { severity: "warning" })
1374
+ ];
1375
+ }
1376
+ function renderSummary(totalBlocked, staleCount, byPriority) {
1377
+ const stats = [
1378
+ { label: "Total Blocked", value: totalBlocked, color: "yellow" },
1379
+ { label: "Stale (3+ days)", value: staleCount, color: staleCount > 0 ? "red" : "gray" }
1380
+ ];
1381
+ if (totalBlocked > 0) {
1382
+ if (byPriority.critical > 0) {
1383
+ stats.push({ label: "Critical", value: byPriority.critical, color: "red" });
1384
+ }
1385
+ if (byPriority.high > 0) {
1386
+ stats.push({ label: "High", value: byPriority.high, color: "magenta" });
1387
+ }
1388
+ }
1389
+ return ["", ...renderStats(stats)];
1390
+ }
1391
+ async function blockedCmd(options = {}) {
1392
+ const tasks = await listTasks();
1393
+ const blockedInfo = analyzeBlockedTasks(tasks);
1394
+ const displayItems = options.stale ? blockedInfo.filter((info) => info.isStale) : blockedInfo;
1395
+ const staleItems = blockedInfo.filter((info) => info.isStale);
1396
+ const byPriority = { critical: 0, high: 0, medium: 0, low: 0 };
1397
+ for (const info of blockedInfo) {
1398
+ byPriority[info.task.priority]++;
1399
+ }
1400
+ const lines = [];
1401
+ const title = options.stale ? "Stale Blocked Tasks" : "Blocked Tasks";
1402
+ lines.push(c.yellow(c.bold(`[!] ${title}`)));
1403
+ lines.push(c.gray(hline(50)));
1404
+ if (displayItems.length === 0) {
1405
+ if (options.stale) {
1406
+ lines.push("");
1407
+ lines.push(c.green(" No stale blocked tasks found."));
1408
+ lines.push(c.gray(" All blocked tasks have been blocked for less than 3 days."));
1409
+ } else {
1410
+ lines.push("");
1411
+ lines.push(c.green(" No blocked tasks found."));
1412
+ lines.push(c.gray(" All tasks are ready to work or completed."));
1413
+ }
1414
+ console.log(lines.join(`
1415
+ `));
1416
+ return;
1417
+ }
1418
+ lines.push("");
1419
+ for (const info of displayItems) {
1420
+ lines.push(...renderBlockedTask(info));
1421
+ lines.push("");
1422
+ }
1423
+ if (!options.stale && staleItems.length > 0) {
1424
+ lines.push(...renderStaleWarnings(staleItems));
1425
+ }
1426
+ lines.push(...renderSummary(blockedInfo.length, staleItems.length, byPriority));
1427
+ if (!options.stale && staleItems.length > 0) {
1428
+ lines.push("");
1429
+ lines.push(c.gray(" Tip: Use --stale to show only stale blocked tasks"));
1430
+ }
1431
+ console.log(lines.join(`
1432
+ `));
1433
+ }
1434
+
1435
+ // src/commands/health.ts
1436
+ import {
1437
+ detectCircularDependencies,
1438
+ buildDependencyIndices
1439
+ } from "@task-mcp/shared";
1440
+ function getOverdueTasks2(tasks) {
1441
+ const today = new Date;
1442
+ today.setHours(0, 0, 0, 0);
1443
+ return tasks.filter((t) => {
1444
+ if (t.status === "completed" || t.status === "cancelled")
1445
+ return false;
1446
+ if (!t.dueDate)
1447
+ return false;
1448
+ const due = new Date(t.dueDate);
1449
+ due.setHours(0, 0, 0, 0);
1450
+ return due < today;
1451
+ });
1452
+ }
1453
+ function getStaleBlockedTasks2(tasks, staleDays = 3) {
1454
+ const cutoff = Date.now() - staleDays * 24 * 60 * 60 * 1000;
1455
+ return tasks.filter((t) => {
1456
+ if (t.status !== "blocked")
1457
+ return false;
1458
+ const updated = new Date(t.updatedAt).getTime();
1459
+ return updated < cutoff;
1460
+ });
1461
+ }
1462
+ function findBottlenecks(tasks, limit = 5) {
1463
+ const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
1464
+ const { dependentsIndex } = buildDependencyIndices(activeTasks);
1465
+ const bottlenecks = [];
1466
+ for (const task of activeTasks) {
1467
+ const dependents = dependentsIndex.get(task.id) ?? [];
1468
+ if (dependents.length > 0) {
1469
+ bottlenecks.push({
1470
+ task,
1471
+ blockingCount: dependents.length
1472
+ });
1473
+ }
1474
+ }
1475
+ bottlenecks.sort((a, b) => b.blockingCount - a.blockingCount);
1476
+ return bottlenecks.slice(0, limit);
1477
+ }
1478
+ function generateRecommendations(tasks, stats, metrics, bottlenecks) {
1479
+ const recommendations = [];
1480
+ const overdueMetric = metrics.find((m) => m.name === "Overdue tasks");
1481
+ if (overdueMetric?.status === "fail") {
1482
+ recommendations.push("Address overdue tasks immediately to prevent further delays");
1483
+ }
1484
+ const staleMetric = metrics.find((m) => m.name === "Stale blocked");
1485
+ if (staleMetric?.status !== "pass") {
1486
+ recommendations.push("Review stale blocked tasks - they may need dependency updates or reassignment");
1487
+ }
1488
+ if (bottlenecks.length > 0 && bottlenecks[0].blockingCount >= 3) {
1489
+ recommendations.push(`Focus on "${bottlenecks[0].task.title}" - it blocks ${bottlenecks[0].blockingCount} other tasks`);
1490
+ }
1491
+ if (stats.completionPercent < 30 && stats.total > 5) {
1492
+ recommendations.push("Completion rate is low. Consider breaking down large tasks or adjusting priorities");
1493
+ }
1494
+ if (stats.blocked > stats.total * 0.3 && stats.total > 3) {
1495
+ recommendations.push("Many tasks are blocked. Review dependency structure for optimization");
1496
+ }
1497
+ if (stats.inProgress === 0 && stats.pending > 0) {
1498
+ recommendations.push("No tasks in progress. Pick up the next available task to maintain momentum");
1499
+ }
1500
+ if (recommendations.length === 0) {
1501
+ recommendations.push("Workspace is in good health. Keep up the good work!");
1502
+ }
1503
+ return recommendations;
1504
+ }
1505
+ function calculateHealthScore(stats, cycles, overdue, staleBlocked) {
1506
+ let score = 100;
1507
+ if (cycles.length > 0) {
1508
+ score -= 30;
1509
+ }
1510
+ const overdueRatio = stats.total > 0 ? overdue.length / stats.total : 0;
1511
+ score -= Math.min(25, overdueRatio * 100);
1512
+ const staleRatio = stats.total > 0 ? staleBlocked.length / stats.total : 0;
1513
+ score -= Math.min(15, staleRatio * 50);
1514
+ const blockedRatio = stats.total > 0 ? stats.blocked / stats.total : 0;
1515
+ score -= Math.min(15, blockedRatio * 30);
1516
+ if (stats.completionPercent >= 50) {
1517
+ score += 5;
1518
+ }
1519
+ return Math.max(0, Math.min(100, Math.round(score)));
1520
+ }
1521
+ function analyzeHealth(tasks) {
1522
+ const stats = calculateStats(tasks);
1523
+ const depMetrics = calculateDependencyMetrics(tasks);
1524
+ const cycles = detectCircularDependencies(tasks);
1525
+ const overdue = getOverdueTasks2(tasks);
1526
+ const staleBlocked = getStaleBlockedTasks2(tasks);
1527
+ const bottlenecks = findBottlenecks(tasks);
1528
+ const metrics = [
1529
+ {
1530
+ name: "Completion rate",
1531
+ status: stats.completionPercent >= 50 ? "pass" : stats.completionPercent >= 20 ? "warn" : "fail",
1532
+ value: `${stats.completionPercent}%`,
1533
+ detail: `${stats.completed}/${stats.total - stats.cancelled} tasks`
1534
+ },
1535
+ {
1536
+ name: "No cycles",
1537
+ status: cycles.length === 0 ? "pass" : "fail",
1538
+ value: cycles.length === 0 ? "Clean" : `${cycles.length} found`,
1539
+ detail: cycles.length > 0 ? `Involves: ${cycles[0].join(" -> ")}` : undefined
1540
+ },
1541
+ {
1542
+ name: "Blocked tasks",
1543
+ status: stats.blocked === 0 ? "pass" : stats.blocked <= 2 ? "warn" : "fail",
1544
+ value: `${stats.blocked} tasks`,
1545
+ detail: depMetrics.blockedByDependencies > 0 ? `${depMetrics.blockedByDependencies} waiting on dependencies` : undefined
1546
+ },
1547
+ {
1548
+ name: "Overdue tasks",
1549
+ status: overdue.length === 0 ? "pass" : "fail",
1550
+ value: overdue.length === 0 ? "None" : `${overdue.length} tasks`,
1551
+ detail: overdue.length > 0 ? overdue.map((t) => t.title).slice(0, 2).join(", ") : undefined
1552
+ },
1553
+ {
1554
+ name: "Stale blocked",
1555
+ status: staleBlocked.length === 0 ? "pass" : "warn",
1556
+ value: staleBlocked.length === 0 ? "None" : `${staleBlocked.length} tasks (>3 days)`,
1557
+ detail: staleBlocked.length > 0 ? staleBlocked.map((t) => t.title).slice(0, 2).join(", ") : undefined
1558
+ }
1559
+ ];
1560
+ const score = calculateHealthScore(stats, cycles, overdue, staleBlocked);
1561
+ const recommendations = generateRecommendations(tasks, stats, metrics, bottlenecks);
1562
+ return {
1563
+ score,
1564
+ metrics,
1565
+ bottlenecks,
1566
+ recommendations
1567
+ };
1568
+ }
1569
+ function getMetricSymbol(status) {
1570
+ switch (status) {
1571
+ case "pass":
1572
+ return c.green("[/]");
1573
+ case "warn":
1574
+ return c.yellow("[!]");
1575
+ case "fail":
1576
+ return c.red("[x]");
1577
+ }
1578
+ }
1579
+ function renderHealthScore(score, workspace) {
1580
+ const lines = [];
1581
+ let scoreColor;
1582
+ let label;
1583
+ if (score >= 80) {
1584
+ scoreColor = c.green;
1585
+ label = "Excellent";
1586
+ } else if (score >= 60) {
1587
+ scoreColor = c.cyan;
1588
+ label = "Good";
1589
+ } else if (score >= 40) {
1590
+ scoreColor = c.yellow;
1591
+ label = "Fair";
1592
+ } else {
1593
+ scoreColor = c.red;
1594
+ label = "Needs Attention";
1595
+ }
1596
+ lines.push(c.cyan(c.bold("Workspace Health")));
1597
+ lines.push(c.gray(hline(50)));
1598
+ lines.push("");
1599
+ lines.push(` ${c.gray("Workspace:")} ${c.bold(workspace)}`);
1600
+ lines.push("");
1601
+ lines.push(` ${c.gray("Score:")} ${scoreColor(c.bold(`${score}`))}${c.gray("/100")} ${c.dim(`(${label})`)}`);
1602
+ lines.push("");
1603
+ lines.push(` ${progressBar(score, 100, { width: 40, showPercent: false })}`);
1604
+ return lines;
1605
+ }
1606
+ function renderMetrics(metrics) {
1607
+ const lines = [];
1608
+ lines.push("");
1609
+ lines.push(c.cyan(c.bold(" Health Metrics")));
1610
+ lines.push("");
1611
+ for (const metric of metrics) {
1612
+ const symbol = getMetricSymbol(metric.status);
1613
+ const value = metric.status === "pass" ? c.green(metric.value) : metric.status === "warn" ? c.yellow(metric.value) : c.red(metric.value);
1614
+ lines.push(` ${symbol} ${metric.name}: ${value}`);
1615
+ if (metric.detail) {
1616
+ lines.push(` ${c.dim(metric.detail)}`);
1617
+ }
1618
+ }
1619
+ return lines;
1620
+ }
1621
+ function renderBottlenecks(bottlenecks) {
1622
+ if (bottlenecks.length === 0) {
1623
+ return [];
1624
+ }
1625
+ const items = bottlenecks.map((b) => ({
1626
+ primary: `${c.gray(b.task.id.slice(-4))} ${b.task.title}`,
1627
+ secondary: c.yellow(`blocks ${b.blockingCount} task${b.blockingCount > 1 ? "s" : ""}`),
1628
+ prefix: formatStatusSymbol(b.task.status)
1629
+ }));
1630
+ return [
1631
+ "",
1632
+ ...renderSection("Bottlenecks", items, { icon: "" })
1633
+ ];
1634
+ }
1635
+ function renderRecommendations(recommendations) {
1636
+ const lines = [];
1637
+ lines.push("");
1638
+ lines.push(c.cyan(c.bold(" Recommendations")));
1639
+ lines.push("");
1640
+ for (const rec of recommendations) {
1641
+ lines.push(` ${c.dim("-")} ${rec}`);
1642
+ }
1643
+ return lines;
1644
+ }
1645
+ function renderQuickSummary(score, workspace) {
1646
+ let scoreColor;
1647
+ let label;
1648
+ if (score >= 80) {
1649
+ scoreColor = c.green;
1650
+ label = "Excellent";
1651
+ } else if (score >= 60) {
1652
+ scoreColor = c.cyan;
1653
+ label = "Good";
1654
+ } else if (score >= 40) {
1655
+ scoreColor = c.yellow;
1656
+ label = "Fair";
1657
+ } else {
1658
+ scoreColor = c.red;
1659
+ label = "Needs Attention";
1660
+ }
1661
+ return [
1662
+ `${c.bold(workspace)} health: ${scoreColor(c.bold(`${score}`))}${c.gray("/100")} ${c.dim(`(${label})`)}`,
1663
+ `${progressBar(score, 100, { width: 30, showPercent: false })}`
1664
+ ];
1665
+ }
1666
+ async function healthCmd(options = {}) {
1667
+ const tasks = await listTasks();
1668
+ const workspace = getCurrentWorkspace();
1669
+ if (tasks.length === 0) {
1670
+ console.log(c.yellow("No tasks found in workspace."));
1671
+ console.log(c.gray(`Workspace: ${workspace}`));
1672
+ return;
1673
+ }
1674
+ const analysis = analyzeHealth(tasks);
1675
+ if (options.quick) {
1676
+ const lines2 = renderQuickSummary(analysis.score, workspace);
1677
+ console.log(lines2.join(`
1678
+ `));
1679
+ return;
1680
+ }
1681
+ const lines = [];
1682
+ lines.push(...renderHealthScore(analysis.score, workspace));
1683
+ lines.push(...renderMetrics(analysis.metrics));
1684
+ lines.push(...renderBottlenecks(analysis.bottlenecks));
1685
+ lines.push(...renderRecommendations(analysis.recommendations));
1686
+ lines.push("");
1687
+ console.log(lines.join(`
1688
+ `));
1689
+ }
588
1690
  // package.json
589
1691
  var package_default = {
590
1692
  name: "@task-mcp/cli",
591
- version: "1.0.28",
1693
+ version: "1.0.29",
592
1694
  description: "Zero-dependency CLI for task-mcp with Bun native visualization",
593
1695
  type: "module",
594
1696
  bin: {
@@ -658,7 +1760,7 @@ function parseArgs(argv) {
658
1760
  }
659
1761
  }
660
1762
  return {
661
- command: positional[0] ?? "dashboard",
1763
+ command: positional[0] ?? "",
662
1764
  args: positional.slice(1),
663
1765
  flags
664
1766
  };
@@ -672,7 +1774,16 @@ ${c.yellow("USAGE")}
672
1774
  ${c.bold("task")} [command] [options]
673
1775
 
674
1776
  ${c.yellow("COMMANDS")}
675
- ${c.cyan("dashboard")} [workspace] Show workspace dashboard (default)
1777
+ ${c.cyan("(no command)")} Show smart dashboard (default)
1778
+ ${c.cyan("next")} Show suggested next task with details
1779
+ ${c.cyan("n")} Alias for next
1780
+ ${c.cyan("blocked")} Show blocked tasks and their causes
1781
+ ${c.cyan("b")} Alias for blocked
1782
+ ${c.cyan("tree")} Show task hierarchy as ASCII tree
1783
+ ${c.cyan("t")} Alias for tree
1784
+ ${c.cyan("health")} Show workspace health analysis
1785
+ ${c.cyan("h")} Alias for health
1786
+ ${c.cyan("dashboard")} [workspace] Show detailed workspace dashboard
676
1787
  ${c.cyan("d")} [workspace] Alias for dashboard
677
1788
  ${c.cyan("list")} List all tasks
678
1789
  ${c.cyan("ls")} Alias for list
@@ -695,8 +1806,11 @@ ${c.yellow("INBOX SUBCOMMANDS")}
695
1806
 
696
1807
  ${c.yellow("OPTIONS")}
697
1808
  ${c.cyan("-i, --interactive")} Start interactive mode
1809
+ ${c.cyan("--strategy")} <strategy> Next task strategy (balanced,quick_wins,unblock,critical_path)
698
1810
  ${c.cyan("--status")} <status> Filter tasks by status (pending,in_progress,blocked,completed,cancelled)
699
1811
  ${c.cyan("--priority")} <priority> Filter tasks by priority (critical,high,medium,low)
1812
+ ${c.cyan("--stale")} Show only stale blocked tasks (3+ days)
1813
+ ${c.cyan("--quick")} Quick health score only
700
1814
  ${c.cyan("--all")} Include completed/cancelled tasks
701
1815
 
702
1816
  ${c.yellow("EXAMPLES")}
@@ -786,10 +1900,37 @@ async function main() {
786
1900
  }
787
1901
  try {
788
1902
  switch (command) {
1903
+ case "":
1904
+ await smartDashboard();
1905
+ break;
789
1906
  case "dashboard":
790
1907
  case "d":
791
1908
  await dashboard(args[0]);
792
1909
  break;
1910
+ case "next":
1911
+ case "n":
1912
+ await nextCmd({
1913
+ strategy: flags.strategy
1914
+ });
1915
+ break;
1916
+ case "blocked":
1917
+ case "b":
1918
+ await blockedCmd({
1919
+ stale: flags.stale === true
1920
+ });
1921
+ break;
1922
+ case "tree":
1923
+ case "t":
1924
+ await treeCmd({
1925
+ all: flags.all === true
1926
+ });
1927
+ break;
1928
+ case "health":
1929
+ case "h":
1930
+ await healthCmd({
1931
+ quick: flags.quick === true
1932
+ });
1933
+ break;
793
1934
  case "list":
794
1935
  case "ls":
795
1936
  await listTasksCmd({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@task-mcp/cli",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "Zero-dependency CLI for task-mcp with Bun native visualization",
5
5
  "type": "module",
6
6
  "bin": {