@task-mcp/shared 1.0.20 → 1.0.22

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.
Files changed (175) hide show
  1. package/README.md +122 -0
  2. package/package.json +1 -6
  3. package/src/algorithms/critical-path.ts +31 -6
  4. package/src/algorithms/dependency-integrity.ts +5 -2
  5. package/src/algorithms/topological-sort.ts +66 -17
  6. package/src/utils/index.ts +5 -5
  7. package/src/utils/natural-language.ts +210 -83
  8. package/src/utils/projection.ts +8 -8
  9. package/src/utils/workspace.ts +16 -4
  10. package/dist/algorithms/critical-path.d.ts +0 -46
  11. package/dist/algorithms/critical-path.d.ts.map +0 -1
  12. package/dist/algorithms/critical-path.js +0 -320
  13. package/dist/algorithms/critical-path.js.map +0 -1
  14. package/dist/algorithms/critical-path.test.d.ts +0 -2
  15. package/dist/algorithms/critical-path.test.d.ts.map +0 -1
  16. package/dist/algorithms/critical-path.test.js +0 -194
  17. package/dist/algorithms/critical-path.test.js.map +0 -1
  18. package/dist/algorithms/dependency-integrity.d.ts +0 -81
  19. package/dist/algorithms/dependency-integrity.d.ts.map +0 -1
  20. package/dist/algorithms/dependency-integrity.js +0 -207
  21. package/dist/algorithms/dependency-integrity.js.map +0 -1
  22. package/dist/algorithms/dependency-integrity.test.d.ts +0 -2
  23. package/dist/algorithms/dependency-integrity.test.d.ts.map +0 -1
  24. package/dist/algorithms/dependency-integrity.test.js +0 -309
  25. package/dist/algorithms/dependency-integrity.test.js.map +0 -1
  26. package/dist/algorithms/index.d.ts +0 -5
  27. package/dist/algorithms/index.d.ts.map +0 -1
  28. package/dist/algorithms/index.js +0 -5
  29. package/dist/algorithms/index.js.map +0 -1
  30. package/dist/algorithms/tech-analysis.d.ts +0 -106
  31. package/dist/algorithms/tech-analysis.d.ts.map +0 -1
  32. package/dist/algorithms/tech-analysis.js +0 -344
  33. package/dist/algorithms/tech-analysis.js.map +0 -1
  34. package/dist/algorithms/tech-analysis.test.d.ts +0 -2
  35. package/dist/algorithms/tech-analysis.test.d.ts.map +0 -1
  36. package/dist/algorithms/tech-analysis.test.js +0 -338
  37. package/dist/algorithms/tech-analysis.test.js.map +0 -1
  38. package/dist/algorithms/topological-sort.d.ts +0 -41
  39. package/dist/algorithms/topological-sort.d.ts.map +0 -1
  40. package/dist/algorithms/topological-sort.js +0 -165
  41. package/dist/algorithms/topological-sort.js.map +0 -1
  42. package/dist/algorithms/topological-sort.test.d.ts +0 -2
  43. package/dist/algorithms/topological-sort.test.d.ts.map +0 -1
  44. package/dist/algorithms/topological-sort.test.js +0 -162
  45. package/dist/algorithms/topological-sort.test.js.map +0 -1
  46. package/dist/index.d.ts +0 -4
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/index.js +0 -7
  49. package/dist/index.js.map +0 -1
  50. package/dist/schemas/inbox.d.ts +0 -55
  51. package/dist/schemas/inbox.d.ts.map +0 -1
  52. package/dist/schemas/inbox.js +0 -25
  53. package/dist/schemas/inbox.js.map +0 -1
  54. package/dist/schemas/index.d.ts +0 -7
  55. package/dist/schemas/index.d.ts.map +0 -1
  56. package/dist/schemas/index.js +0 -17
  57. package/dist/schemas/index.js.map +0 -1
  58. package/dist/schemas/project.d.ts +0 -177
  59. package/dist/schemas/project.d.ts.map +0 -1
  60. package/dist/schemas/project.js +0 -56
  61. package/dist/schemas/project.js.map +0 -1
  62. package/dist/schemas/response-format.d.ts +0 -148
  63. package/dist/schemas/response-format.d.ts.map +0 -1
  64. package/dist/schemas/response-format.js +0 -18
  65. package/dist/schemas/response-format.js.map +0 -1
  66. package/dist/schemas/response-schema.d.ts +0 -307
  67. package/dist/schemas/response-schema.d.ts.map +0 -1
  68. package/dist/schemas/response-schema.js +0 -75
  69. package/dist/schemas/response-schema.js.map +0 -1
  70. package/dist/schemas/response-schema.test.d.ts +0 -2
  71. package/dist/schemas/response-schema.test.d.ts.map +0 -1
  72. package/dist/schemas/response-schema.test.js +0 -256
  73. package/dist/schemas/response-schema.test.js.map +0 -1
  74. package/dist/schemas/state.d.ts +0 -17
  75. package/dist/schemas/state.d.ts.map +0 -1
  76. package/dist/schemas/state.js +0 -17
  77. package/dist/schemas/state.js.map +0 -1
  78. package/dist/schemas/task.d.ts +0 -881
  79. package/dist/schemas/task.d.ts.map +0 -1
  80. package/dist/schemas/task.js +0 -189
  81. package/dist/schemas/task.js.map +0 -1
  82. package/dist/schemas/view.d.ts +0 -143
  83. package/dist/schemas/view.d.ts.map +0 -1
  84. package/dist/schemas/view.js +0 -48
  85. package/dist/schemas/view.js.map +0 -1
  86. package/dist/utils/dashboard-renderer.d.ts +0 -93
  87. package/dist/utils/dashboard-renderer.d.ts.map +0 -1
  88. package/dist/utils/dashboard-renderer.js +0 -424
  89. package/dist/utils/dashboard-renderer.js.map +0 -1
  90. package/dist/utils/dashboard-renderer.test.d.ts +0 -2
  91. package/dist/utils/dashboard-renderer.test.d.ts.map +0 -1
  92. package/dist/utils/dashboard-renderer.test.js +0 -774
  93. package/dist/utils/dashboard-renderer.test.js.map +0 -1
  94. package/dist/utils/date.d.ts +0 -94
  95. package/dist/utils/date.d.ts.map +0 -1
  96. package/dist/utils/date.js +0 -323
  97. package/dist/utils/date.js.map +0 -1
  98. package/dist/utils/date.test.d.ts +0 -2
  99. package/dist/utils/date.test.d.ts.map +0 -1
  100. package/dist/utils/date.test.js +0 -276
  101. package/dist/utils/date.test.js.map +0 -1
  102. package/dist/utils/hierarchy.d.ts +0 -102
  103. package/dist/utils/hierarchy.d.ts.map +0 -1
  104. package/dist/utils/hierarchy.js +0 -236
  105. package/dist/utils/hierarchy.js.map +0 -1
  106. package/dist/utils/hierarchy.test.d.ts +0 -2
  107. package/dist/utils/hierarchy.test.d.ts.map +0 -1
  108. package/dist/utils/hierarchy.test.js +0 -436
  109. package/dist/utils/hierarchy.test.js.map +0 -1
  110. package/dist/utils/id.d.ts +0 -60
  111. package/dist/utils/id.d.ts.map +0 -1
  112. package/dist/utils/id.js +0 -118
  113. package/dist/utils/id.js.map +0 -1
  114. package/dist/utils/id.test.d.ts +0 -2
  115. package/dist/utils/id.test.d.ts.map +0 -1
  116. package/dist/utils/id.test.js +0 -193
  117. package/dist/utils/id.test.js.map +0 -1
  118. package/dist/utils/index.d.ts +0 -12
  119. package/dist/utils/index.d.ts.map +0 -1
  120. package/dist/utils/index.js +0 -34
  121. package/dist/utils/index.js.map +0 -1
  122. package/dist/utils/natural-language.d.ts +0 -57
  123. package/dist/utils/natural-language.d.ts.map +0 -1
  124. package/dist/utils/natural-language.js +0 -211
  125. package/dist/utils/natural-language.js.map +0 -1
  126. package/dist/utils/natural-language.test.d.ts +0 -2
  127. package/dist/utils/natural-language.test.d.ts.map +0 -1
  128. package/dist/utils/natural-language.test.js +0 -197
  129. package/dist/utils/natural-language.test.js.map +0 -1
  130. package/dist/utils/priority-queue.d.ts +0 -17
  131. package/dist/utils/priority-queue.d.ts.map +0 -1
  132. package/dist/utils/priority-queue.js +0 -62
  133. package/dist/utils/priority-queue.js.map +0 -1
  134. package/dist/utils/priority-queue.test.d.ts +0 -2
  135. package/dist/utils/priority-queue.test.d.ts.map +0 -1
  136. package/dist/utils/priority-queue.test.js +0 -82
  137. package/dist/utils/priority-queue.test.js.map +0 -1
  138. package/dist/utils/projection.d.ts +0 -65
  139. package/dist/utils/projection.d.ts.map +0 -1
  140. package/dist/utils/projection.js +0 -180
  141. package/dist/utils/projection.js.map +0 -1
  142. package/dist/utils/projection.test.d.ts +0 -2
  143. package/dist/utils/projection.test.d.ts.map +0 -1
  144. package/dist/utils/projection.test.js +0 -336
  145. package/dist/utils/projection.test.js.map +0 -1
  146. package/dist/utils/terminal-ui.d.ts +0 -208
  147. package/dist/utils/terminal-ui.d.ts.map +0 -1
  148. package/dist/utils/terminal-ui.js +0 -611
  149. package/dist/utils/terminal-ui.js.map +0 -1
  150. package/dist/utils/terminal-ui.test.d.ts +0 -2
  151. package/dist/utils/terminal-ui.test.d.ts.map +0 -1
  152. package/dist/utils/terminal-ui.test.js +0 -683
  153. package/dist/utils/terminal-ui.test.js.map +0 -1
  154. package/dist/utils/workspace.d.ts +0 -100
  155. package/dist/utils/workspace.d.ts.map +0 -1
  156. package/dist/utils/workspace.js +0 -173
  157. package/dist/utils/workspace.js.map +0 -1
  158. package/dist/utils/workspace.test.d.ts +0 -2
  159. package/dist/utils/workspace.test.d.ts.map +0 -1
  160. package/dist/utils/workspace.test.js +0 -97
  161. package/dist/utils/workspace.test.js.map +0 -1
  162. package/src/algorithms/critical-path.test.ts +0 -241
  163. package/src/algorithms/dependency-integrity.test.ts +0 -348
  164. package/src/algorithms/tech-analysis.test.ts +0 -413
  165. package/src/algorithms/topological-sort.test.ts +0 -190
  166. package/src/schemas/response-schema.test.ts +0 -314
  167. package/src/utils/dashboard-renderer.test.ts +0 -983
  168. package/src/utils/date.test.ts +0 -329
  169. package/src/utils/hierarchy.test.ts +0 -505
  170. package/src/utils/id.test.ts +0 -235
  171. package/src/utils/natural-language.test.ts +0 -242
  172. package/src/utils/priority-queue.test.ts +0 -103
  173. package/src/utils/projection.test.ts +0 -425
  174. package/src/utils/terminal-ui.test.ts +0 -831
  175. package/src/utils/workspace.test.ts +0 -125
@@ -30,6 +30,170 @@ function validateInputLength(input: string): void {
30
30
  }
31
31
  }
32
32
 
33
+ /**
34
+ * Extracted metadata from natural language input
35
+ */
36
+ export interface ExtractedMetadata {
37
+ /** Extracted priority (!high, !medium, !low, !critical) */
38
+ priority?: Priority;
39
+ /** Extracted due date (today, tomorrow, next week, etc.) */
40
+ dueDate?: string;
41
+ /** Extracted tags (#tag) */
42
+ tags?: string[];
43
+ /** Extracted estimated time in minutes (~2h, ~30m) */
44
+ estimateMinutes?: number;
45
+ /** Remaining text after metadata extraction */
46
+ remaining: string;
47
+ }
48
+
49
+ /**
50
+ * Options for metadata extraction
51
+ */
52
+ export interface ExtractMetadataOptions {
53
+ /** Extract priority markers (!high, !low, etc.) */
54
+ extractPriority?: boolean;
55
+ /** Extract date expressions (today, tomorrow, by Friday, etc.) */
56
+ extractDate?: boolean;
57
+ /** Extract hashtags (#tag) */
58
+ extractTags?: boolean;
59
+ /** Extract time estimates (~2h, ~30m) */
60
+ extractEstimate?: boolean;
61
+ }
62
+
63
+ const DEFAULT_EXTRACT_OPTIONS: ExtractMetadataOptions = {
64
+ extractPriority: true,
65
+ extractDate: true,
66
+ extractTags: true,
67
+ extractEstimate: true,
68
+ };
69
+
70
+ /**
71
+ * Extract metadata from natural language input
72
+ *
73
+ * Supports:
74
+ * - Priority: !high, !medium, !low, !critical, !높음, !보통, !낮음, !긴급
75
+ * - Date: today, tomorrow, next week, 내일, 오늘, by Friday, 금요일까지
76
+ * - Tags: #tag, #개발, #backend
77
+ * - Time estimate: ~2h, ~30m, ~1h30m (with ~ prefix)
78
+ *
79
+ * @param input - Raw natural language input
80
+ * @param options - Options to control which metadata types to extract
81
+ * @returns Extracted metadata and remaining text
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const result = extractMetadata("Review PR tomorrow #dev !high ~2h");
86
+ * // {
87
+ * // priority: "high",
88
+ * // dueDate: "2025-01-01",
89
+ * // tags: ["dev"],
90
+ * // estimateMinutes: 120,
91
+ * // remaining: "Review PR"
92
+ * // }
93
+ * ```
94
+ */
95
+ export function extractMetadata(
96
+ input: string,
97
+ options: ExtractMetadataOptions = DEFAULT_EXTRACT_OPTIONS
98
+ ): ExtractedMetadata {
99
+ validateInputLength(input);
100
+ let remaining = input.trim();
101
+ const result: ExtractedMetadata = { remaining: "" };
102
+
103
+ // Extract priority (!high, !critical, !medium, !low, !높음, !보통, !낮음)
104
+ if (options.extractPriority !== false) {
105
+ const priorityMatch = remaining.match(/!([\p{L}\p{N}_]+)/gu);
106
+ if (priorityMatch) {
107
+ for (const match of priorityMatch) {
108
+ const priority = parsePriority(match.slice(1));
109
+ if (priority) {
110
+ result.priority = priority;
111
+ remaining = remaining.replace(match, "").trim();
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ // Extract tags (#dev, #backend, #개발)
119
+ if (options.extractTags !== false) {
120
+ const tagMatches = remaining.match(/#([\p{L}\p{N}_]+)/gu);
121
+ if (tagMatches) {
122
+ result.tags = tagMatches.map((m) => m.slice(1));
123
+ for (const match of tagMatches) {
124
+ remaining = remaining.replace(match, "").trim();
125
+ }
126
+ }
127
+ }
128
+
129
+ // Extract due date patterns
130
+ if (options.extractDate !== false) {
131
+ // "by Friday", "by tomorrow", "until next week"
132
+ const byMatch = remaining.match(/\b(by|until|before)\s+(\w+(\s+\w+)?)/i);
133
+ if (byMatch) {
134
+ const dateStr = byMatch[2]!;
135
+ const date = parseRelativeDate(dateStr);
136
+ if (date) {
137
+ result.dueDate = formatDate(date);
138
+ remaining = remaining.replace(byMatch[0], "").trim();
139
+ }
140
+ }
141
+
142
+ // Korean date patterns: "내일까지", "금요일까지"
143
+ if (!result.dueDate) {
144
+ const koreanDueMatch = remaining.match(/(\S+)까지/);
145
+ if (koreanDueMatch) {
146
+ const dateStr = koreanDueMatch[1]!;
147
+ const date = parseRelativeDate(dateStr);
148
+ if (date) {
149
+ result.dueDate = formatDate(date);
150
+ remaining = remaining.replace(koreanDueMatch[0], "").trim();
151
+ }
152
+ }
153
+ }
154
+
155
+ // "tomorrow", "today" at the end
156
+ if (!result.dueDate) {
157
+ const dateWords = ["tomorrow", "today", "내일", "오늘", "모레"];
158
+ for (const word of dateWords) {
159
+ if (remaining.toLowerCase().endsWith(word)) {
160
+ const date = parseRelativeDate(word);
161
+ if (date) {
162
+ result.dueDate = formatDate(date);
163
+ remaining = remaining.slice(0, -word.length).trim();
164
+ break;
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ // Extract time estimate (~2h, ~30m, ~1h30m)
172
+ if (options.extractEstimate !== false) {
173
+ const timeMatch = remaining.match(/~(\d+h\d+m|\d+h|\d+m)\b/);
174
+ if (timeMatch) {
175
+ let minutes = 0;
176
+ const hoursMatch = timeMatch[1]!.match(/(\d+)h/);
177
+ const minsMatch = timeMatch[1]!.match(/(\d+)m/);
178
+ if (hoursMatch) {
179
+ minutes += parseInt(hoursMatch[1]!, 10) * 60;
180
+ }
181
+ if (minsMatch) {
182
+ minutes += parseInt(minsMatch[1]!, 10);
183
+ }
184
+ if (minutes > 0) {
185
+ result.estimateMinutes = minutes;
186
+ remaining = remaining.replace(timeMatch[0], "").trim();
187
+ }
188
+ }
189
+ }
190
+
191
+ // Clean up extra spaces
192
+ result.remaining = remaining.replace(/\s+/g, " ").trim();
193
+
194
+ return result;
195
+ }
196
+
33
197
  /**
34
198
  * Target type for parsed input
35
199
  */
@@ -94,21 +258,21 @@ export function parseInput(input: string): ParsedInput {
94
258
  * - "성능 개선 아이디어 #performance"
95
259
  */
96
260
  export function parseInboxInput(input: string): InboxCreateInput {
97
- validateInputLength(input);
98
- let remaining = input.trim();
261
+ // Inbox only extracts tags
262
+ const metadata = extractMetadata(input, {
263
+ extractPriority: false,
264
+ extractDate: false,
265
+ extractTags: true,
266
+ extractEstimate: false,
267
+ });
268
+
99
269
  const result: InboxCreateInput = { content: "" };
100
270
 
101
- // Extract tags (#dev, #backend, #개발)
102
- const tagMatches = remaining.match(/#([\p{L}\p{N}_]+)/gu);
103
- if (tagMatches) {
104
- result.tags = tagMatches.map((m) => m.slice(1));
105
- for (const match of tagMatches) {
106
- remaining = remaining.replace(match, "").trim();
107
- }
271
+ if (metadata.tags) {
272
+ result.tags = metadata.tags;
108
273
  }
109
274
 
110
- // Clean up and set content
111
- result.content = remaining.replace(/\s+/g, " ").trim();
275
+ result.content = metadata.remaining;
112
276
 
113
277
  if (!result.content) {
114
278
  throw new InputValidationError(
@@ -123,7 +287,7 @@ export function parseInboxInput(input: string): InboxCreateInput {
123
287
  * Parse natural language task input
124
288
  *
125
289
  * Examples:
126
- * - "Review PR tomorrow #dev !high"
290
+ * - "Review PR tomorrow #dev !high ~2h"
127
291
  * - "내일까지 보고서 작성 #업무 !높음 @집중"
128
292
  * - "Fix bug by Friday #backend !critical"
129
293
  * - "Write tests every Monday #testing"
@@ -133,20 +297,7 @@ export function parseTaskInput(input: string): TaskCreateInput {
133
297
  let remaining = input.trim();
134
298
  const result: TaskCreateInput = { title: "" };
135
299
 
136
- // Extract priority (!high, !critical, !medium, !low, !높음, !보통, !낮음)
137
- const priorityMatch = remaining.match(/!([\p{L}\p{N}_]+)/gu);
138
- if (priorityMatch) {
139
- for (const match of priorityMatch) {
140
- const priority = parsePriority(match.slice(1));
141
- if (priority) {
142
- result.priority = priority;
143
- remaining = remaining.replace(match, "").trim();
144
- break;
145
- }
146
- }
147
- }
148
-
149
- // Extract contexts (@focus, @review, @집중)
300
+ // Extract contexts first (@focus, @review, @집중) - task-specific
150
301
  const contextMatches = remaining.match(/@([\p{L}\p{N}_]+)/gu);
151
302
  if (contextMatches) {
152
303
  result.contexts = contextMatches.map((m) => m.slice(1));
@@ -155,57 +306,20 @@ export function parseTaskInput(input: string): TaskCreateInput {
155
306
  }
156
307
  }
157
308
 
158
- // Extract tags (#dev, #backend, #개발)
159
- const tagMatches = remaining.match(/#([\p{L}\p{N}_]+)/gu);
160
- if (tagMatches) {
161
- result.tags = tagMatches.map((m) => m.slice(1));
162
- for (const match of tagMatches) {
163
- remaining = remaining.replace(match, "").trim();
164
- }
165
- }
166
-
167
- // Extract due date patterns
168
- // "by Friday", "by tomorrow", "until next week"
169
- const byMatch = remaining.match(/\b(by|until|before)\s+(\w+(\s+\w+)?)/i);
170
- if (byMatch) {
171
- const dateStr = byMatch[2]!;
172
- const date = parseRelativeDate(dateStr);
173
- if (date) {
174
- result.dueDate = formatDate(date);
175
- remaining = remaining.replace(byMatch[0], "").trim();
176
- }
177
- }
178
-
179
- // Korean date patterns: "내일까지", "금요일까지"
180
- const koreanDueMatch = remaining.match(/(\S+)까지/);
181
- if (koreanDueMatch && !result.dueDate) {
182
- const dateStr = koreanDueMatch[1]!;
183
- const date = parseRelativeDate(dateStr);
184
- if (date) {
185
- result.dueDate = formatDate(date);
186
- remaining = remaining.replace(koreanDueMatch[0], "").trim();
187
- }
188
- }
189
-
190
- // "tomorrow", "today" at the end
191
- const dateWords = ["tomorrow", "today", "내일", "오늘", "모레"];
192
- for (const word of dateWords) {
193
- if (remaining.toLowerCase().endsWith(word) && !result.dueDate) {
194
- const date = parseRelativeDate(word);
195
- if (date) {
196
- result.dueDate = formatDate(date);
197
- remaining = remaining.slice(0, -word.length).trim();
198
- }
199
- }
309
+ // Extract sortOrder (^1, ^10) - task-specific
310
+ const sortMatch = remaining.match(/\^(\d+)/);
311
+ if (sortMatch) {
312
+ result.sortOrder = parseInt(sortMatch[1]!, 10);
313
+ remaining = remaining.replace(sortMatch[0], "").trim();
200
314
  }
201
315
 
202
- // Extract time estimate (30m, 2h, 1h30m)
203
- // Pattern matches: "2h30m", "2h", "30m" (but not empty string)
204
- const timeMatch = remaining.match(/\b(\d+h\d+m|\d+h|\d+m)\b/);
205
- if (timeMatch) {
316
+ // Extract time estimate without ~ prefix for backward compatibility (30m, 2h, 1h30m)
317
+ // This handles the legacy format before extractMetadata handles ~prefix format
318
+ const legacyTimeMatch = remaining.match(/\b(\d+h\d+m|\d+h|\d+m)\b/);
319
+ if (legacyTimeMatch && !remaining.includes("~" + legacyTimeMatch[1])) {
206
320
  let minutes = 0;
207
- const hoursMatch = timeMatch[0].match(/(\d+)h/);
208
- const minsMatch = timeMatch[0].match(/(\d+)m/);
321
+ const hoursMatch = legacyTimeMatch[0].match(/(\d+)h/);
322
+ const minsMatch = legacyTimeMatch[0].match(/(\d+)m/);
209
323
  if (hoursMatch) {
210
324
  minutes += parseInt(hoursMatch[1]!, 10) * 60;
211
325
  }
@@ -214,20 +328,33 @@ export function parseTaskInput(input: string): TaskCreateInput {
214
328
  }
215
329
  if (minutes > 0) {
216
330
  result.estimate = { expected: minutes, confidence: "medium" };
217
- remaining = remaining.replace(timeMatch[0], "").trim();
331
+ remaining = remaining.replace(legacyTimeMatch[0], "").trim();
218
332
  }
219
333
  }
220
334
 
221
- // Extract sortOrder (^1, ^10, ^순서1)
222
- // Used for manual ordering within task lists
223
- const sortMatch = remaining.match(/\^(\d+)/);
224
- if (sortMatch) {
225
- result.sortOrder = parseInt(sortMatch[1]!, 10);
226
- remaining = remaining.replace(sortMatch[0], "").trim();
335
+ // Use extractMetadata for common patterns
336
+ const metadata = extractMetadata(remaining, {
337
+ extractPriority: true,
338
+ extractDate: true,
339
+ extractTags: true,
340
+ extractEstimate: true,
341
+ });
342
+
343
+ // Apply extracted metadata
344
+ if (metadata.priority) {
345
+ result.priority = metadata.priority;
346
+ }
347
+ if (metadata.dueDate) {
348
+ result.dueDate = metadata.dueDate;
349
+ }
350
+ if (metadata.tags) {
351
+ result.tags = metadata.tags;
352
+ }
353
+ if (metadata.estimateMinutes && !result.estimate) {
354
+ result.estimate = { expected: metadata.estimateMinutes, confidence: "medium" };
227
355
  }
228
356
 
229
- // Clean up extra spaces
230
- result.title = remaining.replace(/\s+/g, " ").trim();
357
+ result.title = metadata.remaining;
231
358
 
232
359
  if (!result.title) {
233
360
  throw new InputValidationError(
@@ -27,7 +27,7 @@ function assertNever(value: never): never {
27
27
  /**
28
28
  * Project a single task to the specified format
29
29
  */
30
- export function projectTask(task: Task, format: ResponseFormat): TaskSummary | TaskPreview | Task {
30
+ export function formatTask(task: Task, format: ResponseFormat): TaskSummary | TaskPreview | Task {
31
31
  switch (format) {
32
32
  case "concise":
33
33
  return {
@@ -59,19 +59,19 @@ export function projectTask(task: Task, format: ResponseFormat): TaskSummary | T
59
59
  /**
60
60
  * Project multiple tasks with optional limit
61
61
  */
62
- export function projectTasks(
62
+ export function formatTasks(
63
63
  tasks: Task[],
64
64
  format: ResponseFormat,
65
65
  limit?: number
66
66
  ): (TaskSummary | TaskPreview | Task)[] {
67
67
  const sliced = limit ? tasks.slice(0, limit) : tasks;
68
- return sliced.map((task) => projectTask(task, format));
68
+ return sliced.map((task) => formatTask(task, format));
69
69
  }
70
70
 
71
71
  /**
72
72
  * Project tasks with pagination
73
73
  */
74
- export function projectTasksPaginated(
74
+ export function formatTasksPaginated(
75
75
  tasks: Task[],
76
76
  format: ResponseFormat,
77
77
  limit: number = 20,
@@ -79,7 +79,7 @@ export function projectTasksPaginated(
79
79
  ): PaginatedResponse<TaskSummary | TaskPreview | Task> {
80
80
  const effectiveLimit = Math.min(limit, 100);
81
81
  const sliced = tasks.slice(offset, offset + effectiveLimit);
82
- const projected = sliced.map((task) => projectTask(task, format));
82
+ const projected = sliced.map((task) => formatTask(task, format));
83
83
 
84
84
  return {
85
85
  items: projected,
@@ -93,7 +93,7 @@ export function projectTasksPaginated(
93
93
  /**
94
94
  * Project a single inbox item to the specified format
95
95
  */
96
- export function projectInboxItem(
96
+ export function formatInboxItem(
97
97
  item: InboxItem,
98
98
  format: ResponseFormat
99
99
  ): InboxSummary | InboxPreview | InboxItem {
@@ -124,13 +124,13 @@ export function projectInboxItem(
124
124
  /**
125
125
  * Project multiple inbox items with optional limit
126
126
  */
127
- export function projectInboxItems(
127
+ export function formatInboxItems(
128
128
  items: InboxItem[],
129
129
  format: ResponseFormat,
130
130
  limit?: number
131
131
  ): (InboxSummary | InboxPreview | InboxItem)[] {
132
132
  const sliced = limit ? items.slice(0, limit) : items;
133
- return sliced.map((item) => projectInboxItem(item, format));
133
+ return sliced.map((item) => formatInboxItem(item, format));
134
134
  }
135
135
 
136
136
  /**
@@ -7,12 +7,18 @@
7
7
  * 3. Current working directory basename (final fallback)
8
8
  */
9
9
 
10
- import { exec } from "node:child_process";
10
+ import { exec, execSync } from "node:child_process";
11
11
  import { promisify } from "node:util";
12
12
  import { basename } from "node:path";
13
13
 
14
14
  const execAsync = promisify(exec);
15
15
 
16
+ /**
17
+ * Maximum length for workspace names.
18
+ * Prevents excessive path lengths and potential DoS via long names.
19
+ */
20
+ const MAX_WORKSPACE_LENGTH = 64;
21
+
16
22
  /**
17
23
  * Normalize a string to a valid workspace name.
18
24
  *
@@ -21,6 +27,7 @@ const execAsync = promisify(exec);
21
27
  * - Replace spaces with hyphens
22
28
  * - Keep alphanumeric, hyphens, and underscores
23
29
  * - Trim leading/trailing hyphens
30
+ * - Limit to MAX_WORKSPACE_LENGTH characters
24
31
  *
25
32
  * @param name - Raw name to normalize
26
33
  * @returns Normalized workspace name
@@ -29,15 +36,19 @@ const execAsync = promisify(exec);
29
36
  * ```typescript
30
37
  * normalizeWorkspace('My Project'); // 'my-project'
31
38
  * normalizeWorkspace('Task-MCP'); // 'task-mcp'
39
+ * normalizeWorkspace('a'.repeat(100)); // 64 chars max
32
40
  * ```
33
41
  */
34
42
  export function normalizeWorkspace(name: string): string {
35
- return name
43
+ const normalized = name
36
44
  .toLowerCase()
37
45
  .replace(/\s+/g, "-")
38
46
  .replace(/[^a-z0-9_-]/g, "")
39
47
  .replace(/^-+|-+$/g, "")
40
- || "default";
48
+ .slice(0, MAX_WORKSPACE_LENGTH) // Enforce length limit
49
+ .replace(/-+$/, ""); // Clean trailing hyphens after truncation
50
+
51
+ return normalized || "default";
41
52
  }
42
53
 
43
54
  /**
@@ -66,11 +77,12 @@ export async function getGitRepoRoot(cwd?: string): Promise<string | null> {
66
77
  */
67
78
  export function getGitRepoRootSync(cwd?: string): string | null {
68
79
  try {
69
- const { execSync } = require("node:child_process");
70
80
  const result = execSync("git rev-parse --show-toplevel", {
71
81
  cwd: cwd ?? process.cwd(),
72
82
  encoding: "utf-8",
73
83
  stdio: ["pipe", "pipe", "pipe"],
84
+ timeout: 5000, // 5 second timeout
85
+ maxBuffer: 1024, // Limit output buffer
74
86
  });
75
87
  return (result as string).trim() || null;
76
88
  } catch {
@@ -1,46 +0,0 @@
1
- import type { Task } from "../schemas/task.js";
2
- /**
3
- * Task with computed CPM (Critical Path Method) values
4
- */
5
- export interface CPMTask extends Task {
6
- earliestStart: number;
7
- earliestFinish: number;
8
- latestStart: number;
9
- latestFinish: number;
10
- slack: number;
11
- isCritical: boolean;
12
- dependentCount: number;
13
- }
14
- /**
15
- * Result of critical path analysis
16
- */
17
- export interface CPMResult {
18
- tasks: CPMTask[];
19
- criticalPath: CPMTask[];
20
- projectDuration: number;
21
- bottlenecks: CPMTask[];
22
- }
23
- /**
24
- * Perform Critical Path Method analysis
25
- *
26
- * CPM calculates:
27
- * - Earliest Start (ES): Earliest a task can start
28
- * - Earliest Finish (EF): ES + duration
29
- * - Latest Finish (LF): Latest a task can finish without delaying project
30
- * - Latest Start (LS): LF - duration
31
- * - Slack: LS - ES (or LF - EF)
32
- * - Critical Path: Tasks with slack = 0
33
- */
34
- export declare function criticalPathAnalysis(tasks: Task[]): CPMResult;
35
- /**
36
- * Find tasks that can be executed in parallel (no dependencies between them)
37
- */
38
- export declare function findParallelTasks(tasks: Task[]): Task[][];
39
- /**
40
- * Suggest the next best task to work on
41
- */
42
- export declare function suggestNextTask(tasks: Task[], options?: {
43
- contexts?: string[];
44
- maxMinutes?: number;
45
- }): Task | null;
46
- //# sourceMappingURL=critical-path.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"critical-path.d.ts","sourceRoot":"","sources":["../../src/algorithms/critical-path.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAO/C;;GAEG;AACH,MAAM,WAAW,OAAQ,SAAQ,IAAI;IAEnC,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,YAAY,EAAE,OAAO,EAAE,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,OAAO,EAAE,CAAC;CACxB;AA8ND;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,SAAS,CAmD7D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,CAiDzD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CAChB,GACL,IAAI,GAAG,IAAI,CAyDb"}