@task-mcp/shared 0.1.0
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/dist/algorithms/critical-path.d.ts +46 -0
- package/dist/algorithms/critical-path.d.ts.map +1 -0
- package/dist/algorithms/critical-path.js +308 -0
- package/dist/algorithms/critical-path.js.map +1 -0
- package/dist/algorithms/critical-path.test.d.ts +2 -0
- package/dist/algorithms/critical-path.test.d.ts.map +1 -0
- package/dist/algorithms/critical-path.test.js +194 -0
- package/dist/algorithms/critical-path.test.js.map +1 -0
- package/dist/algorithms/index.d.ts +3 -0
- package/dist/algorithms/index.d.ts.map +1 -0
- package/dist/algorithms/index.js +3 -0
- package/dist/algorithms/index.js.map +1 -0
- package/dist/algorithms/topological-sort.d.ts +41 -0
- package/dist/algorithms/topological-sort.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.js +168 -0
- package/dist/algorithms/topological-sort.js.map +1 -0
- package/dist/algorithms/topological-sort.test.d.ts +2 -0
- package/dist/algorithms/topological-sort.test.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.test.js +162 -0
- package/dist/algorithms/topological-sort.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +7 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/project.d.ts +55 -0
- package/dist/schemas/project.d.ts.map +1 -0
- package/dist/schemas/project.js +48 -0
- package/dist/schemas/project.js.map +1 -0
- package/dist/schemas/task.d.ts +124 -0
- package/dist/schemas/task.d.ts.map +1 -0
- package/dist/schemas/task.js +89 -0
- package/dist/schemas/task.js.map +1 -0
- package/dist/schemas/view.d.ts +44 -0
- package/dist/schemas/view.d.ts.map +1 -0
- package/dist/schemas/view.js +33 -0
- package/dist/schemas/view.js.map +1 -0
- package/dist/utils/date.d.ts +25 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +103 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/date.test.d.ts +2 -0
- package/dist/utils/date.test.d.ts.map +1 -0
- package/dist/utils/date.test.js +138 -0
- package/dist/utils/date.test.js.map +1 -0
- package/dist/utils/id.d.ts +27 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +41 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/natural-language.d.ts +12 -0
- package/dist/utils/natural-language.d.ts.map +1 -0
- package/dist/utils/natural-language.js +112 -0
- package/dist/utils/natural-language.js.map +1 -0
- package/dist/utils/natural-language.test.d.ts +2 -0
- package/dist/utils/natural-language.test.d.ts.map +1 -0
- package/dist/utils/natural-language.test.js +132 -0
- package/dist/utils/natural-language.test.js.map +1 -0
- package/package.json +46 -0
- package/src/algorithms/critical-path.test.ts +241 -0
- package/src/algorithms/critical-path.ts +413 -0
- package/src/algorithms/index.ts +17 -0
- package/src/algorithms/topological-sort.test.ts +190 -0
- package/src/algorithms/topological-sort.ts +204 -0
- package/src/index.ts +8 -0
- package/src/schemas/index.ts +30 -0
- package/src/schemas/project.ts +62 -0
- package/src/schemas/task.ts +116 -0
- package/src/schemas/view.ts +46 -0
- package/src/utils/date.test.ts +160 -0
- package/src/utils/date.ts +119 -0
- package/src/utils/id.ts +45 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/natural-language.test.ts +154 -0
- package/src/utils/natural-language.ts +125 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get current ISO timestamp
|
|
3
|
+
*/
|
|
4
|
+
export function now(): string {
|
|
5
|
+
return new Date().toISOString();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse relative date strings
|
|
10
|
+
*/
|
|
11
|
+
export function parseRelativeDate(input: string): Date | null {
|
|
12
|
+
const today = new Date();
|
|
13
|
+
today.setHours(0, 0, 0, 0);
|
|
14
|
+
|
|
15
|
+
const lower = input.toLowerCase().trim();
|
|
16
|
+
|
|
17
|
+
// Today
|
|
18
|
+
if (lower === "today" || lower === "오늘") {
|
|
19
|
+
return today;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Tomorrow
|
|
23
|
+
if (lower === "tomorrow" || lower === "내일") {
|
|
24
|
+
const d = new Date(today);
|
|
25
|
+
d.setDate(d.getDate() + 1);
|
|
26
|
+
return d;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Yesterday
|
|
30
|
+
if (lower === "yesterday" || lower === "어제") {
|
|
31
|
+
const d = new Date(today);
|
|
32
|
+
d.setDate(d.getDate() - 1);
|
|
33
|
+
return d;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Next week
|
|
37
|
+
if (lower === "next week" || lower === "다음주") {
|
|
38
|
+
const d = new Date(today);
|
|
39
|
+
d.setDate(d.getDate() + 7);
|
|
40
|
+
return d;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// In N days
|
|
44
|
+
const inDaysMatch = lower.match(/^in (\d+) days?$/);
|
|
45
|
+
if (inDaysMatch) {
|
|
46
|
+
const d = new Date(today);
|
|
47
|
+
d.setDate(d.getDate() + parseInt(inDaysMatch[1]!, 10));
|
|
48
|
+
return d;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// N일 후
|
|
52
|
+
const koreanDaysMatch = lower.match(/^(\d+)일\s*후$/);
|
|
53
|
+
if (koreanDaysMatch) {
|
|
54
|
+
const d = new Date(today);
|
|
55
|
+
d.setDate(d.getDate() + parseInt(koreanDaysMatch[1]!, 10));
|
|
56
|
+
return d;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Weekday names
|
|
60
|
+
const weekdays = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
61
|
+
const weekdayIndex = weekdays.indexOf(lower);
|
|
62
|
+
if (weekdayIndex !== -1) {
|
|
63
|
+
const d = new Date(today);
|
|
64
|
+
const currentDay = d.getDay();
|
|
65
|
+
const daysUntil = (weekdayIndex - currentDay + 7) % 7 || 7;
|
|
66
|
+
d.setDate(d.getDate() + daysUntil);
|
|
67
|
+
return d;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Try parsing as ISO date
|
|
71
|
+
const parsed = new Date(input);
|
|
72
|
+
if (!isNaN(parsed.getTime())) {
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Format date as YYYY-MM-DD
|
|
81
|
+
*/
|
|
82
|
+
export function formatDate(date: Date): string {
|
|
83
|
+
return date.toISOString().split("T")[0]!;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a date is today
|
|
88
|
+
*/
|
|
89
|
+
export function isToday(date: Date | string): boolean {
|
|
90
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
91
|
+
const today = new Date();
|
|
92
|
+
return (
|
|
93
|
+
d.getFullYear() === today.getFullYear() &&
|
|
94
|
+
d.getMonth() === today.getMonth() &&
|
|
95
|
+
d.getDate() === today.getDate()
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if a date is before today
|
|
101
|
+
*/
|
|
102
|
+
export function isPastDue(date: Date | string): boolean {
|
|
103
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
104
|
+
const today = new Date();
|
|
105
|
+
today.setHours(0, 0, 0, 0);
|
|
106
|
+
return d < today;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a date is within the next N days
|
|
111
|
+
*/
|
|
112
|
+
export function isWithinDays(date: Date | string, days: number): boolean {
|
|
113
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
114
|
+
const today = new Date();
|
|
115
|
+
today.setHours(0, 0, 0, 0);
|
|
116
|
+
const future = new Date(today);
|
|
117
|
+
future.setDate(future.getDate() + days);
|
|
118
|
+
return d >= today && d <= future;
|
|
119
|
+
}
|
package/src/utils/id.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a unique ID with optional prefix
|
|
3
|
+
*/
|
|
4
|
+
export function generateId(prefix: string = ""): string {
|
|
5
|
+
const timestamp = Date.now().toString(36);
|
|
6
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
7
|
+
return prefix ? `${prefix}_${timestamp}${random}` : `${timestamp}${random}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate a task ID
|
|
12
|
+
*/
|
|
13
|
+
export function generateTaskId(): string {
|
|
14
|
+
return generateId("task");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a project ID
|
|
19
|
+
*/
|
|
20
|
+
export function generateProjectId(): string {
|
|
21
|
+
return generateId("proj");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate a view ID
|
|
26
|
+
*/
|
|
27
|
+
export function generateViewId(): string {
|
|
28
|
+
return generateId("view");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate a project ID format
|
|
33
|
+
* @returns true if valid format (proj_[alphanumeric])
|
|
34
|
+
*/
|
|
35
|
+
export function isValidProjectId(id: string): boolean {
|
|
36
|
+
return /^proj_[a-z0-9]+$/.test(id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate a task ID format
|
|
41
|
+
* @returns true if valid format (task_[alphanumeric])
|
|
42
|
+
*/
|
|
43
|
+
export function isValidTaskId(id: string): boolean {
|
|
44
|
+
return /^task_[a-z0-9]+$/.test(id);
|
|
45
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { generateId, generateTaskId, generateProjectId, generateViewId, isValidProjectId, isValidTaskId } from "./id.js";
|
|
2
|
+
export { now, parseRelativeDate, formatDate, isToday, isPastDue, isWithinDays } from "./date.js";
|
|
3
|
+
export { parseTaskInput } from "./natural-language.js";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { parseTaskInput } from "./natural-language.js";
|
|
3
|
+
|
|
4
|
+
describe("parseTaskInput", () => {
|
|
5
|
+
describe("priority parsing", () => {
|
|
6
|
+
test("parses !critical priority", () => {
|
|
7
|
+
const result = parseTaskInput("Fix bug !critical");
|
|
8
|
+
expect(result.priority).toBe("critical");
|
|
9
|
+
expect(result.title).toBe("Fix bug");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("parses !high priority", () => {
|
|
13
|
+
const result = parseTaskInput("Review PR !high");
|
|
14
|
+
expect(result.priority).toBe("high");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("parses !medium priority", () => {
|
|
18
|
+
const result = parseTaskInput("Write docs !medium");
|
|
19
|
+
expect(result.priority).toBe("medium");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("parses !low priority", () => {
|
|
23
|
+
const result = parseTaskInput("Clean up !low");
|
|
24
|
+
expect(result.priority).toBe("low");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("parses Korean priority !높음", () => {
|
|
28
|
+
const result = parseTaskInput("버그 수정 !높음");
|
|
29
|
+
expect(result.priority).toBe("high");
|
|
30
|
+
expect(result.title).toBe("버그 수정");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("parses Korean priority !긴급", () => {
|
|
34
|
+
const result = parseTaskInput("장애 대응 !긴급");
|
|
35
|
+
expect(result.priority).toBe("critical");
|
|
36
|
+
expect(result.title).toBe("장애 대응");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("tag parsing", () => {
|
|
41
|
+
test("parses single tag", () => {
|
|
42
|
+
const result = parseTaskInput("Fix auth #backend");
|
|
43
|
+
expect(result.tags).toEqual(["backend"]);
|
|
44
|
+
expect(result.title).toBe("Fix auth");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("parses multiple tags", () => {
|
|
48
|
+
const result = parseTaskInput("Update API #backend #api #v2");
|
|
49
|
+
expect(result.tags).toEqual(["backend", "api", "v2"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("parses Korean tags", () => {
|
|
53
|
+
const result = parseTaskInput("보고서 작성 #업무 #문서");
|
|
54
|
+
expect(result.tags).toEqual(["업무", "문서"]);
|
|
55
|
+
expect(result.title).toBe("보고서 작성");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("context parsing", () => {
|
|
60
|
+
test("parses single context", () => {
|
|
61
|
+
const result = parseTaskInput("Deep work task @focus");
|
|
62
|
+
expect(result.contexts).toEqual(["focus"]);
|
|
63
|
+
expect(result.title).toBe("Deep work task");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("parses multiple contexts", () => {
|
|
67
|
+
const result = parseTaskInput("Review code @focus @office");
|
|
68
|
+
expect(result.contexts).toEqual(["focus", "office"]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("due date parsing", () => {
|
|
73
|
+
test("parses 'by tomorrow'", () => {
|
|
74
|
+
const result = parseTaskInput("Submit report by tomorrow");
|
|
75
|
+
expect(result.dueDate).toBeDefined();
|
|
76
|
+
expect(result.title).toBe("Submit report");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("parses 'until Friday'", () => {
|
|
80
|
+
const result = parseTaskInput("Complete feature until Friday");
|
|
81
|
+
expect(result.dueDate).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("parses Korean '내일까지'", () => {
|
|
85
|
+
const result = parseTaskInput("보고서 제출 내일까지");
|
|
86
|
+
expect(result.dueDate).toBeDefined();
|
|
87
|
+
expect(result.title).toBe("보고서 제출");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("parses standalone 'tomorrow' at end", () => {
|
|
91
|
+
const result = parseTaskInput("Submit PR tomorrow");
|
|
92
|
+
expect(result.dueDate).toBeDefined();
|
|
93
|
+
expect(result.title).toBe("Submit PR");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("time estimate parsing", () => {
|
|
98
|
+
test("parses hours only", () => {
|
|
99
|
+
const result = parseTaskInput("Write tests 2h");
|
|
100
|
+
expect(result.estimate?.expected).toBe(120);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("parses minutes only", () => {
|
|
104
|
+
const result = parseTaskInput("Quick fix 30m");
|
|
105
|
+
expect(result.estimate?.expected).toBe(30);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("parses combined hours and minutes", () => {
|
|
109
|
+
const result = parseTaskInput("Big refactor 1h30m");
|
|
110
|
+
expect(result.estimate?.expected).toBe(90);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("removes time estimate from title", () => {
|
|
114
|
+
const result = parseTaskInput("Task 2h30m done");
|
|
115
|
+
expect(result.estimate?.expected).toBe(150);
|
|
116
|
+
expect(result.title).toBe("Task done");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("combined parsing", () => {
|
|
121
|
+
test("parses full English input", () => {
|
|
122
|
+
const result = parseTaskInput("Review PR #dev !high @focus");
|
|
123
|
+
expect(result.title).toBe("Review PR");
|
|
124
|
+
expect(result.tags).toEqual(["dev"]);
|
|
125
|
+
expect(result.priority).toBe("high");
|
|
126
|
+
expect(result.contexts).toEqual(["focus"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("parses input with due date", () => {
|
|
130
|
+
const result = parseTaskInput("Submit report by tomorrow #work");
|
|
131
|
+
expect(result.title).toBe("Submit report");
|
|
132
|
+
expect(result.tags).toEqual(["work"]);
|
|
133
|
+
expect(result.dueDate).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("parses full Korean input", () => {
|
|
137
|
+
const result = parseTaskInput("보고서 작성 내일까지 #업무 !높음 @집중");
|
|
138
|
+
expect(result.title).toBe("보고서 작성");
|
|
139
|
+
expect(result.tags).toEqual(["업무"]);
|
|
140
|
+
expect(result.priority).toBe("high");
|
|
141
|
+
expect(result.contexts).toEqual(["집중"]);
|
|
142
|
+
expect(result.dueDate).toBeDefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("returns clean title with no metadata", () => {
|
|
146
|
+
const result = parseTaskInput("Simple task");
|
|
147
|
+
expect(result.title).toBe("Simple task");
|
|
148
|
+
expect(result.tags).toBeUndefined();
|
|
149
|
+
expect(result.priority).toBeUndefined();
|
|
150
|
+
expect(result.contexts).toBeUndefined();
|
|
151
|
+
expect(result.dueDate).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Priority, TaskCreateInput } from "../schemas/task.js";
|
|
2
|
+
import { parseRelativeDate } from "./date.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse natural language task input
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - "Review PR tomorrow #dev !high"
|
|
9
|
+
* - "내일까지 보고서 작성 #업무 !높음 @집중"
|
|
10
|
+
* - "Fix bug by Friday #backend !critical"
|
|
11
|
+
* - "Write tests every Monday #testing"
|
|
12
|
+
*/
|
|
13
|
+
export function parseTaskInput(input: string): TaskCreateInput {
|
|
14
|
+
let remaining = input.trim();
|
|
15
|
+
const result: TaskCreateInput = { title: "" };
|
|
16
|
+
|
|
17
|
+
// Extract priority (!high, !critical, !medium, !low, !높음, !보통, !낮음)
|
|
18
|
+
const priorityMatch = remaining.match(/!([\p{L}\p{N}_]+)/gu);
|
|
19
|
+
if (priorityMatch) {
|
|
20
|
+
for (const match of priorityMatch) {
|
|
21
|
+
const priority = parsePriority(match.slice(1));
|
|
22
|
+
if (priority) {
|
|
23
|
+
result.priority = priority;
|
|
24
|
+
remaining = remaining.replace(match, "").trim();
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extract contexts (@focus, @review, @집중)
|
|
31
|
+
const contextMatches = remaining.match(/@([\p{L}\p{N}_]+)/gu);
|
|
32
|
+
if (contextMatches) {
|
|
33
|
+
result.contexts = contextMatches.map((m) => m.slice(1));
|
|
34
|
+
for (const match of contextMatches) {
|
|
35
|
+
remaining = remaining.replace(match, "").trim();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Extract tags (#dev, #backend, #개발)
|
|
40
|
+
const tagMatches = remaining.match(/#([\p{L}\p{N}_]+)/gu);
|
|
41
|
+
if (tagMatches) {
|
|
42
|
+
result.tags = tagMatches.map((m) => m.slice(1));
|
|
43
|
+
for (const match of tagMatches) {
|
|
44
|
+
remaining = remaining.replace(match, "").trim();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extract due date patterns
|
|
49
|
+
// "by Friday", "by tomorrow", "until next week"
|
|
50
|
+
const byMatch = remaining.match(/\b(by|until|before)\s+(\w+(\s+\w+)?)/i);
|
|
51
|
+
if (byMatch) {
|
|
52
|
+
const dateStr = byMatch[2]!;
|
|
53
|
+
const date = parseRelativeDate(dateStr);
|
|
54
|
+
if (date) {
|
|
55
|
+
result.dueDate = date.toISOString();
|
|
56
|
+
remaining = remaining.replace(byMatch[0], "").trim();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Korean date patterns: "내일까지", "금요일까지"
|
|
61
|
+
const koreanDueMatch = remaining.match(/(\S+)까지/);
|
|
62
|
+
if (koreanDueMatch && !result.dueDate) {
|
|
63
|
+
const dateStr = koreanDueMatch[1]!;
|
|
64
|
+
const date = parseRelativeDate(dateStr);
|
|
65
|
+
if (date) {
|
|
66
|
+
result.dueDate = date.toISOString();
|
|
67
|
+
remaining = remaining.replace(koreanDueMatch[0], "").trim();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// "tomorrow", "today" at the end
|
|
72
|
+
const dateWords = ["tomorrow", "today", "내일", "오늘", "모레"];
|
|
73
|
+
for (const word of dateWords) {
|
|
74
|
+
if (remaining.toLowerCase().endsWith(word) && !result.dueDate) {
|
|
75
|
+
const date = parseRelativeDate(word);
|
|
76
|
+
if (date) {
|
|
77
|
+
result.dueDate = date.toISOString();
|
|
78
|
+
remaining = remaining.slice(0, -word.length).trim();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Extract time estimate (30m, 2h, 1h30m)
|
|
84
|
+
// Pattern matches: "2h30m", "2h", "30m" (but not empty string)
|
|
85
|
+
const timeMatch = remaining.match(/\b(\d+h\d+m|\d+h|\d+m)\b/);
|
|
86
|
+
if (timeMatch) {
|
|
87
|
+
let minutes = 0;
|
|
88
|
+
const hoursMatch = timeMatch[0].match(/(\d+)h/);
|
|
89
|
+
const minsMatch = timeMatch[0].match(/(\d+)m/);
|
|
90
|
+
if (hoursMatch) {
|
|
91
|
+
minutes += parseInt(hoursMatch[1]!, 10) * 60;
|
|
92
|
+
}
|
|
93
|
+
if (minsMatch) {
|
|
94
|
+
minutes += parseInt(minsMatch[1]!, 10);
|
|
95
|
+
}
|
|
96
|
+
if (minutes > 0) {
|
|
97
|
+
result.estimate = { expected: minutes, confidence: "medium" };
|
|
98
|
+
remaining = remaining.replace(timeMatch[0], "").trim();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Clean up extra spaces
|
|
103
|
+
result.title = remaining.replace(/\s+/g, " ").trim();
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parsePriority(input: string): Priority | null {
|
|
109
|
+
const lower = input.toLowerCase();
|
|
110
|
+
|
|
111
|
+
const priorityMap: Record<string, Priority> = {
|
|
112
|
+
critical: "critical",
|
|
113
|
+
highest: "critical",
|
|
114
|
+
긴급: "critical",
|
|
115
|
+
high: "high",
|
|
116
|
+
높음: "high",
|
|
117
|
+
medium: "medium",
|
|
118
|
+
normal: "medium",
|
|
119
|
+
보통: "medium",
|
|
120
|
+
low: "low",
|
|
121
|
+
낮음: "low",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return priorityMap[lower] ?? null;
|
|
125
|
+
}
|