@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 +1 -1
- package/src/algorithms/critical-path.ts +14 -34
- package/src/algorithms/dependency-integrity.ts +4 -13
- package/src/algorithms/tech-analysis.ts +27 -27
- package/src/algorithms/topological-sort.ts +6 -10
- package/src/schemas/index.ts +2 -13
- package/src/schemas/response-format.ts +6 -6
- package/src/schemas/response-schema.ts +25 -20
- package/src/schemas/task.ts +4 -22
- package/src/utils/dashboard-renderer.ts +27 -59
- package/src/utils/date.ts +2 -10
- package/src/utils/hierarchy.ts +4 -5
- package/src/utils/index.ts +7 -1
- package/src/utils/terminal-ui.ts +53 -54
package/package.json
CHANGED
|
@@ -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(
|
|
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,
|
|
9
|
-
infra: 0,
|
|
10
|
-
devops: 1,
|
|
11
|
-
backend: 2,
|
|
12
|
-
frontend: 3,
|
|
13
|
-
test: 4,
|
|
14
|
-
docs: 4,
|
|
15
|
-
refactor: 5,
|
|
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 (
|
|
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",
|
|
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;
|
|
321
|
+
low: number; // 1 to LOW_MAX
|
|
318
322
|
medium: number; // LOW_MAX+1 to MEDIUM_MAX
|
|
319
|
-
high: number;
|
|
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(
|
|
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) {
|
package/src/schemas/index.ts
CHANGED
|
@@ -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;
|
|
133
|
-
blocked: number;
|
|
134
|
-
noDeps: number;
|
|
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;
|
|
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;
|
|
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;
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
59
|
+
consequences: z
|
|
60
|
+
.array(z.string())
|
|
61
|
+
.optional()
|
|
59
62
|
.describe("List of effects or changes that will occur"),
|
|
60
|
-
defaultConfirm: z
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
|
|
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>;
|
package/src/schemas/task.ts
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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, {
|
package/src/utils/hierarchy.ts
CHANGED
|
@@ -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(
|
|
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) {
|
package/src/utils/index.ts
CHANGED
|
@@ -31,7 +31,13 @@ export {
|
|
|
31
31
|
type DateParseResult,
|
|
32
32
|
type IsWithinDaysResult,
|
|
33
33
|
} from "./date.js";
|
|
34
|
-
export {
|
|
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,
|
package/src/utils/terminal-ui.ts
CHANGED
|
@@ -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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
483
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|