@task-mcp/cli 1.0.11 → 1.0.13

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/cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Zero-dependency CLI for task-mcp with Bun native visualization",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,8 +30,8 @@
30
30
  "directory": "packages/cli"
31
31
  },
32
32
  "dependencies": {
33
- "@task-mcp/mcp-server": "^1.0.10",
34
- "@task-mcp/shared": "^1.0.9"
33
+ "@task-mcp/mcp-server": "^1.0.11",
34
+ "@task-mcp/shared": "^1.0.10"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/bun": "^1.1.14",
package/src/ansi.ts CHANGED
@@ -1,453 +1,50 @@
1
1
  /**
2
- * Zero-dependency ANSI utilities for terminal visualization
3
- * Uses Bun native APIs where available, falls back to raw ANSI codes
4
- */
5
-
6
- // ANSI escape codes
7
- const ESC = "\x1b[";
8
- const RESET = `${ESC}0m`;
9
-
10
- // Color codes (foreground)
11
- const FG = {
12
- black: 30,
13
- red: 31,
14
- green: 32,
15
- yellow: 33,
16
- blue: 34,
17
- magenta: 35,
18
- cyan: 36,
19
- white: 37,
20
- gray: 90,
21
- brightRed: 91,
22
- brightGreen: 92,
23
- brightYellow: 93,
24
- brightBlue: 94,
25
- brightMagenta: 95,
26
- brightCyan: 96,
27
- brightWhite: 97,
28
- } as const;
29
-
30
- // Style codes
31
- const STYLE = {
32
- bold: 1,
33
- dim: 2,
34
- italic: 3,
35
- underline: 4,
36
- inverse: 7,
37
- strikethrough: 9,
38
- } as const;
39
-
40
- type ColorName = keyof typeof FG;
41
- type StyleName = keyof typeof STYLE;
42
-
43
- /**
44
- * Apply color to text
45
- */
46
- export function color(text: string, colorName: ColorName): string {
47
- return `${ESC}${FG[colorName]}m${text}${RESET}`;
48
- }
49
-
50
- /**
51
- * Apply style to text
52
- */
53
- export function style(text: string, styleName: StyleName): string {
54
- return `${ESC}${STYLE[styleName]}m${text}${RESET}`;
55
- }
56
-
57
- /**
58
- * Combine color and style
59
- */
60
- export function styled(text: string, colorName: ColorName, styleName?: StyleName): string {
61
- if (styleName) {
62
- return `${ESC}${STYLE[styleName]};${FG[colorName]}m${text}${RESET}`;
63
- }
64
- return color(text, colorName);
65
- }
66
-
67
- // Convenience color functions
68
- export const c = {
69
- red: (t: string) => color(t, "red"),
70
- green: (t: string) => color(t, "green"),
71
- yellow: (t: string) => color(t, "yellow"),
72
- blue: (t: string) => color(t, "blue"),
73
- cyan: (t: string) => color(t, "cyan"),
74
- magenta: (t: string) => color(t, "magenta"),
75
- gray: (t: string) => color(t, "gray"),
76
- white: (t: string) => color(t, "white"),
77
- bold: (t: string) => style(t, "bold"),
78
- dim: (t: string) => style(t, "dim"),
79
-
80
- // Semantic colors
81
- success: (t: string) => color(t, "green"),
82
- error: (t: string) => color(t, "red"),
83
- warning: (t: string) => color(t, "yellow"),
84
- info: (t: string) => color(t, "cyan"),
85
- muted: (t: string) => color(t, "gray"),
86
- };
87
-
88
- // Box drawing characters (Unicode)
89
- export const BOX = {
90
- // Single line
91
- topLeft: "┌",
92
- topRight: "┐",
93
- bottomLeft: "└",
94
- bottomRight: "┘",
95
- horizontal: "─",
96
- vertical: "│",
97
-
98
- // T-junctions
99
- tTop: "┬",
100
- tBottom: "┴",
101
- tLeft: "├",
102
- tRight: "┤",
103
- cross: "┼",
104
-
105
- // Double line
106
- dTopLeft: "╔",
107
- dTopRight: "╗",
108
- dBottomLeft: "╚",
109
- dBottomRight: "╝",
110
- dHorizontal: "═",
111
- dVertical: "║",
112
-
113
- // Rounded
114
- rTopLeft: "╭",
115
- rTopRight: "╮",
116
- rBottomLeft: "╰",
117
- rBottomRight: "╯",
118
- } as const;
119
-
120
- /**
121
- * Create a horizontal line
122
- */
123
- export function hline(width: number, char: string = BOX.horizontal): string {
124
- return char.repeat(width);
125
- }
126
-
127
- /**
128
- * Create a box around text
129
- */
130
- export function box(content: string, options: {
131
- padding?: number;
132
- borderColor?: ColorName;
133
- title?: string;
134
- rounded?: boolean;
135
- } = {}): string {
136
- const { padding = 1, borderColor = "cyan", title, rounded = true } = options;
137
-
138
- const lines = content.split("\n");
139
- const maxLen = Math.max(...lines.map(l => displayWidth(stripAnsi(l))), title ? title.length + 2 : 0);
140
- const innerWidth = maxLen + padding * 2;
141
-
142
- const tl = rounded ? BOX.rTopLeft : BOX.topLeft;
143
- const tr = rounded ? BOX.rTopRight : BOX.topRight;
144
- const bl = rounded ? BOX.rBottomLeft : BOX.bottomLeft;
145
- const br = rounded ? BOX.rBottomRight : BOX.bottomRight;
146
- const h = BOX.horizontal;
147
- const v = BOX.vertical;
148
-
149
- const applyBorder = (s: string) => color(s, borderColor);
150
-
151
- // Top border with optional title
152
- let top: string;
153
- if (title) {
154
- const titlePart = ` ${title} `;
155
- const remaining = innerWidth - titlePart.length;
156
- const leftPad = Math.floor(remaining / 2);
157
- const rightPad = remaining - leftPad;
158
- top = applyBorder(tl + h.repeat(leftPad)) + c.bold(titlePart) + applyBorder(h.repeat(rightPad) + tr);
159
- } else {
160
- top = applyBorder(tl + h.repeat(innerWidth) + tr);
161
- }
162
-
163
- // Padding lines
164
- const padLine = applyBorder(v) + " ".repeat(innerWidth) + applyBorder(v);
165
- const paddingLines = Array(padding).fill(padLine);
166
-
167
- // Content lines
168
- const contentLines = lines.map(line => {
169
- const lineWidth = displayWidth(stripAnsi(line));
170
- const padRight = innerWidth - lineWidth - padding;
171
- return applyBorder(v) + " ".repeat(padding) + line + " ".repeat(Math.max(0, padRight)) + applyBorder(v);
172
- });
173
-
174
- // Bottom border
175
- const bottom = applyBorder(bl + h.repeat(innerWidth) + br);
176
-
177
- return [top, ...paddingLines, ...contentLines, ...paddingLines, bottom].join("\n");
178
- }
179
-
180
- /**
181
- * Strip ANSI codes from string (for length calculation)
182
- */
183
- export function stripAnsi(str: string): string {
184
- return str.replace(/\x1b\[[0-9;]*m/g, "");
185
- }
186
-
187
- /**
188
- * Check if a character is a wide character (CJK, emoji, etc.)
189
- * Wide characters take 2 columns in terminal
190
- */
191
- function isWideChar(char: string): boolean {
192
- const code = char.codePointAt(0) ?? 0;
193
-
194
- // CJK ranges
195
- if (
196
- (code >= 0x1100 && code <= 0x115F) || // Hangul Jamo
197
- (code >= 0x2E80 && code <= 0x9FFF) || // CJK
198
- (code >= 0xAC00 && code <= 0xD7AF) || // Hangul Syllables
199
- (code >= 0xF900 && code <= 0xFAFF) || // CJK Compatibility
200
- (code >= 0xFE10 && code <= 0xFE1F) || // Vertical forms
201
- (code >= 0xFE30 && code <= 0xFE6F) || // CJK Compatibility Forms
202
- (code >= 0xFF00 && code <= 0xFF60) || // Fullwidth Forms
203
- (code >= 0xFFE0 && code <= 0xFFE6) || // Fullwidth Signs
204
- (code >= 0x20000 && code <= 0x2FFFF) // CJK Extension B+
205
- ) {
206
- return true;
207
- }
208
- return false;
209
- }
210
-
211
- /**
212
- * Get display width of a string (CJK = 2, ASCII = 1)
213
- */
214
- export function displayWidth(str: string): number {
215
- const stripped = stripAnsi(str);
216
- let width = 0;
217
- for (const char of stripped) {
218
- width += isWideChar(char) ? 2 : 1;
219
- }
220
- return width;
221
- }
222
-
223
- /**
224
- * Pad string to width (accounting for ANSI codes and wide characters)
225
- */
226
- export function pad(str: string, width: number, align: "left" | "right" | "center" = "left"): string {
227
- const len = displayWidth(str);
228
- const diff = width - len;
229
- if (diff <= 0) return str;
230
-
231
- switch (align) {
232
- case "right":
233
- return " ".repeat(diff) + str;
234
- case "center":
235
- const left = Math.floor(diff / 2);
236
- return " ".repeat(left) + str + " ".repeat(diff - left);
237
- default:
238
- return str + " ".repeat(diff);
239
- }
240
- }
241
-
242
- /**
243
- * Truncate string to max display width
244
- */
245
- export function truncate(str: string, maxLen: number, suffix = "..."): string {
246
- const stripped = stripAnsi(str);
247
- if (displayWidth(stripped) <= maxLen) return str;
248
-
249
- // Truncate by display width
250
- const suffixWidth = displayWidth(suffix);
251
- let width = 0;
252
- let result = "";
253
-
254
- for (const char of stripped) {
255
- const charWidth = isWideChar(char) ? 2 : 1;
256
- if (width + charWidth > maxLen - suffixWidth) break;
257
- result += char;
258
- width += charWidth;
259
- }
260
-
261
- return result + suffix;
262
- }
263
-
264
- /**
265
- * Create a simple table
266
- */
267
- export interface TableColumn {
268
- header: string;
269
- key: string;
270
- width?: number;
271
- align?: "left" | "right" | "center";
272
- format?: (value: unknown) => string;
273
- }
274
-
275
- export function table<T extends Record<string, unknown>>(
276
- data: T[],
277
- columns: TableColumn[],
278
- options: {
279
- headerColor?: ColorName;
280
- borderColor?: ColorName;
281
- } = {}
282
- ): string {
283
- const { headerColor = "cyan", borderColor = "gray" } = options;
284
-
285
- // Calculate column widths using display width
286
- const widths = columns.map(col => {
287
- const headerWidth = displayWidth(col.header);
288
- const maxDataWidth = Math.max(
289
- ...data.map(row => {
290
- const val = col.format ? col.format(row[col.key]) : String(row[col.key] ?? "");
291
- return displayWidth(val);
292
- }),
293
- 0
294
- );
295
- return col.width ?? Math.max(headerWidth, maxDataWidth);
296
- });
297
-
298
- const border = color(BOX.vertical, borderColor);
299
- const hBorder = color(BOX.horizontal, borderColor);
300
-
301
- // Header
302
- const headerRow = columns
303
- .map((col, i) => color(pad(col.header, widths[i], col.align), headerColor))
304
- .join(` ${border} `);
305
-
306
- // Separator
307
- const separator = widths.map(w => hBorder.repeat(w)).join(color(`─${BOX.cross}─`, borderColor));
308
-
309
- // Data rows
310
- const dataRows = data.map(row =>
311
- columns
312
- .map((col, i) => {
313
- const val = col.format ? col.format(row[col.key]) : String(row[col.key] ?? "");
314
- return pad(truncate(val, widths[i]), widths[i], col.align);
315
- })
316
- .join(` ${border} `)
317
- );
318
-
319
- return [headerRow, separator, ...dataRows].join("\n");
320
- }
321
-
322
- /**
323
- * Create a progress bar
324
- */
325
- export function progressBar(
326
- current: number,
327
- total: number,
328
- options: {
329
- width?: number;
330
- filled?: string;
331
- empty?: string;
332
- showPercent?: boolean;
333
- filledColor?: ColorName;
334
- emptyColor?: ColorName;
335
- } = {}
336
- ): string {
337
- const {
338
- width = 20,
339
- filled = "█",
340
- empty = "░",
341
- showPercent = true,
342
- filledColor = "green",
343
- emptyColor = "gray",
344
- } = options;
345
-
346
- const percent = total > 0 ? Math.round((current / total) * 100) : 0;
347
- const filledCount = Math.round((percent / 100) * width);
348
- const emptyCount = width - filledCount;
349
-
350
- const bar = color(filled.repeat(filledCount), filledColor) + color(empty.repeat(emptyCount), emptyColor);
351
-
352
- return showPercent ? `${bar} ${percent}%` : bar;
353
- }
354
-
355
- /**
356
- * Status icons
357
- */
358
- export const icons = {
359
- // Status
360
- pending: c.yellow("○"),
361
- in_progress: c.blue("◐"),
362
- completed: c.green("✓"),
363
- blocked: c.red("✗"),
364
- cancelled: c.gray("⊘"),
365
-
366
- // Priority
367
- critical: c.red("!!!"),
368
- high: c.yellow("!!"),
369
- medium: c.blue("!"),
370
- low: c.gray("·"),
371
-
372
- // Misc
373
- arrow: c.cyan("→"),
374
- bullet: c.gray("•"),
375
- check: c.green("✓"),
376
- cross: c.red("✗"),
377
- warning: c.yellow("⚠"),
378
- info: c.cyan("ℹ"),
379
- };
380
-
381
- /**
382
- * Place multiple boxes side by side
383
- * Each box is a multi-line string
384
- */
385
- export function sideBySide(boxes: string[], gap: number = 2): string {
386
- // Split each box into lines
387
- const boxLines = boxes.map(b => b.split("\n"));
388
-
389
- // Find max height
390
- const maxHeight = Math.max(...boxLines.map(lines => lines.length));
391
-
392
- // Find width of each box (using first line as reference)
393
- const boxWidths = boxLines.map(lines => {
394
- return Math.max(...lines.map(l => displayWidth(l)));
395
- });
396
-
397
- // Pad each box to max height
398
- const paddedBoxLines = boxLines.map((lines, i) => {
399
- const width = boxWidths[i];
400
- while (lines.length < maxHeight) {
401
- lines.push(" ".repeat(width));
402
- }
403
- // Ensure each line is padded to box width
404
- return lines.map(line => {
405
- const lineWidth = displayWidth(line);
406
- if (lineWidth < width) {
407
- return line + " ".repeat(width - lineWidth);
408
- }
409
- return line;
410
- });
411
- });
412
-
413
- // Combine lines horizontally
414
- const result: string[] = [];
415
- const gapStr = " ".repeat(gap);
416
-
417
- for (let i = 0; i < maxHeight; i++) {
418
- const lineParts = paddedBoxLines.map(lines => lines[i] ?? "");
419
- result.push(lineParts.join(gapStr));
420
- }
421
-
422
- return result.join("\n");
423
- }
424
-
425
- /**
426
- * ASCII art text (simple implementation)
427
- */
428
- export function banner(text: string): string {
429
- // Simple block letters for "TASK MCP"
430
- const letters: Record<string, string[]> = {
431
- T: ["████", " ██ ", " ██ ", " ██ ", " ██ "],
432
- A: [" ██ ", "████", "██ █", "████", "██ █"],
433
- S: ["████", "██ ", "████", " ██", "████"],
434
- K: ["██ █", "███ ", "██ ", "███ ", "██ █"],
435
- M: ["█ █", "██ ██", "█ █ █", "█ █", "█ █"],
436
- C: ["████", "██ ", "██ ", "██ ", "████"],
437
- P: ["████", "██ █", "████", "██ ", "██ "],
438
- " ": [" ", " ", " ", " ", " "],
439
- };
440
-
441
- const chars = text.toUpperCase().split("");
442
- const lines: string[] = ["", "", "", "", ""];
443
-
444
- for (const char of chars) {
445
- const letterLines = letters[char] ?? letters[" "];
446
- for (let i = 0; i < 5; i++) {
447
- lines[i] += (letterLines[i] ?? "") + " ";
448
- }
449
- }
450
-
451
- return lines.map(l => c.cyan(l)).join("\n");
452
- }
453
-
2
+ * Re-export terminal UI utilities from shared package
3
+ * This file exists for backward compatibility
4
+ */
5
+
6
+ export {
7
+ // Colors and styles
8
+ color,
9
+ style,
10
+ styled,
11
+ c,
12
+ // Box drawing
13
+ BOX,
14
+ box,
15
+ drawBox,
16
+ hline,
17
+ // String utilities
18
+ stripAnsi,
19
+ displayWidth,
20
+ visibleLength,
21
+ pad,
22
+ padEnd,
23
+ padStart,
24
+ center,
25
+ truncateStr,
26
+ // Progress bar
27
+ progressBar,
28
+ type ProgressBarOptions,
29
+ // Layout
30
+ sideBySide,
31
+ sideBySideArrays,
32
+ type BoxOptions,
33
+ // Tables
34
+ table,
35
+ renderTable,
36
+ type TableColumn,
37
+ // Formatters
38
+ statusColors,
39
+ statusIcons,
40
+ icons,
41
+ formatStatus,
42
+ priorityColors,
43
+ formatPriority,
44
+ formatDependencies,
45
+ // Banner
46
+ banner,
47
+ } from "@task-mcp/shared";
48
+
49
+ // Alias for CLI compatibility
50
+ export { truncateStr as truncate } from "@task-mcp/shared";
@@ -1,261 +1,30 @@
1
1
  /**
2
2
  * Dashboard command - displays project overview with unified widgets
3
- * Inspired by TaskMaster CLI design
3
+ * Uses shared dashboard renderer from @task-mcp/shared
4
4
  */
5
5
 
6
6
  import { VERSION } from "../index.js";
7
+ import { c } from "../ansi.js";
7
8
  import {
8
- c,
9
- box,
10
- progressBar,
11
- table,
12
- icons,
13
- banner,
14
- pad,
15
- truncate,
16
- stripAnsi,
17
- sideBySide,
18
- displayWidth,
19
- type TableColumn,
20
- } from "../ansi.js";
9
+ renderGlobalDashboard,
10
+ renderProjectDashboard,
11
+ type Task as SharedTask,
12
+ type Project as SharedProject,
13
+ } from "@task-mcp/shared";
21
14
  import {
22
15
  listProjects,
23
16
  listTasks,
24
17
  listAllTasks,
25
- calculateStats,
26
- calculateDependencyMetrics,
27
- suggestNextTask,
28
- getTodayTasks,
29
- getOverdueTasks,
30
18
  listInboxItems,
31
19
  type Task,
32
20
  type Project,
33
21
  } from "../storage.js";
34
22
 
35
- // =============================================================================
36
- // Formatters
37
- // =============================================================================
38
-
39
- function formatPriority(priority: Task["priority"]): string {
40
- const colors: Record<string, (s: string) => string> = {
41
- critical: c.red,
42
- high: c.yellow,
43
- medium: c.blue,
44
- low: c.gray,
45
- };
46
- return (colors[priority] ?? c.gray)(priority);
47
- }
48
-
49
- // =============================================================================
50
- // Widget: Unified Status (combines Overview + Schedule + Dependencies)
51
- // =============================================================================
52
-
53
- function renderStatusWidget(tasks: Task[], projects: Project[]): string {
54
- const stats = calculateStats(tasks);
55
- const depMetrics = calculateDependencyMetrics(tasks);
56
- const today = getTodayTasks(tasks);
57
- const overdue = getOverdueTasks(tasks);
58
- const activeTasks = stats.total - stats.cancelled;
59
- const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
60
-
61
- const lines: string[] = [];
62
-
63
- // Progress bar with fraction
64
- const bar = progressBar(stats.completed, activeTasks, { width: 24 });
65
- lines.push(`${bar} ${c.bold(`${percent}%`)} ${stats.completed}/${activeTasks} tasks`);
66
- lines.push("");
67
-
68
- // Status counts (2 rows)
69
- lines.push(
70
- `${c.green("Done")}: ${stats.completed} ` +
71
- `${c.blue("Progress")}: ${stats.inProgress} ` +
72
- `${c.yellow("Pending")}: ${stats.pending} ` +
73
- `${c.red("Blocked")}: ${stats.blocked}`
74
- );
75
-
76
- // Schedule info
77
- const scheduleInfo = [];
78
- if (overdue.length > 0) scheduleInfo.push(c.red(`Overdue: ${overdue.length}`));
79
- if (today.length > 0) scheduleInfo.push(c.yellow(`Today: ${today.length}`));
80
- if (scheduleInfo.length > 0) {
81
- lines.push(scheduleInfo.join(" "));
82
- }
83
-
84
- lines.push("");
85
-
86
- // Priority breakdown
87
- lines.push(
88
- `${c.red("Critical")}: ${stats.byPriority.critical} ` +
89
- `${c.yellow("High")}: ${stats.byPriority.high} ` +
90
- `${c.blue("Medium")}: ${stats.byPriority.medium} ` +
91
- `${c.gray("Low")}: ${stats.byPriority.low}`
92
- );
93
-
94
- // Dependencies summary
95
- lines.push(
96
- `${c.green("Ready")}: ${depMetrics.readyToWork} ` +
97
- `${c.red("Blocked")}: ${depMetrics.blockedByDependencies}` +
98
- (depMetrics.mostDependedOn
99
- ? ` ${c.dim("Bottleneck:")} ${truncate(depMetrics.mostDependedOn.title, 15)}`
100
- : "")
101
- );
102
-
103
- return box(lines.join("\n"), {
104
- title: "Status",
105
- borderColor: "cyan",
106
- padding: 1,
107
- });
108
- }
109
-
110
- // =============================================================================
111
- // Widget: Next Actions (combines Next Task + Inbox)
112
- // =============================================================================
113
-
114
- async function renderActionsWidget(tasks: Task[]): Promise<string> {
115
- const nextTask = suggestNextTask(tasks);
116
-
117
- const lines: string[] = [];
118
-
119
- // Next tasks (top 3 suggestions)
120
- const readyTasks = tasks
121
- .filter(t => t.status === "pending" && (!t.dependencies || t.dependencies.length === 0))
122
- .sort((a, b) => {
123
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
124
- return (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2);
125
- })
126
- .slice(0, 4);
127
-
128
- if (readyTasks.length > 0) {
129
- for (const task of readyTasks) {
130
- const deps = task.dependencies?.length ?? 0;
131
- const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
132
- lines.push(`${c.cyan("→")} ${truncate(task.title, 24)}`);
133
- lines.push(` ${formatPriority(task.priority)}, ${depsInfo}`);
134
- }
135
- } else if (nextTask) {
136
- const deps = nextTask.dependencies?.length ?? 0;
137
- const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
138
- lines.push(`${c.cyan("→")} ${truncate(nextTask.title, 24)}`);
139
- lines.push(` ${formatPriority(nextTask.priority)}, ${depsInfo}`);
140
- } else {
141
- lines.push(c.dim("No tasks ready"));
142
- }
143
-
144
- return box(lines.join("\n"), {
145
- title: "Next Actions",
146
- borderColor: "green",
147
- padding: 1,
148
- });
149
- }
150
-
151
- // =============================================================================
152
- // Widget: Inbox
153
- // =============================================================================
154
-
155
- async function renderInboxWidget(): Promise<string | null> {
156
- const pendingItems = await listInboxItems("pending");
157
-
158
- if (pendingItems.length === 0) {
159
- return null; // Don't show if empty
160
- }
161
-
162
- const lines: string[] = [];
163
- lines.push(`${c.yellow("Pending")}: ${pendingItems.length} items`);
164
- lines.push("");
165
-
166
- // Show up to 3 items
167
- for (const item of pendingItems.slice(0, 3)) {
168
- const date = new Date(item.capturedAt);
169
- const ago = getTimeAgo(date);
170
- const tags = item.tags?.length ? c.dim(` #${item.tags[0]}`) : "";
171
- lines.push(`${c.yellow("○")} ${truncate(item.content, 40)}${tags}`);
172
- lines.push(` ${c.dim(ago)}`);
173
- }
174
-
175
- if (pendingItems.length > 3) {
176
- lines.push(c.gray(`+${pendingItems.length - 3} more`));
177
- }
178
-
179
- return box(lines.join("\n"), {
180
- title: "Inbox",
181
- borderColor: "yellow",
182
- padding: 1,
183
- });
184
- }
185
-
186
- function getTimeAgo(date: Date): string {
187
- const now = new Date();
188
- const diffMs = now.getTime() - date.getTime();
189
- const diffMins = Math.floor(diffMs / 60000);
190
- const diffHours = Math.floor(diffMins / 60);
191
- const diffDays = Math.floor(diffHours / 24);
192
-
193
- if (diffMins < 60) return `${diffMins}m ago`;
194
- if (diffHours < 24) return `${diffHours}h ago`;
195
- if (diffDays < 7) return `${diffDays}d ago`;
196
- return date.toLocaleDateString();
197
- }
198
-
199
- // =============================================================================
200
- // Projects Table
201
- // =============================================================================
202
-
203
- async function renderProjectsTable(projects: Project[]): Promise<string> {
204
- if (projects.length === 0) {
205
- return c.gray("No projects found.");
206
- }
207
-
208
- const rows: {
209
- name: string;
210
- progress: string;
211
- ready: number;
212
- blocked: number;
213
- total: number;
214
- }[] = [];
215
-
216
- for (const project of projects.slice(0, 10)) {
217
- const tasks = await listTasks(project.id);
218
- const stats = calculateStats(tasks);
219
- const depMetrics = calculateDependencyMetrics(tasks);
220
- const activeTasks = stats.total - stats.cancelled;
221
- const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
222
-
223
- // Create mini progress bar
224
- const barWidth = 8;
225
- const filled = Math.round((percent / 100) * barWidth);
226
- const empty = barWidth - filled;
227
- const miniBar = c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
228
-
229
- rows.push({
230
- name: truncate(project.name, 20),
231
- progress: `${miniBar} ${pad(String(percent) + "%", 4, "right")}`,
232
- ready: depMetrics.readyToWork,
233
- blocked: depMetrics.blockedByDependencies,
234
- total: activeTasks,
235
- });
236
- }
237
-
238
- const columns: TableColumn[] = [
239
- { header: "Project", key: "name", width: 22 },
240
- { header: "Progress", key: "progress", width: 16 },
241
- { header: "Tasks", key: "total", width: 6, align: "right" },
242
- { header: "Ready", key: "ready", width: 6, align: "right", format: (v) => c.green(String(v)) },
243
- { header: "Blocked", key: "blocked", width: 8, align: "right", format: (v) => Number(v) > 0 ? c.red(String(v)) : c.gray(String(v)) },
244
- ];
245
-
246
- return table(rows as unknown as Record<string, unknown>[], columns);
247
- }
248
-
249
23
  // =============================================================================
250
24
  // Main Dashboard
251
25
  // =============================================================================
252
26
 
253
27
  export async function dashboard(projectId?: string): Promise<void> {
254
- // Print banner
255
- console.log();
256
- console.log(banner("TASK MCP"));
257
- console.log();
258
-
259
28
  // Get projects and tasks
260
29
  const projects = await listProjects();
261
30
 
@@ -283,62 +52,34 @@ export async function dashboard(projectId?: string): Promise<void> {
283
52
  tasks = await listAllTasks();
284
53
  }
285
54
 
286
- // Project info header
287
- const projectInfo = project
288
- ? `${c.bold("Project:")} ${project.name}`
289
- : `${c.bold("All Projects")} (${projects.length} projects)`;
290
-
291
- console.log(c.dim(`v${VERSION} ${projectInfo}`));
292
- console.log();
293
-
294
- // Render widgets
295
- const statusWidget = renderStatusWidget(tasks, projects);
296
- const actionsWidget = await renderActionsWidget(tasks);
297
-
298
- // Print widgets side by side
299
- console.log(sideBySide([statusWidget, actionsWidget], 2));
300
- console.log();
55
+ // Get inbox items
56
+ const inboxItems = await listInboxItems("pending");
301
57
 
302
- // Inbox widget (if there are pending items)
303
- const inboxWidget = await renderInboxWidget();
304
- if (inboxWidget) {
305
- console.log(inboxWidget);
306
- console.log();
58
+ // Create task lookup for projects table
59
+ const tasksByProject = new Map<string, Task[]>();
60
+ for (const p of projects) {
61
+ tasksByProject.set(p.id, await listTasks(p.id));
307
62
  }
308
-
309
- // Projects table (only if showing all projects)
310
- if (!project && projects.length > 1) {
311
- console.log(c.bold("Projects"));
312
- console.log();
313
- console.log(await renderProjectsTable(projects));
314
- console.log();
315
- }
316
-
317
- // Task list for single project view
318
- if (project || projects.length === 1) {
319
- const activeTasks = tasks.filter(t => t.status !== "completed" && t.status !== "cancelled");
320
- if (activeTasks.length > 0) {
321
- const displayTasks = activeTasks.slice(0, 10);
322
-
323
- console.log(c.bold(`Tasks (${activeTasks.length})`));
324
- console.log();
325
-
326
- const columns: TableColumn[] = [
327
- { header: "Title", key: "title", width: 40 },
328
- { header: "Status", key: "status", width: 12, format: (v) => {
329
- const icon = icons[v as Task["status"]] ?? icons.pending;
330
- return `${icon} ${v}`;
331
- }},
332
- { header: "Priority", key: "priority", width: 10, format: (v) => formatPriority(v as Task["priority"]) },
333
- ];
334
-
335
- console.log(table(displayTasks as unknown as Record<string, unknown>[], columns));
336
-
337
- if (activeTasks.length > 10) {
338
- console.log(c.gray(`\n(+${activeTasks.length - 10} more tasks)`));
339
- }
340
- }
63
+ const getProjectTasks = (pid: string) => tasksByProject.get(pid) ?? [];
64
+
65
+ // Render dashboard using shared renderer
66
+ // Cast types - CLI types are compatible subset of shared types
67
+ let output: string;
68
+ if (project) {
69
+ output = renderProjectDashboard(
70
+ project as SharedProject,
71
+ tasks as SharedTask[],
72
+ { version: VERSION }
73
+ );
74
+ } else {
75
+ output = renderGlobalDashboard(
76
+ projects as SharedProject[],
77
+ tasks as SharedTask[],
78
+ inboxItems,
79
+ (pid) => (getProjectTasks(pid) as SharedTask[]),
80
+ { version: VERSION }
81
+ );
341
82
  }
342
83
 
343
- console.log();
84
+ console.log(output);
344
85
  }
package/src/storage.ts CHANGED
@@ -27,7 +27,7 @@ export interface Task {
27
27
  priority: "critical" | "high" | "medium" | "low";
28
28
  projectId: string;
29
29
  parentId?: string;
30
- dependencies?: { taskId: string; type: string; reason?: string }[];
30
+ dependencies?: { taskId: string; type: "blocks" | "blocked_by" | "related"; reason?: string }[];
31
31
  dueDate?: string;
32
32
  createdAt: string;
33
33
  updatedAt: string;
@@ -43,7 +43,7 @@ export interface Project {
43
43
  name: string;
44
44
  description?: string;
45
45
  status: "active" | "on_hold" | "completed" | "archived";
46
- defaultPriority?: string;
46
+ defaultPriority?: "critical" | "high" | "medium" | "low";
47
47
  createdAt: string;
48
48
  updatedAt: string;
49
49
  targetDate?: string;