@task-mcp/shared 1.0.22 → 1.0.23

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@task-mcp/shared",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "description": "Shared utilities for task-mcp: types, algorithms, and natural language parsing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,9 +1,5 @@
1
1
  import type { Task } from "../schemas/task.js";
2
- import {
3
- topologicalSort,
4
- findDependents,
5
- priorityToNumber,
6
- } from "./topological-sort.js";
2
+ import { topologicalSort, priorityToNumber } from "./topological-sort.js";
7
3
 
8
4
  /**
9
5
  * Task with computed CPM (Critical Path Method) values
@@ -43,9 +39,7 @@ function getTaskDuration(task: Task): number {
43
39
  * Get blocked_by dependencies for a task
44
40
  */
45
41
  function getBlockedByDeps(task: Task): string[] {
46
- return (task.dependencies ?? [])
47
- .filter((d) => d.type === "blocked_by")
48
- .map((d) => d.taskId);
42
+ return (task.dependencies ?? []).filter((d) => d.type === "blocked_by").map((d) => d.taskId);
49
43
  }
50
44
 
51
45
  /**
@@ -110,7 +104,10 @@ function calculateProjectDuration(taskMap: Map<string, CPMTask>): number {
110
104
  * This is O(n * d) where d is average dependencies, done once upfront
111
105
  * Allows O(1) successor lookup instead of O(n) per task
112
106
  */
113
- function buildSuccessorIndex(sortedTasks: Task[], taskMap: Map<string, CPMTask>): Map<string, CPMTask[]> {
107
+ function buildSuccessorIndex(
108
+ sortedTasks: Task[],
109
+ taskMap: Map<string, CPMTask>
110
+ ): Map<string, CPMTask[]> {
114
111
  const successorIndex = new Map<string, CPMTask[]>();
115
112
 
116
113
  // Initialize empty arrays for all tasks
@@ -244,9 +241,7 @@ function extractCriticalPath(sortedTasks: Task[], taskMap: Map<string, CPMTask>)
244
241
  * Find top bottlenecks (critical tasks blocking the most downstream work)
245
242
  */
246
243
  function findBottlenecks(criticalPath: CPMTask[], limit = 5): CPMTask[] {
247
- return [...criticalPath]
248
- .sort((a, b) => b.dependentCount - a.dependentCount)
249
- .slice(0, limit);
244
+ return [...criticalPath].sort((a, b) => b.dependentCount - a.dependentCount).slice(0, limit);
250
245
  }
251
246
 
252
247
  /**
@@ -262,9 +257,7 @@ function findBottlenecks(criticalPath: CPMTask[], limit = 5): CPMTask[] {
262
257
  */
263
258
  export function criticalPathAnalysis(tasks: Task[]): CPMResult {
264
259
  // Filter to only pending/in_progress tasks
265
- const activeTasks = tasks.filter(
266
- (t) => t.status === "pending" || t.status === "in_progress"
267
- );
260
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
268
261
 
269
262
  if (activeTasks.length === 0) {
270
263
  return {
@@ -318,16 +311,12 @@ export function criticalPathAnalysis(tasks: Task[]): CPMResult {
318
311
  * Optimized to O(n + e) where n = number of tasks, e = number of dependencies
319
312
  */
320
313
  export function findParallelTasks(tasks: Task[]): Task[][] {
321
- const activeTasks = tasks.filter(
322
- (t) => t.status === "pending" || t.status === "in_progress"
323
- );
314
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
324
315
 
325
316
  if (activeTasks.length === 0) return [];
326
317
 
327
318
  // Find tasks with no uncompleted dependencies
328
- const completedIds = new Set(
329
- tasks.filter((t) => t.status === "completed").map((t) => t.id)
330
- );
319
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
331
320
 
332
321
  const available = activeTasks.filter((task) => {
333
322
  const deps = getBlockedByDeps(task);
@@ -376,9 +365,7 @@ export function findParallelTasks(tasks: Task[]): Task[][] {
376
365
  if (processed.has(other.id)) continue;
377
366
 
378
367
  // O(1) check: tasks are independent if neither depends on the other
379
- const independent =
380
- !conflicting.has(other.id) &&
381
- !dependsOn.get(other.id)?.has(task.id);
368
+ const independent = !conflicting.has(other.id) && !dependsOn.get(other.id)?.has(task.id);
382
369
 
383
370
  if (independent) {
384
371
  group.push(other);
@@ -402,9 +389,7 @@ export function suggestNextTask(
402
389
  maxMinutes?: number;
403
390
  } = {}
404
391
  ): Task | null {
405
- const activeTasks = tasks.filter(
406
- (t) => t.status === "pending" || t.status === "in_progress"
407
- );
392
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
408
393
 
409
394
  if (activeTasks.length === 0) return null;
410
395
 
@@ -412,9 +397,7 @@ export function suggestNextTask(
412
397
  const cpm = criticalPathAnalysis(tasks);
413
398
 
414
399
  // Filter by availability (all dependencies completed)
415
- const completedIds = new Set(
416
- tasks.filter((t) => t.status === "completed").map((t) => t.id)
417
- );
400
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
418
401
 
419
402
  let candidates = cpm.tasks.filter((task) => {
420
403
  const deps = getBlockedByDeps(task);
@@ -434,9 +417,7 @@ export function suggestNextTask(
434
417
 
435
418
  // Filter by time if specified
436
419
  if (options.maxMinutes) {
437
- const timeFiltered = candidates.filter(
438
- (t) => getTaskDuration(t) <= options.maxMinutes!
439
- );
420
+ const timeFiltered = candidates.filter((t) => getTaskDuration(t) <= options.maxMinutes!);
440
421
  if (timeFiltered.length > 0) {
441
422
  candidates = timeFiltered;
442
423
  }
@@ -459,4 +440,3 @@ export function suggestNextTask(
459
440
  scored.sort((a, b) => b.score - a.score);
460
441
  return scored[0]?.task ?? null;
461
442
  }
462
-
@@ -35,9 +35,7 @@ export interface ValidateDependencyOptions {
35
35
  /**
36
36
  * Validates if a dependency can be added between two tasks
37
37
  */
38
- export function validateDependency(
39
- options: ValidateDependencyOptions
40
- ): DependencyValidationResult {
38
+ export function validateDependency(options: ValidateDependencyOptions): DependencyValidationResult {
41
39
  const { taskId, blockedBy, tasks, checkDuplicates = true } = options;
42
40
 
43
41
  // 1. Self-dependency check
@@ -117,9 +115,7 @@ export interface InvalidDependencyReference {
117
115
  /**
118
116
  * Find all invalid dependency references (orphaned references)
119
117
  */
120
- export function findInvalidDependencies(
121
- tasks: Task[]
122
- ): InvalidDependencyReference[] {
118
+ export function findInvalidDependencies(tasks: Task[]): InvalidDependencyReference[] {
123
119
  const taskIds = new Set(tasks.map((t) => t.id));
124
120
  const invalid: InvalidDependencyReference[] = [];
125
121
 
@@ -242,17 +238,12 @@ export interface DependencyIntegrityReport {
242
238
  /**
243
239
  * Check overall dependency integrity of a project
244
240
  */
245
- export function checkDependencyIntegrity(
246
- tasks: Task[]
247
- ): DependencyIntegrityReport {
241
+ export function checkDependencyIntegrity(tasks: Task[]): DependencyIntegrityReport {
248
242
  const selfDeps = findSelfDependencies(tasks);
249
243
  const invalidRefs = findInvalidDependencies(tasks);
250
244
  const cycles = detectCircularDependencies(tasks);
251
245
 
252
- const totalDependencies = tasks.reduce(
253
- (sum, t) => sum + (t.dependencies?.length ?? 0),
254
- 0
255
- );
246
+ const totalDependencies = tasks.reduce((sum, t) => sum + (t.dependencies?.length ?? 0), 0);
256
247
 
257
248
  const issueCount = selfDeps.length + invalidRefs.length + cycles.length;
258
249
  const valid = issueCount === 0;
@@ -5,14 +5,14 @@ import type { Task, TechArea, RiskLevel } from "../schemas/task.js";
5
5
  * Based on dependency flow: DB changes → Infrastructure → Backend → Frontend → Tests
6
6
  */
7
7
  const TECH_ORDER: Record<TechArea, number> = {
8
- schema: 0, // DB/schema changes first
9
- infra: 0, // Infrastructure setup
10
- devops: 1, // CI/CD pipelines
11
- backend: 2, // API/server
12
- frontend: 3, // UI
13
- test: 4, // Tests
14
- docs: 4, // Documentation
15
- refactor: 5, // Refactoring last
8
+ schema: 0, // DB/schema changes first
9
+ infra: 0, // Infrastructure setup
10
+ devops: 1, // CI/CD pipelines
11
+ backend: 2, // API/server
12
+ frontend: 3, // UI
13
+ test: 4, // Tests
14
+ docs: 4, // Documentation
15
+ refactor: 5, // Refactoring last
16
16
  };
17
17
 
18
18
  /**
@@ -143,9 +143,7 @@ function hasBreakingChange(task: Task): boolean {
143
143
  */
144
144
  export function suggestSafeOrder(tasks: Task[]): SafeOrderResult {
145
145
  // Filter to active tasks only
146
- const activeTasks = tasks.filter(
147
- (t) => t.status === "pending" || t.status === "in_progress"
148
- );
146
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
149
147
 
150
148
  if (activeTasks.length === 0) {
151
149
  return {
@@ -175,7 +173,10 @@ export function suggestSafeOrder(tasks: Task[]): SafeOrderResult {
175
173
  if (aBreaking !== bBreaking) return aBreaking - bBreaking;
176
174
 
177
175
  // 4. Priority as tiebreaker (higher priority first)
178
- return (PRIORITY_ORDER[a.priority] ?? DEFAULT_PRIORITY_ORDER) - (PRIORITY_ORDER[b.priority] ?? DEFAULT_PRIORITY_ORDER);
176
+ return (
177
+ (PRIORITY_ORDER[a.priority] ?? DEFAULT_PRIORITY_ORDER) -
178
+ (PRIORITY_ORDER[b.priority] ?? DEFAULT_PRIORITY_ORDER)
179
+ );
179
180
  });
180
181
 
181
182
  // Group into phases by tech level
@@ -258,9 +259,7 @@ function getPrimaryArea(task: Task): TechArea | "mixed" {
258
259
  */
259
260
  export function findBreakingChanges(tasks: Task[]): Task[] {
260
261
  return tasks.filter(
261
- (t) =>
262
- (t.status === "pending" || t.status === "in_progress") &&
263
- hasBreakingChange(t)
262
+ (t) => (t.status === "pending" || t.status === "in_progress") && hasBreakingChange(t)
264
263
  );
265
264
  }
266
265
 
@@ -284,16 +283,21 @@ export function groupByTechArea(tasks: Task[]): Map<TechArea, Task[]> {
284
283
 
285
284
  // Initialize all groups
286
285
  const allAreas: TechArea[] = [
287
- "schema", "infra", "devops", "backend", "frontend", "test", "docs", "refactor"
286
+ "schema",
287
+ "infra",
288
+ "devops",
289
+ "backend",
290
+ "frontend",
291
+ "test",
292
+ "docs",
293
+ "refactor",
288
294
  ];
289
295
  for (const area of allAreas) {
290
296
  groups.set(area, []);
291
297
  }
292
298
 
293
299
  // Group active tasks
294
- const activeTasks = tasks.filter(
295
- (t) => t.status === "pending" || t.status === "in_progress"
296
- );
300
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
297
301
 
298
302
  for (const task of activeTasks) {
299
303
  const areas = task.techStack?.areas ?? [];
@@ -314,9 +318,9 @@ export function groupByTechArea(tasks: Task[]): Map<TechArea, Task[]> {
314
318
  * Complexity distribution by level
315
319
  */
316
320
  export interface ComplexityDistribution {
317
- low: number; // 1 to LOW_MAX
321
+ low: number; // 1 to LOW_MAX
318
322
  medium: number; // LOW_MAX+1 to MEDIUM_MAX
319
- high: number; // MEDIUM_MAX+1 to 10
323
+ high: number; // MEDIUM_MAX+1 to 10
320
324
  }
321
325
 
322
326
  /**
@@ -337,9 +341,7 @@ export interface ComplexitySummary {
337
341
  * Analyze complexity distribution across tasks
338
342
  */
339
343
  export function getComplexitySummary(tasks: Task[]): ComplexitySummary {
340
- const activeTasks = tasks.filter(
341
- (t) => t.status === "pending" || t.status === "in_progress"
342
- );
344
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
343
345
 
344
346
  const analyzed = activeTasks.filter((t) => t.complexity?.score !== undefined);
345
347
  const unanalyzed = activeTasks.filter((t) => t.complexity?.score === undefined);
@@ -394,9 +396,7 @@ export interface TechStackSummary {
394
396
  * Analyze tech stack distribution across tasks
395
397
  */
396
398
  export function getTechStackSummary(tasks: Task[]): TechStackSummary {
397
- const activeTasks = tasks.filter(
398
- (t) => t.status === "pending" || t.status === "in_progress"
399
- );
399
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
400
400
 
401
401
  const analyzed = activeTasks.filter((t) => t.techStack?.areas !== undefined);
402
402
  const unanalyzed = activeTasks.filter((t) => t.techStack?.areas === undefined);
@@ -115,20 +115,13 @@ export function topologicalSort(tasks: Task[]): Task[] {
115
115
  /**
116
116
  * Detect if adding a dependency would create a cycle
117
117
  */
118
- export function wouldCreateCycle(
119
- tasks: Task[],
120
- fromId: string,
121
- toId: string
122
- ): boolean {
118
+ export function wouldCreateCycle(tasks: Task[], fromId: string, toId: string): boolean {
123
119
  // Create a temporary task list with the new dependency
124
120
  const tempTasks = tasks.map((t) => {
125
121
  if (t.id === fromId) {
126
122
  return {
127
123
  ...t,
128
- dependencies: [
129
- ...(t.dependencies ?? []),
130
- { taskId: toId, type: "blocked_by" as const },
131
- ],
124
+ dependencies: [...(t.dependencies ?? []), { taskId: toId, type: "blocked_by" as const }],
132
125
  };
133
126
  }
134
127
  return t;
@@ -173,7 +166,10 @@ export function buildDependencyIndices(tasks: Task[]): {
173
166
  .map((d) => d.taskId);
174
167
 
175
168
  // Store direct dependencies for this task
176
- dependenciesIndex.set(task.id, deps.filter((depId) => taskMap.has(depId)));
169
+ dependenciesIndex.set(
170
+ task.id,
171
+ deps.filter((depId) => taskMap.has(depId))
172
+ );
177
173
 
178
174
  // Update dependents index (reverse lookup)
179
175
  for (const depId of deps) {
@@ -19,21 +19,10 @@ export {
19
19
  } from "./task.js";
20
20
 
21
21
  // View schemas
22
- export {
23
- SmartViewFilter,
24
- SortField,
25
- SortOrder,
26
- SmartView,
27
- BuiltInView,
28
- } from "./view.js";
22
+ export { SmartViewFilter, SortField, SortOrder, SmartView, BuiltInView } from "./view.js";
29
23
 
30
24
  // Inbox schemas
31
- export {
32
- InboxStatus,
33
- InboxItem,
34
- InboxCreateInput,
35
- InboxUpdateInput,
36
- } from "./inbox.js";
25
+ export { InboxStatus, InboxItem, InboxCreateInput, InboxUpdateInput } from "./inbox.js";
37
26
 
38
27
  // Response format schemas (token optimization)
39
28
  export {
@@ -129,9 +129,9 @@ export interface DashboardPriorityBreakdown {
129
129
 
130
130
  // Dependency metrics
131
131
  export interface DashboardDependencyMetrics {
132
- ready: number; // No dependencies or all satisfied
133
- blocked: number; // Has unsatisfied dependencies
134
- noDeps: number; // Tasks with no dependencies
132
+ ready: number; // No dependencies or all satisfied
133
+ blocked: number; // Has unsatisfied dependencies
134
+ noDeps: number; // Tasks with no dependencies
135
135
  }
136
136
 
137
137
  // Next task recommendation
@@ -139,12 +139,12 @@ export interface DashboardNextTask {
139
139
  id: string;
140
140
  title: string;
141
141
  priority: string;
142
- reason: string; // Why this task is recommended
142
+ reason: string; // Why this task is recommended
143
143
  }
144
144
 
145
145
  // Critical path info
146
146
  export interface DashboardCriticalPath {
147
- length: number; // Total duration in minutes
147
+ length: number; // Total duration in minutes
148
148
  taskCount: number; // Number of tasks on critical path
149
149
  }
150
150
 
@@ -162,7 +162,7 @@ export interface DashboardStatusBreakdown {
162
162
  export interface DashboardSubtaskProgress {
163
163
  completed: number;
164
164
  total: number;
165
- pct: number; // 0-100
165
+ pct: number; // 0-100
166
166
  inProgress: number;
167
167
  pending: number;
168
168
  blocked: number;
@@ -28,26 +28,27 @@ const BaseResponse = z.object({
28
28
  type: ResponseType.describe("Type of response: question, suggestion, or confirmation"),
29
29
  message: z.string().describe("Main message to display to user"),
30
30
  context: z.string().optional().describe("Additional context or explanation"),
31
- priority: ResponsePriority.optional().describe("Urgency/importance of response (default: medium)"),
31
+ priority: ResponsePriority.optional().describe(
32
+ "Urgency/importance of response (default: medium)"
33
+ ),
32
34
  });
33
35
 
34
36
  // Question response - for clarification
35
37
  export const QuestionResponse = BaseResponse.extend({
36
38
  type: z.literal("question"),
37
- options: z.array(ResponseOption).optional()
39
+ options: z
40
+ .array(ResponseOption)
41
+ .optional()
38
42
  .describe("Multiple choice options (omit for free-form answer)"),
39
- defaultOption: z.string().optional()
40
- .describe("Default option value if user provides no input"),
43
+ defaultOption: z.string().optional().describe("Default option value if user provides no input"),
41
44
  });
42
45
  export type QuestionResponse = z.infer<typeof QuestionResponse>;
43
46
 
44
47
  // Suggestion response - for recommendations
45
48
  export const SuggestionResponse = BaseResponse.extend({
46
49
  type: z.literal("suggestion"),
47
- options: z.array(ResponseOption).min(1)
48
- .describe("Suggested options for user to choose from"),
49
- reasoning: z.string().optional()
50
- .describe("Explanation of why these suggestions were made"),
50
+ options: z.array(ResponseOption).min(1).describe("Suggested options for user to choose from"),
51
+ reasoning: z.string().optional().describe("Explanation of why these suggestions were made"),
51
52
  });
52
53
  export type SuggestionResponse = z.infer<typeof SuggestionResponse>;
53
54
 
@@ -55,9 +56,13 @@ export type SuggestionResponse = z.infer<typeof SuggestionResponse>;
55
56
  export const ConfirmationResponse = BaseResponse.extend({
56
57
  type: z.literal("confirmation"),
57
58
  action: z.string().describe("Action that will be taken if confirmed"),
58
- consequences: z.array(z.string()).optional()
59
+ consequences: z
60
+ .array(z.string())
61
+ .optional()
59
62
  .describe("List of effects or changes that will occur"),
60
- defaultConfirm: z.boolean().optional()
63
+ defaultConfirm: z
64
+ .boolean()
65
+ .optional()
61
66
  .describe("Default choice if user provides no input (default: false)"),
62
67
  });
63
68
  export type ConfirmationResponse = z.infer<typeof ConfirmationResponse>;
@@ -74,19 +79,19 @@ export type AgentResponse = z.infer<typeof AgentResponse>;
74
79
  export const GenerateResponseInput = z.object({
75
80
  type: ResponseType.describe("Type of response to generate"),
76
81
  message: z.string().describe("Main message"),
77
- options: z.array(ResponseOption).optional()
78
- .describe("Options for question/suggestion types"),
82
+ options: z.array(ResponseOption).optional().describe("Options for question/suggestion types"),
79
83
  context: z.string().optional(),
80
84
  priority: ResponsePriority.optional().default("medium"),
81
- action: z.string().optional()
82
- .describe("Action description (for confirmation type)"),
83
- consequences: z.array(z.string()).optional()
85
+ action: z.string().optional().describe("Action description (for confirmation type)"),
86
+ consequences: z
87
+ .array(z.string())
88
+ .optional()
84
89
  .describe("Consequences list (for confirmation type)"),
85
- reasoning: z.string().optional()
86
- .describe("Reasoning explanation (for suggestion type)"),
87
- defaultOption: z.string().optional()
88
- .describe("Default option value (for question type)"),
89
- defaultConfirm: z.boolean().optional()
90
+ reasoning: z.string().optional().describe("Reasoning explanation (for suggestion type)"),
91
+ defaultOption: z.string().optional().describe("Default option value (for question type)"),
92
+ defaultConfirm: z
93
+ .boolean()
94
+ .optional()
90
95
  .describe("Default confirmation choice (for confirmation type)"),
91
96
  });
92
97
  export type GenerateResponseInput = z.infer<typeof GenerateResponseInput>;
@@ -5,13 +5,7 @@ export const Priority = z.enum(["critical", "high", "medium", "low"]);
5
5
  export type Priority = z.infer<typeof Priority>;
6
6
 
7
7
  // Task status with clear state machine
8
- export const TaskStatus = z.enum([
9
- "pending",
10
- "in_progress",
11
- "blocked",
12
- "completed",
13
- "cancelled",
14
- ]);
8
+ export const TaskStatus = z.enum(["pending", "in_progress", "blocked", "completed", "cancelled"]);
15
9
  export type TaskStatus = z.infer<typeof TaskStatus>;
16
10
 
17
11
  // Dependency relationship types
@@ -37,25 +31,13 @@ export const TimeEstimate = z
37
31
  .refine(
38
32
  (data) => {
39
33
  const { optimistic, expected, pessimistic } = data;
40
- if (
41
- optimistic !== undefined &&
42
- expected !== undefined &&
43
- optimistic > expected
44
- ) {
34
+ if (optimistic !== undefined && expected !== undefined && optimistic > expected) {
45
35
  return false;
46
36
  }
47
- if (
48
- expected !== undefined &&
49
- pessimistic !== undefined &&
50
- expected > pessimistic
51
- ) {
37
+ if (expected !== undefined && pessimistic !== undefined && expected > pessimistic) {
52
38
  return false;
53
39
  }
54
- if (
55
- optimistic !== undefined &&
56
- pessimistic !== undefined &&
57
- optimistic > pessimistic
58
- ) {
40
+ if (optimistic !== undefined && pessimistic !== undefined && optimistic > pessimistic) {
59
41
  return false;
60
42
  }
61
43
  return true;
@@ -41,11 +41,13 @@ export interface DashboardStats {
41
41
  export interface DependencyMetrics {
42
42
  readyToWork: number;
43
43
  blockedByDependencies: number;
44
- mostDependedOn?: {
45
- id: string;
46
- title: string;
47
- dependentCount: number;
48
- } | undefined;
44
+ mostDependedOn?:
45
+ | {
46
+ id: string;
47
+ title: string;
48
+ dependentCount: number;
49
+ }
50
+ | undefined;
49
51
  }
50
52
 
51
53
  export interface WorkspaceInfo {
@@ -107,9 +109,7 @@ export function calculateStats(tasks: Task[]): DashboardStats {
107
109
  }
108
110
 
109
111
  export function calculateDependencyMetrics(tasks: Task[]): DependencyMetrics {
110
- const completedIds = new Set(
111
- tasks.filter((t) => t.status === "completed").map((t) => t.id)
112
- );
112
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
113
113
 
114
114
  let readyToWork = 0;
115
115
  let blockedByDependencies = 0;
@@ -134,10 +134,7 @@ export function calculateDependencyMetrics(tasks: Task[]): DependencyMetrics {
134
134
 
135
135
  // Track dependent counts
136
136
  for (const dep of deps) {
137
- dependentCounts.set(
138
- dep.taskId,
139
- (dependentCounts.get(dep.taskId) ?? 0) + 1
140
- );
137
+ dependentCounts.set(dep.taskId, (dependentCounts.get(dep.taskId) ?? 0) + 1);
141
138
  }
142
139
  }
143
140
 
@@ -192,8 +189,7 @@ export function renderStatusWidget(tasks: Task[]): string {
192
189
  const today = getTodayTasks(tasks);
193
190
  const overdue = getOverdueTasks(tasks);
194
191
  const activeTasks = stats.total - stats.cancelled;
195
- const percent =
196
- activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
192
+ const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
197
193
 
198
194
  const lines: string[] = [];
199
195
 
@@ -252,16 +248,10 @@ export function renderActionsWidget(tasks: Task[]): string {
252
248
 
253
249
  // Get top 4 ready tasks sorted by priority
254
250
  const readyTasks = tasks
255
- .filter(
256
- (t) =>
257
- t.status === "pending" &&
258
- (!t.dependencies || t.dependencies.length === 0)
259
- )
251
+ .filter((t) => t.status === "pending" && (!t.dependencies || t.dependencies.length === 0))
260
252
  .sort((a, b) => {
261
253
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
262
- return (
263
- (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2)
264
- );
254
+ return (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2);
265
255
  })
266
256
  .slice(0, 4);
267
257
 
@@ -341,17 +331,13 @@ export function renderWorkspacesTable(
341
331
  const stats = calculateStats(tasks);
342
332
  const depMetrics = calculateDependencyMetrics(tasks);
343
333
  const activeTasks = stats.total - stats.cancelled;
344
- const percent =
345
- activeTasks > 0
346
- ? Math.round((stats.completed / activeTasks) * 100)
347
- : 0;
334
+ const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
348
335
 
349
336
  // Create mini progress bar
350
337
  const barWidth = 8;
351
338
  const filled = Math.round((percent / 100) * barWidth);
352
339
  const empty = barWidth - filled;
353
- const miniBar =
354
- c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
340
+ const miniBar = c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
355
341
 
356
342
  rows.push({
357
343
  name: truncateStr(ws.name, 20),
@@ -392,9 +378,7 @@ export const renderProjectsTable = renderWorkspacesTable;
392
378
  * Render Tasks table for single workspace view
393
379
  */
394
380
  export function renderTasksTable(tasks: Task[], limit: number = 10): string {
395
- const activeTasks = tasks.filter(
396
- (t) => t.status !== "completed" && t.status !== "cancelled"
397
- );
381
+ const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
398
382
 
399
383
  if (activeTasks.length === 0) {
400
384
  return c.gray("No active tasks.");
@@ -421,10 +405,7 @@ export function renderTasksTable(tasks: Task[], limit: number = 10): string {
421
405
  },
422
406
  ];
423
407
 
424
- let result = table(
425
- displayTasks as unknown as Record<string, unknown>[],
426
- columns
427
- );
408
+ let result = table(displayTasks as unknown as Record<string, unknown>[], columns);
428
409
 
429
410
  if (activeTasks.length > limit) {
430
411
  result += `\n${c.gray(`(+${activeTasks.length - limit} more tasks)`)}`;
@@ -440,21 +421,14 @@ export function renderTasksTable(tasks: Task[], limit: number = 10): string {
440
421
  function getTodayTasks(tasks: Task[]): Task[] {
441
422
  const today = new Date().toISOString().split("T")[0];
442
423
  return tasks.filter(
443
- (t) =>
444
- t.dueDate === today &&
445
- t.status !== "completed" &&
446
- t.status !== "cancelled"
424
+ (t) => t.dueDate === today && t.status !== "completed" && t.status !== "cancelled"
447
425
  );
448
426
  }
449
427
 
450
428
  function getOverdueTasks(tasks: Task[]): Task[] {
451
429
  const today = new Date().toISOString().split("T")[0] ?? "";
452
430
  return tasks.filter(
453
- (t) =>
454
- t.dueDate &&
455
- t.dueDate < today &&
456
- t.status !== "completed" &&
457
- t.status !== "cancelled"
431
+ (t) => t.dueDate && t.dueDate < today && t.status !== "completed" && t.status !== "cancelled"
458
432
  );
459
433
  }
460
434
 
@@ -538,9 +512,7 @@ export function renderDashboard(
538
512
 
539
513
  // Tasks table (for single workspace view)
540
514
  if (showTasks && (currentWorkspace || workspaces.length === 1)) {
541
- const activeTasks = tasks.filter(
542
- (t) => t.status !== "completed" && t.status !== "cancelled"
543
- );
515
+ const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
544
516
  if (activeTasks.length > 0) {
545
517
  lines.push(c.bold(`Tasks (${activeTasks.length})`));
546
518
  lines.push("");
@@ -565,7 +537,7 @@ export function renderWorkspaceDashboard(
565
537
  const wsInfo: WorkspaceInfo = {
566
538
  name: workspace,
567
539
  taskCount: tasks.length,
568
- completedCount: tasks.filter(t => t.status === "completed").length,
540
+ completedCount: tasks.filter((t) => t.status === "completed").length,
569
541
  };
570
542
 
571
543
  const data: DashboardData = {
@@ -576,17 +548,13 @@ export function renderWorkspaceDashboard(
576
548
  activeTag: options.activeTag,
577
549
  };
578
550
 
579
- return renderDashboard(
580
- data,
581
- () => tasks,
582
- {
583
- showBanner: true,
584
- showInbox: false,
585
- showWorkspaces: false,
586
- showTasks: true,
587
- stripAnsiCodes: options.stripAnsiCodes,
588
- }
589
- );
551
+ return renderDashboard(data, () => tasks, {
552
+ showBanner: true,
553
+ showInbox: false,
554
+ showWorkspaces: false,
555
+ showTasks: true,
556
+ stripAnsiCodes: options.stripAnsiCodes,
557
+ });
590
558
  }
591
559
 
592
560
  // Legacy export for backwards compatibility
package/src/utils/date.ts CHANGED
@@ -247,11 +247,7 @@ export function parseDateString(input: string): Date {
247
247
 
248
248
  const d = new Date(input);
249
249
  if (isNaN(d.getTime())) {
250
- throw new DateParseError(
251
- `Unable to parse "${input}" as a date`,
252
- input,
253
- "invalid_format"
254
- );
250
+ throw new DateParseError(`Unable to parse "${input}" as a date`, input, "invalid_format");
255
251
  }
256
252
 
257
253
  return d;
@@ -280,11 +276,7 @@ export function formatDisplayDate(date: Date | string): string {
280
276
  }
281
277
 
282
278
  if (!isValidDate(d)) {
283
- throw new DateParseError(
284
- "Invalid Date object provided",
285
- String(date),
286
- "invalid_date"
287
- );
279
+ throw new DateParseError("Invalid Date object provided", String(date), "invalid_date");
288
280
  }
289
281
 
290
282
  const parts = new Intl.DateTimeFormat(undefined, {
@@ -34,7 +34,9 @@ export function getTaskLevel(tasks: Task[], taskId: string): number {
34
34
  while (currentTask.parentId) {
35
35
  const parent = taskMap.get(currentTask.parentId);
36
36
  if (!parent) {
37
- console.warn(`[task-mcp] Orphaned parent reference: task "${currentTask.id}" references non-existent parent "${currentTask.parentId}"`);
37
+ console.warn(
38
+ `[task-mcp] Orphaned parent reference: task "${currentTask.id}" references non-existent parent "${currentTask.parentId}"`
39
+ );
38
40
  break;
39
41
  }
40
42
  level++;
@@ -57,10 +59,7 @@ export function getTaskLevel(tasks: Task[], taskId: string): number {
57
59
  * @param parentId - ID of the proposed parent task
58
60
  * @returns true if a child can be added, false if it would exceed max depth
59
61
  */
60
- export function validateHierarchyDepth(
61
- tasks: Task[],
62
- parentId: string
63
- ): boolean {
62
+ export function validateHierarchyDepth(tasks: Task[], parentId: string): boolean {
64
63
  const parentLevel = getTaskLevel(tasks, parentId);
65
64
 
66
65
  if (parentLevel === -1) {
@@ -31,7 +31,13 @@ export {
31
31
  type DateParseResult,
32
32
  type IsWithinDaysResult,
33
33
  } from "./date.js";
34
- export { parseTaskInput, parseInboxInput, parseInput, type ParseTarget, type ParsedInput } from "./natural-language.js";
34
+ export {
35
+ parseTaskInput,
36
+ parseInboxInput,
37
+ parseInput,
38
+ type ParseTarget,
39
+ type ParsedInput,
40
+ } from "./natural-language.js";
35
41
  export {
36
42
  MAX_HIERARCHY_DEPTH,
37
43
  getTaskLevel,
@@ -185,11 +185,9 @@ export function isFullWidth(char: string): boolean {
185
185
  (code >= 0xffe0 && code <= 0xffe6) || // Fullwidth Symbols
186
186
  (code >= 0x20000 && code <= 0x2ffff) || // CJK Extension B, C, D, E, F
187
187
  (code >= 0x30000 && code <= 0x3ffff) || // CJK Extension G, H, I
188
-
189
188
  // Symbols (typically wide in terminals)
190
189
  (code >= 0x2600 && code <= 0x26ff) || // Misc Symbols (sun, moon, stars, etc.)
191
190
  (code >= 0x2700 && code <= 0x27bf) || // Dingbats
192
-
193
191
  // Emoji ranges (comprehensive coverage)
194
192
  (code >= 0x1f1e0 && code <= 0x1f1ff) || // Regional Indicator Symbols (flags)
195
193
  (code >= 0x1f300 && code <= 0x1f5ff) || // Misc Symbols & Pictographs
@@ -202,7 +200,6 @@ export function isFullWidth(char: string): boolean {
202
200
  (code >= 0x1fa00 && code <= 0x1fa6f) || // Chess Symbols
203
201
  (code >= 0x1fa70 && code <= 0x1faff) || // Symbols & Pictographs Extended-A
204
202
  (code >= 0x1fb00 && code <= 0x1fbff) || // Symbols for Legacy Computing
205
-
206
203
  // Additional emoji-related
207
204
  (code >= 0x231a && code <= 0x231b) || // Watch, Hourglass
208
205
  (code >= 0x23e9 && code <= 0x23f3) || // Media control symbols
@@ -278,7 +275,11 @@ export const visibleLength = displayWidth;
278
275
  /**
279
276
  * Pad string to width (accounting for display width)
280
277
  */
281
- export function pad(str: string, width: number, align: "left" | "right" | "center" = "left"): string {
278
+ export function pad(
279
+ str: string,
280
+ width: number,
281
+ align: "left" | "right" | "center" = "left"
282
+ ): string {
282
283
  const len = displayWidth(str);
283
284
  const diff = width - len;
284
285
  if (diff <= 0) return str;
@@ -359,7 +360,8 @@ export function progressBar(
359
360
  const filledCount = Math.round((percent / 100) * width);
360
361
  const emptyCount = width - filledCount;
361
362
 
362
- const bar = color(filled.repeat(filledCount), filledColor) + color(empty.repeat(emptyCount), emptyColor);
363
+ const bar =
364
+ color(filled.repeat(filledCount), filledColor) + color(empty.repeat(emptyCount), emptyColor);
363
365
 
364
366
  return showPercent ? `${bar} ${percent}%` : bar;
365
367
  }
@@ -383,7 +385,10 @@ export function box(content: string, options: BoxOptions = {}): string {
383
385
  const { padding = 1, borderColor = "cyan", title, rounded = true } = options;
384
386
 
385
387
  const lines = content.split("\n");
386
- const maxLen = Math.max(...lines.map(l => displayWidth(stripAnsi(l))), title ? title.length + 2 : 0);
388
+ const maxLen = Math.max(
389
+ ...lines.map((l) => displayWidth(stripAnsi(l))),
390
+ title ? title.length + 2 : 0
391
+ );
387
392
  const innerWidth = options.width ? options.width - 2 - padding * 2 : maxLen + padding * 2;
388
393
 
389
394
  const tl = rounded ? BOX.rTopLeft : BOX.topLeft;
@@ -402,7 +407,10 @@ export function box(content: string, options: BoxOptions = {}): string {
402
407
  const remaining = innerWidth - titlePart.length;
403
408
  const leftPad = Math.floor(remaining / 2);
404
409
  const rightPad = remaining - leftPad;
405
- top = applyBorder(tl + h.repeat(leftPad)) + c.bold(titlePart) + applyBorder(h.repeat(rightPad) + tr);
410
+ top =
411
+ applyBorder(tl + h.repeat(leftPad)) +
412
+ c.bold(titlePart) +
413
+ applyBorder(h.repeat(rightPad) + tr);
406
414
  } else {
407
415
  top = applyBorder(tl + h.repeat(innerWidth) + tr);
408
416
  }
@@ -412,10 +420,16 @@ export function box(content: string, options: BoxOptions = {}): string {
412
420
  const paddingLines = Array(padding).fill(padLine);
413
421
 
414
422
  // Content lines
415
- const contentLines = lines.map(line => {
423
+ const contentLines = lines.map((line) => {
416
424
  const lineWidth = displayWidth(stripAnsi(line));
417
425
  const padRight = innerWidth - lineWidth - padding;
418
- return applyBorder(v) + " ".repeat(padding) + line + " ".repeat(Math.max(0, padRight)) + applyBorder(v);
426
+ return (
427
+ applyBorder(v) +
428
+ " ".repeat(padding) +
429
+ line +
430
+ " ".repeat(Math.max(0, padRight)) +
431
+ applyBorder(v)
432
+ );
419
433
  });
420
434
 
421
435
  // Bottom border
@@ -452,16 +466,16 @@ export function drawBox(lines: string[], options: BoxOptions = {}): string[] {
452
466
  const rightBorder = remainingWidth - leftBorder;
453
467
  result.push(
454
468
  applyBorder(BOX.topLeft) +
455
- applyBorder(BOX.horizontal.repeat(leftBorder)) +
456
- c.label(titleStr) +
457
- applyBorder(BOX.horizontal.repeat(rightBorder)) +
458
- applyBorder(BOX.topRight)
469
+ applyBorder(BOX.horizontal.repeat(leftBorder)) +
470
+ c.label(titleStr) +
471
+ applyBorder(BOX.horizontal.repeat(rightBorder)) +
472
+ applyBorder(BOX.topRight)
459
473
  );
460
474
  } else {
461
475
  result.push(
462
476
  applyBorder(BOX.topLeft) +
463
- applyBorder(BOX.horizontal.repeat(boxWidth - 2)) +
464
- applyBorder(BOX.topRight)
477
+ applyBorder(BOX.horizontal.repeat(boxWidth - 2)) +
478
+ applyBorder(BOX.topRight)
465
479
  );
466
480
  }
467
481
 
@@ -469,18 +483,18 @@ export function drawBox(lines: string[], options: BoxOptions = {}): string[] {
469
483
  for (const line of lines) {
470
484
  result.push(
471
485
  applyBorder(BOX.vertical) +
472
- padStr +
473
- padEnd(line, innerWidth) +
474
- padStr +
475
- applyBorder(BOX.vertical)
486
+ padStr +
487
+ padEnd(line, innerWidth) +
488
+ padStr +
489
+ applyBorder(BOX.vertical)
476
490
  );
477
491
  }
478
492
 
479
493
  // Bottom border
480
494
  result.push(
481
495
  applyBorder(BOX.bottomLeft) +
482
- applyBorder(BOX.horizontal.repeat(boxWidth - 2)) +
483
- applyBorder(BOX.bottomRight)
496
+ applyBorder(BOX.horizontal.repeat(boxWidth - 2)) +
497
+ applyBorder(BOX.bottomRight)
484
498
  );
485
499
 
486
500
  return result;
@@ -494,9 +508,9 @@ export function drawBox(lines: string[], options: BoxOptions = {}): string[] {
494
508
  * Place multiple boxes side by side (string input, string output)
495
509
  */
496
510
  export function sideBySide(boxes: string[], gap: number = 2): string {
497
- const boxLines = boxes.map(b => b.split("\n"));
498
- const maxHeight = Math.max(...boxLines.map(lines => lines.length));
499
- const boxWidths = boxLines.map(lines => Math.max(...lines.map(l => displayWidth(l))));
511
+ const boxLines = boxes.map((b) => b.split("\n"));
512
+ const maxHeight = Math.max(...boxLines.map((lines) => lines.length));
513
+ const boxWidths = boxLines.map((lines) => Math.max(...lines.map((l) => displayWidth(l))));
500
514
 
501
515
  // Pad each box to max height
502
516
  const paddedBoxLines = boxLines.map((lines, i) => {
@@ -504,7 +518,7 @@ export function sideBySide(boxes: string[], gap: number = 2): string {
504
518
  while (lines.length < maxHeight) {
505
519
  lines.push(" ".repeat(width));
506
520
  }
507
- return lines.map(line => {
521
+ return lines.map((line) => {
508
522
  const lineWidth = displayWidth(line);
509
523
  if (lineWidth < width) {
510
524
  return line + " ".repeat(width - lineWidth);
@@ -518,7 +532,7 @@ export function sideBySide(boxes: string[], gap: number = 2): string {
518
532
  const gapStr = " ".repeat(gap);
519
533
 
520
534
  for (let i = 0; i < maxHeight; i++) {
521
- const lineParts = paddedBoxLines.map(lines => lines[i] ?? "");
535
+ const lineParts = paddedBoxLines.map((lines) => lines[i] ?? "");
522
536
  result.push(lineParts.join(gapStr));
523
537
  }
524
538
 
@@ -529,14 +543,8 @@ export function sideBySide(boxes: string[], gap: number = 2): string {
529
543
  * Merge two boxes side by side (array input, array output)
530
544
  * For MCP server compatibility
531
545
  */
532
- export function sideBySideArrays(
533
- leftLines: string[],
534
- rightLines: string[],
535
- gap = 2
536
- ): string[] {
537
- const leftWidth = leftLines.length > 0
538
- ? Math.max(...leftLines.map(displayWidth))
539
- : 0;
546
+ export function sideBySideArrays(leftLines: string[], rightLines: string[], gap = 2): string[] {
547
+ const leftWidth = leftLines.length > 0 ? Math.max(...leftLines.map(displayWidth)) : 0;
540
548
 
541
549
  const maxLines = Math.max(leftLines.length, rightLines.length);
542
550
  const result: string[] = [];
@@ -580,10 +588,10 @@ export function table<T extends Record<string, unknown>>(
580
588
  const { headerColor = "cyan", borderColor = "gray" } = options;
581
589
 
582
590
  // Calculate column widths
583
- const widths = columns.map(col => {
591
+ const widths = columns.map((col) => {
584
592
  const headerWidth = displayWidth(col.header);
585
593
  const maxDataWidth = Math.max(
586
- ...data.map(row => {
594
+ ...data.map((row) => {
587
595
  const val = col.format ? col.format(row[col.key], row) : String(row[col.key] ?? "");
588
596
  return displayWidth(val);
589
597
  }),
@@ -601,10 +609,10 @@ export function table<T extends Record<string, unknown>>(
601
609
  .join(` ${border} `);
602
610
 
603
611
  // Separator
604
- const separator = widths.map(w => hBorder.repeat(w)).join(color(`─${BOX.cross}─`, borderColor));
612
+ const separator = widths.map((w) => hBorder.repeat(w)).join(color(`─${BOX.cross}─`, borderColor));
605
613
 
606
614
  // Data rows
607
- const dataRows = data.map(row =>
615
+ const dataRows = data.map((row) =>
608
616
  columns
609
617
  .map((col, i) => {
610
618
  const w = widths[i] ?? 0;
@@ -621,33 +629,24 @@ export function table<T extends Record<string, unknown>>(
621
629
  * Render table with full borders (array output)
622
630
  * For MCP server compatibility
623
631
  */
624
- export function renderTable(
625
- columns: TableColumn[],
626
- rows: Record<string, unknown>[]
627
- ): string[] {
632
+ export function renderTable(columns: TableColumn[], rows: Record<string, unknown>[]): string[] {
628
633
  const colWidths: number[] = columns.map((col) => {
629
634
  const headerWidth = col.header.length;
630
- const maxValueWidth = Math.max(
631
- ...rows.map((row) => String(row[col.key] ?? "").length)
632
- );
635
+ const maxValueWidth = Math.max(...rows.map((row) => String(row[col.key] ?? "").length));
633
636
  return col.width ?? Math.max(headerWidth, maxValueWidth);
634
637
  });
635
638
 
636
639
  const result: string[] = [];
637
640
 
638
641
  // Header row
639
- const headerCells = columns.map((col, i) =>
640
- c.label(center(col.header, colWidths[i] ?? 0))
642
+ const headerCells = columns.map((col, i) => c.label(center(col.header, colWidths[i] ?? 0)));
643
+ result.push(
644
+ c.muted(BOX.vertical) + headerCells.join(c.muted(BOX.vertical)) + c.muted(BOX.vertical)
641
645
  );
642
- result.push(c.muted(BOX.vertical) + headerCells.join(c.muted(BOX.vertical)) + c.muted(BOX.vertical));
643
646
 
644
647
  // Separator
645
648
  const separator = columns.map((_, i) => BOX.horizontal.repeat(colWidths[i] ?? 0));
646
- result.push(
647
- c.muted(BOX.teeRight) +
648
- c.muted(separator.join(BOX.cross)) +
649
- c.muted(BOX.teeLeft)
650
- );
649
+ result.push(c.muted(BOX.teeRight) + c.muted(separator.join(BOX.cross)) + c.muted(BOX.teeLeft));
651
650
 
652
651
  // Data rows
653
652
  for (const row of rows) {
@@ -782,5 +781,5 @@ export function banner(text: string): string {
782
781
  }
783
782
  }
784
783
 
785
- return lines.map(l => c.cyan(l)).join("\n");
784
+ return lines.map((l) => c.cyan(l)).join("\n");
786
785
  }