@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 +3 -3
- package/src/ansi.ts +49 -452
- package/src/commands/dashboard.ts +33 -292
- package/src/storage.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@task-mcp/cli",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
34
|
-
"@task-mcp/shared": "^1.0.
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
//
|
|
287
|
-
const
|
|
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
|
-
//
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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:
|
|
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?:
|
|
46
|
+
defaultPriority?: "critical" | "high" | "medium" | "low";
|
|
47
47
|
createdAt: string;
|
|
48
48
|
updatedAt: string;
|
|
49
49
|
targetDate?: string;
|