@task-mcp/cli 1.0.15 → 1.0.17

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.
@@ -7,6 +7,11 @@ import { c, banner } from "./ansi.js";
7
7
  import { dashboard } from "./commands/dashboard.js";
8
8
  import { listTasksCmd, listProjectsCmd } from "./commands/list.js";
9
9
  import * as readline from "node:readline";
10
+ import {
11
+ MENU_SEPARATOR_WIDTH,
12
+ ERROR_MESSAGE_DELAY_MS,
13
+ WELCOME_SCREEN_DELAY_MS,
14
+ } from "./constants.js";
10
15
 
11
16
  interface MenuOption {
12
17
  key: string;
@@ -40,7 +45,7 @@ async function showMenu(title: string, options: MenuOption[]): Promise<void> {
40
45
  console.log(banner("TASK MCP"));
41
46
  console.log();
42
47
  console.log(c.bold(c.cyan(title)));
43
- console.log(c.dim("─".repeat(40)));
48
+ console.log(c.dim("─".repeat(MENU_SEPARATOR_WIDTH)));
44
49
  console.log();
45
50
 
46
51
  for (const opt of options) {
@@ -56,7 +61,7 @@ async function showMenu(title: string, options: MenuOption[]): Promise<void> {
56
61
  await selected.action();
57
62
  } else if (choice !== "") {
58
63
  console.log(c.error(`Invalid option: ${choice}`));
59
- await sleep(1000);
64
+ await sleep(ERROR_MESSAGE_DELAY_MS);
60
65
  }
61
66
  }
62
67
 
@@ -103,22 +108,6 @@ async function mainMenu(): Promise<boolean> {
103
108
  await tasksMenu();
104
109
  },
105
110
  },
106
- {
107
- key: "a",
108
- label: "Analysis",
109
- description: "View analysis tools",
110
- action: async () => {
111
- await analysisMenu();
112
- },
113
- },
114
- {
115
- key: "q",
116
- label: "Quick Actions",
117
- description: "Common quick actions",
118
- action: async () => {
119
- await quickActionsMenu();
120
- },
121
- },
122
111
  {
123
112
  key: "x",
124
113
  label: "Exit",
@@ -140,7 +129,7 @@ async function mainMenu(): Promise<boolean> {
140
129
  async function dashboardMenu(): Promise<void> {
141
130
  console.clear();
142
131
  console.log(c.bold(c.cyan("Dashboard")));
143
- console.log(c.dim("─".repeat(40)));
132
+ console.log(c.dim("─".repeat(MENU_SEPARATOR_WIDTH)));
144
133
  console.log();
145
134
 
146
135
  const projectId = await prompt(c.cyan("Project ID (Enter for all): "));
@@ -247,131 +236,6 @@ async function tasksMenu(): Promise<void> {
247
236
  await showMenu("Tasks", options);
248
237
  }
249
238
 
250
- /**
251
- * Analysis Menu
252
- */
253
- async function analysisMenu(): Promise<void> {
254
- const options: MenuOption[] = [
255
- {
256
- key: "c",
257
- label: "Complexity Analysis",
258
- description: "View task complexity summary",
259
- action: async () => {
260
- console.clear();
261
- console.log(c.yellow("Complexity Analysis"));
262
- console.log(c.dim("Use MCP tools: get_complexity_summary, save_complexity_analysis"));
263
- console.log();
264
- console.log("Features:");
265
- console.log(" - Score 1-10 complexity rating");
266
- console.log(" - Factors: state_management, cross_cutting, etc.");
267
- console.log(" - Suggested subtask count");
268
- console.log();
269
- await waitForKey();
270
- },
271
- },
272
- {
273
- key: "t",
274
- label: "Tech Stack Analysis",
275
- description: "View tech areas and risk",
276
- action: async () => {
277
- console.clear();
278
- console.log(c.yellow("Tech Stack Analysis"));
279
- console.log(c.dim("Use MCP tools: get_tech_stack_summary, save_tech_stack_analysis"));
280
- console.log();
281
- console.log("Features:");
282
- console.log(" - Areas: schema, backend, frontend, infra, devops, test, docs");
283
- console.log(" - Risk levels: low, medium, high, critical");
284
- console.log(" - Breaking change detection");
285
- console.log();
286
- await waitForKey();
287
- },
288
- },
289
- {
290
- key: "r",
291
- label: "Risk Analysis",
292
- description: "Find high-risk tasks",
293
- action: async () => {
294
- console.clear();
295
- console.log(c.yellow("Risk Analysis"));
296
- console.log(c.dim("Use MCP tools: find_high_risk_tasks, suggest_safe_order"));
297
- console.log();
298
- console.log("Features:");
299
- console.log(" - Identify high/critical risk tasks");
300
- console.log(" - Suggest safe execution order");
301
- console.log(" - Phase-based task grouping");
302
- console.log();
303
- await waitForKey();
304
- },
305
- },
306
- {
307
- key: "b",
308
- label: "Back to Main Menu",
309
- action: () => {},
310
- },
311
- ];
312
-
313
- await showMenu("Analysis Tools", options);
314
- }
315
-
316
- /**
317
- * Quick Actions Menu
318
- */
319
- async function quickActionsMenu(): Promise<void> {
320
- const options: MenuOption[] = [
321
- {
322
- key: "t",
323
- label: "Today's Tasks",
324
- action: async () => {
325
- console.clear();
326
- console.log(c.bold(c.yellow("Today's Tasks")));
327
- console.log(c.dim("Use MCP tool: view_today"));
328
- console.log();
329
- await waitForKey();
330
- },
331
- },
332
- {
333
- key: "w",
334
- label: "This Week",
335
- action: async () => {
336
- console.clear();
337
- console.log(c.bold(c.yellow("This Week's Tasks")));
338
- console.log(c.dim("Use MCP tool: view_this_week"));
339
- console.log();
340
- await waitForKey();
341
- },
342
- },
343
- {
344
- key: "q",
345
- label: "Quick Wins",
346
- action: async () => {
347
- console.clear();
348
- console.log(c.bold(c.yellow("Quick Wins")));
349
- console.log(c.dim("Use MCP tool: view_quick_wins"));
350
- console.log();
351
- await waitForKey();
352
- },
353
- },
354
- {
355
- key: "n",
356
- label: "Next Task Suggestion",
357
- action: async () => {
358
- console.clear();
359
- console.log(c.bold(c.yellow("Next Task Suggestion")));
360
- console.log(c.dim("Use MCP tool: suggest_next_task"));
361
- console.log();
362
- await waitForKey();
363
- },
364
- },
365
- {
366
- key: "b",
367
- label: "Back to Main Menu",
368
- action: () => {},
369
- },
370
- ];
371
-
372
- await showMenu("Quick Actions", options);
373
- }
374
-
375
239
  /**
376
240
  * Start interactive mode
377
241
  */
@@ -382,7 +246,7 @@ export async function startInteractive(): Promise<void> {
382
246
  console.log(c.bold("Welcome to Task MCP Interactive Mode"));
383
247
  console.log(c.dim("Navigate using keyboard shortcuts"));
384
248
  console.log();
385
- await sleep(1000);
249
+ await sleep(WELCOME_SCREEN_DELAY_MS);
386
250
 
387
251
  while (true) {
388
252
  try {
package/src/storage.ts CHANGED
@@ -1,183 +1,68 @@
1
1
  /**
2
- * Simple file-based storage reader for CLI
3
- * Reads directly from .tasks/ directory
2
+ * CLI Storage utilities
3
+ * Wrapper functions for store access and pure functions for task statistics
4
4
  */
5
5
 
6
- import { readdir, readFile } from "node:fs/promises";
7
- import { join } from "node:path";
6
+ import type { Task, Project, InboxItem } from "@task-mcp/shared";
7
+ import {
8
+ TaskStore,
9
+ ProjectStore,
10
+ InboxStore,
11
+ StateStore,
12
+ } from "@task-mcp/mcp-server/storage";
8
13
 
9
- export interface ComplexityAnalysis {
10
- score?: number;
11
- factors?: string[];
12
- rationale?: string;
13
- }
14
+ // Re-export types from shared for consumers of this module
15
+ export type { Task, Project, InboxItem };
14
16
 
15
- export interface TechStackAnalysis {
16
- areas?: string[];
17
- riskLevel?: "low" | "medium" | "high" | "critical";
18
- hasBreakingChange?: boolean;
19
- affectedComponents?: string[];
20
- }
17
+ // Initialize stores
18
+ const taskStore = new TaskStore();
19
+ const projectStore = new ProjectStore();
20
+ const inboxStore = new InboxStore();
21
+ const stateStore = new StateStore();
21
22
 
22
- export interface Task {
23
- id: string;
24
- title: string;
25
- description?: string;
26
- status: "pending" | "in_progress" | "blocked" | "completed" | "cancelled";
27
- priority: "critical" | "high" | "medium" | "low";
28
- projectId: string;
29
- parentId?: string;
30
- dependencies?: { taskId: string; type: "blocks" | "blocked_by" | "related"; reason?: string }[];
31
- dueDate?: string;
32
- createdAt: string;
33
- updatedAt: string;
34
- completedAt?: string;
35
- contexts?: string[];
36
- tags?: string[];
37
- complexity?: ComplexityAnalysis;
38
- techStack?: TechStackAnalysis;
39
- }
40
-
41
- export interface Project {
42
- id: string;
43
- name: string;
44
- description?: string;
45
- status: "active" | "on_hold" | "completed" | "archived";
46
- defaultPriority?: "critical" | "high" | "medium" | "low";
47
- createdAt: string;
48
- updatedAt: string;
49
- targetDate?: string;
50
- }
51
-
52
- export interface TasksFile {
53
- version: number;
54
- tasks: Task[];
55
- }
56
-
57
- export interface InboxItem {
58
- id: string;
59
- content: string;
60
- status: "pending" | "promoted" | "discarded";
61
- source?: string;
62
- tags?: string[];
63
- capturedAt: string;
64
- promotedToTaskId?: string;
65
- }
66
-
67
- export interface InboxFile {
68
- version: number;
69
- items: InboxItem[];
70
- }
71
-
72
- function getTasksDir(): string {
73
- return join(process.cwd(), ".tasks");
74
- }
75
-
76
- /**
77
- * Validate a project ID format to prevent path traversal attacks.
78
- * Valid format: proj_[alphanumeric only]
79
- */
80
- function isValidProjectId(id: string): boolean {
81
- return /^proj_[a-z0-9]+$/.test(id);
82
- }
83
-
84
- async function readJson<T>(path: string): Promise<T | null> {
85
- try {
86
- const content = await readFile(path, "utf-8");
87
- return JSON.parse(content) as T;
88
- } catch {
89
- return null;
90
- }
91
- }
92
-
93
- async function listDirs(path: string): Promise<string[]> {
94
- try {
95
- const entries = await readdir(path, { withFileTypes: true });
96
- return entries.filter(e => e.isDirectory()).map(e => e.name);
97
- } catch {
98
- return [];
99
- }
100
- }
23
+ // =============================================================================
24
+ // Store Wrapper Functions
25
+ // =============================================================================
101
26
 
102
27
  /**
103
28
  * List all projects
104
- * Uses Promise.allSettled for parallel I/O with graceful error handling
105
29
  */
106
30
  export async function listProjects(includeArchived = false): Promise<Project[]> {
107
- const projectsDir = join(getTasksDir(), "projects");
108
- const projectIds = await listDirs(projectsDir);
109
-
110
- // Use allSettled to handle individual project read failures gracefully
111
- const results = await Promise.allSettled(
112
- projectIds.map((id) => readJson<Project>(join(projectsDir, id, "project.json")))
113
- );
114
-
115
- const projects = results
116
- .filter((r): r is PromiseFulfilledResult<Project | null> => r.status === "fulfilled")
117
- .map((r) => r.value)
118
- .filter(
119
- (project): project is Project =>
120
- project !== null && (includeArchived || project.status !== "archived")
121
- );
122
-
123
- return projects.sort(
124
- (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
125
- );
31
+ const projects = await projectStore.list();
32
+ if (includeArchived) {
33
+ return projects;
34
+ }
35
+ return projects.filter((p) => p.status !== "archived");
126
36
  }
127
37
 
128
-
129
38
  /**
130
- * List tasks for a project
39
+ * List tasks for a specific project
131
40
  */
132
41
  export async function listTasks(projectId: string): Promise<Task[]> {
133
- // Validate project ID to prevent path traversal
134
- if (!isValidProjectId(projectId)) {
135
- console.error(`Invalid project ID format: ${projectId}`);
136
- return [];
137
- }
138
- const path = join(getTasksDir(), "projects", projectId, "tasks.json");
139
- const data = await readJson<TasksFile>(path);
140
- return data?.tasks ?? [];
42
+ return taskStore.list(projectId);
141
43
  }
142
44
 
143
45
  /**
144
- * List inbox items
46
+ * List all tasks across all projects
145
47
  */
146
- export async function listInboxItems(status?: "pending" | "promoted" | "discarded"): Promise<InboxItem[]> {
147
- const path = join(getTasksDir(), "inbox.json");
148
- const data = await readJson<InboxFile>(path);
149
- const items = data?.items ?? [];
150
-
151
- if (status) {
152
- return items.filter(i => i.status === status);
153
- }
154
- return items;
48
+ export async function listAllTasks(): Promise<Task[]> {
49
+ return taskStore.listAll();
155
50
  }
156
51
 
157
52
  /**
158
- * Get pending inbox count
53
+ * List inbox items by status
159
54
  */
160
- export async function getInboxCount(): Promise<number> {
161
- const items = await listInboxItems("pending");
162
- return items.length;
55
+ export async function listInboxItems(
56
+ status?: "pending" | "promoted" | "discarded"
57
+ ): Promise<InboxItem[]> {
58
+ return inboxStore.list({ status });
163
59
  }
164
60
 
165
61
  /**
166
- * List all tasks across all projects
167
- * Uses Promise.all for parallel I/O instead of sequential reads
62
+ * Get the active tag (git branch context)
168
63
  */
169
- export async function listAllTasks(): Promise<Task[]> {
170
- const projects = await listProjects(true);
171
-
172
- // Parallel reads instead of sequential for better performance
173
- const taskArrays = await Promise.all(
174
- projects.map((project) => listTasks(project.id))
175
- );
176
- const allTasks = taskArrays.flat();
177
-
178
- return allTasks.sort(
179
- (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
180
- );
64
+ export async function getActiveTag(): Promise<string> {
65
+ return stateStore.getActiveTag();
181
66
  }
182
67
 
183
68
  /**
@@ -320,275 +205,3 @@ export function suggestNextTask(tasks: Task[]): Task | null {
320
205
 
321
206
  return actionable[0] ?? null;
322
207
  }
323
-
324
- /**
325
- * Check if date is today
326
- */
327
- function isToday(dateStr: string): boolean {
328
- const date = new Date(dateStr);
329
- const today = new Date();
330
- return date.toDateString() === today.toDateString();
331
- }
332
-
333
- /**
334
- * Check if date is overdue
335
- */
336
- function isOverdue(dateStr: string): boolean {
337
- const date = new Date(dateStr);
338
- const today = new Date();
339
- today.setHours(0, 0, 0, 0);
340
- return date < today;
341
- }
342
-
343
- /**
344
- * Check if date is within N days
345
- */
346
- function isWithinDays(dateStr: string, days: number): boolean {
347
- const date = new Date(dateStr);
348
- const future = new Date();
349
- future.setDate(future.getDate() + days);
350
- return date <= future;
351
- }
352
-
353
- /**
354
- * Get tasks due today or overdue
355
- */
356
- export function getTodayTasks(tasks: Task[]): Task[] {
357
- return tasks.filter(t =>
358
- t.status !== "completed" &&
359
- t.status !== "cancelled" &&
360
- t.dueDate &&
361
- (isToday(t.dueDate) || isOverdue(t.dueDate))
362
- );
363
- }
364
-
365
- /**
366
- * Get tasks due this week
367
- */
368
- export function getThisWeekTasks(tasks: Task[]): Task[] {
369
- return tasks.filter(t =>
370
- t.status !== "completed" &&
371
- t.status !== "cancelled" &&
372
- t.dueDate &&
373
- isWithinDays(t.dueDate, 7)
374
- );
375
- }
376
-
377
- /**
378
- * Get overdue tasks
379
- */
380
- export function getOverdueTasks(tasks: Task[]): Task[] {
381
- return tasks.filter(t =>
382
- t.status !== "completed" &&
383
- t.status !== "cancelled" &&
384
- t.dueDate &&
385
- isOverdue(t.dueDate)
386
- );
387
- }
388
-
389
- /**
390
- * Task tree node for hierarchy
391
- */
392
- export interface TaskTreeNode {
393
- task: Task;
394
- children: TaskTreeNode[];
395
- }
396
-
397
- /**
398
- * Build task hierarchy tree
399
- */
400
- export function buildTaskTree(tasks: Task[]): TaskTreeNode[] {
401
- const taskMap = new Map(tasks.map(t => [t.id, t]));
402
- const childrenMap = new Map<string, Task[]>();
403
-
404
- // Build parent -> children map
405
- for (const task of tasks) {
406
- if (task.parentId) {
407
- const children = childrenMap.get(task.parentId) ?? [];
408
- children.push(task);
409
- childrenMap.set(task.parentId, children);
410
- }
411
- }
412
-
413
- function buildNode(task: Task): TaskTreeNode {
414
- const children = childrenMap.get(task.id) ?? [];
415
- return {
416
- task,
417
- children: children.map(buildNode),
418
- };
419
- }
420
-
421
- // Return all root tasks (no parent)
422
- const rootTasks = tasks.filter(t => !t.parentId);
423
- return rootTasks.map(buildNode);
424
- }
425
-
426
- /**
427
- * Count total nodes in tree
428
- */
429
- export function countTreeNodes(nodes: TaskTreeNode[]): number {
430
- return nodes.reduce((sum, n) => sum + 1 + countTreeNodes(n.children), 0);
431
- }
432
-
433
- /**
434
- * Simple Critical Path calculation
435
- * Returns tasks that are on the critical path based on dependencies
436
- */
437
- export interface CriticalPathResult {
438
- criticalPath: Task[];
439
- totalDuration: number;
440
- bottlenecks: { task: Task; blocksCount: number }[];
441
- }
442
-
443
- /**
444
- * Calculate complexity and tech stack statistics
445
- */
446
- export interface AnalysisStats {
447
- complexity: {
448
- analyzed: number;
449
- avgScore: number;
450
- distribution: { low: number; medium: number; high: number };
451
- topFactors: { factor: string; count: number }[];
452
- };
453
- techStack: {
454
- analyzed: number;
455
- byArea: Record<string, number>;
456
- byRisk: { low: number; medium: number; high: number; critical: number };
457
- breakingChanges: number;
458
- };
459
- }
460
-
461
- export function calculateAnalysisStats(tasks: Task[]): AnalysisStats {
462
- const activeTasks = tasks.filter(t =>
463
- t.status !== "completed" && t.status !== "cancelled"
464
- );
465
-
466
- // Complexity stats
467
- const tasksWithComplexity = activeTasks.filter(t => t.complexity?.score);
468
- const scores = tasksWithComplexity.map(t => t.complexity!.score!);
469
- const avgScore = scores.length > 0
470
- ? Math.round((scores.reduce((a, b) => a + b, 0) / scores.length) * 10) / 10
471
- : 0;
472
-
473
- const complexityDist = { low: 0, medium: 0, high: 0 };
474
- for (const score of scores) {
475
- if (score <= 3) complexityDist.low++;
476
- else if (score <= 6) complexityDist.medium++;
477
- else complexityDist.high++;
478
- }
479
-
480
- // Count factors
481
- const factorCounts: Record<string, number> = {};
482
- for (const t of tasksWithComplexity) {
483
- for (const f of t.complexity?.factors ?? []) {
484
- factorCounts[f] = (factorCounts[f] ?? 0) + 1;
485
- }
486
- }
487
- const topFactors = Object.entries(factorCounts)
488
- .map(([factor, count]) => ({ factor, count }))
489
- .sort((a, b) => b.count - a.count)
490
- .slice(0, 5);
491
-
492
- // Tech stack stats
493
- const tasksWithTech = activeTasks.filter(t => t.techStack?.areas?.length);
494
- const byArea: Record<string, number> = {};
495
- for (const t of tasksWithTech) {
496
- for (const area of t.techStack?.areas ?? []) {
497
- byArea[area] = (byArea[area] ?? 0) + 1;
498
- }
499
- }
500
-
501
- const byRisk = { low: 0, medium: 0, high: 0, critical: 0 };
502
- for (const t of tasksWithTech) {
503
- const risk = t.techStack?.riskLevel ?? "medium";
504
- byRisk[risk]++;
505
- }
506
-
507
- const breakingChanges = tasksWithTech.filter(t => t.techStack?.hasBreakingChange).length;
508
-
509
- return {
510
- complexity: {
511
- analyzed: tasksWithComplexity.length,
512
- avgScore,
513
- distribution: complexityDist,
514
- topFactors,
515
- },
516
- techStack: {
517
- analyzed: tasksWithTech.length,
518
- byArea,
519
- byRisk,
520
- breakingChanges,
521
- },
522
- };
523
- }
524
-
525
- export function calculateCriticalPath(tasks: Task[]): CriticalPathResult {
526
- const activeTasks = tasks.filter(t =>
527
- t.status !== "completed" && t.status !== "cancelled"
528
- );
529
-
530
- if (activeTasks.length === 0) {
531
- return { criticalPath: [], totalDuration: 0, bottlenecks: [] };
532
- }
533
-
534
- const taskMap = new Map(activeTasks.map(t => [t.id, t]));
535
-
536
- // Count how many tasks each task blocks
537
- const blocksCount: Record<string, number> = {};
538
- for (const task of activeTasks) {
539
- for (const dep of task.dependencies ?? []) {
540
- if (dep.type === "blocked_by") {
541
- blocksCount[dep.taskId] = (blocksCount[dep.taskId] ?? 0) + 1;
542
- }
543
- }
544
- }
545
-
546
- // Find bottlenecks (tasks that block the most other tasks)
547
- const bottlenecks = Object.entries(blocksCount)
548
- .map(([id, count]) => ({ task: taskMap.get(id)!, blocksCount: count }))
549
- .filter(b => b.task)
550
- .sort((a, b) => b.blocksCount - a.blocksCount)
551
- .slice(0, 5);
552
-
553
- // Simple critical path: find longest chain
554
- const getDepth = (taskId: string, visited = new Set<string>()): number => {
555
- if (visited.has(taskId)) return 0;
556
- visited.add(taskId);
557
-
558
- const task = taskMap.get(taskId);
559
- if (!task) return 0;
560
-
561
- const deps = task.dependencies?.filter(d => d.type === "blocked_by") ?? [];
562
- if (deps.length === 0) return 1;
563
-
564
- let maxDepth = 0;
565
- for (const dep of deps) {
566
- maxDepth = Math.max(maxDepth, getDepth(dep.taskId, new Set(visited)));
567
- }
568
- return maxDepth + 1;
569
- };
570
-
571
- // Calculate depth for each task and find critical path
572
- const tasksWithDepth = activeTasks.map(t => ({
573
- task: t,
574
- depth: getDepth(t.id),
575
- }));
576
-
577
- const maxDepth = Math.max(...tasksWithDepth.map(t => t.depth), 0);
578
-
579
- // Critical path = tasks with maximum depth
580
- const criticalPath = tasksWithDepth
581
- .filter(t => t.depth === maxDepth)
582
- .map(t => t.task)
583
- .sort((a, b) => {
584
- const aBlocks = blocksCount[a.id] ?? 0;
585
- const bBlocks = blocksCount[b.id] ?? 0;
586
- return bBlocks - aBlocks;
587
- });
588
-
589
- return {
590
- criticalPath,
591
- totalDuration: maxDepth * 30, // Assume 30 min per task
592
- bottlenecks,
593
- };
594
- }