@task-mcp/shared 1.0.19 → 1.0.20
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/schemas/task.d.ts +290 -34
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/schemas/task.js +50 -13
- package/dist/schemas/task.js.map +1 -1
- package/dist/utils/date.d.ts +13 -0
- package/dist/utils/date.d.ts.map +1 -1
- package/dist/utils/date.js +29 -0
- package/dist/utils/date.js.map +1 -1
- package/dist/utils/hierarchy.d.ts +27 -0
- package/dist/utils/hierarchy.d.ts.map +1 -1
- package/dist/utils/hierarchy.js +48 -1
- package/dist/utils/hierarchy.js.map +1 -1
- package/dist/utils/hierarchy.test.js +86 -1
- package/dist/utils/hierarchy.test.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/natural-language.d.ts.map +1 -1
- package/dist/utils/natural-language.js +6 -0
- package/dist/utils/natural-language.js.map +1 -1
- package/dist/utils/natural-language.test.js +42 -1
- package/dist/utils/natural-language.test.js.map +1 -1
- package/dist/utils/priority-queue.test.d.ts +2 -0
- package/dist/utils/priority-queue.test.d.ts.map +1 -0
- package/dist/utils/priority-queue.test.js +82 -0
- package/dist/utils/priority-queue.test.js.map +1 -0
- package/dist/utils/projection.d.ts.map +1 -1
- package/dist/utils/projection.js +13 -3
- package/dist/utils/projection.js.map +1 -1
- package/dist/utils/workspace.test.d.ts +2 -0
- package/dist/utils/workspace.test.d.ts.map +1 -0
- package/dist/utils/workspace.test.js +97 -0
- package/dist/utils/workspace.test.js.map +1 -0
- package/package.json +1 -1
- package/src/schemas/task.ts +60 -14
- package/src/utils/date.ts +40 -0
- package/src/utils/hierarchy.test.ts +94 -0
- package/src/utils/hierarchy.ts +63 -1
- package/src/utils/index.ts +2 -0
- package/src/utils/natural-language.test.ts +61 -1
- package/src/utils/natural-language.ts +12 -0
- package/src/utils/priority-queue.test.ts +103 -0
- package/src/utils/projection.ts +14 -3
- package/src/utils/workspace.test.ts +125 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
parseTaskInput,
|
|
4
|
+
parseInboxInput,
|
|
5
|
+
InputValidationError,
|
|
6
|
+
} from "./natural-language.js";
|
|
3
7
|
|
|
4
8
|
describe("parseTaskInput", () => {
|
|
5
9
|
describe("priority parsing", () => {
|
|
@@ -179,4 +183,60 @@ describe("parseTaskInput", () => {
|
|
|
179
183
|
expect(result.dueDate).toBeUndefined();
|
|
180
184
|
});
|
|
181
185
|
});
|
|
186
|
+
|
|
187
|
+
describe("empty content validation", () => {
|
|
188
|
+
test("throws error when input contains only tags", () => {
|
|
189
|
+
expect(() => parseTaskInput("#tag #only")).toThrow(InputValidationError);
|
|
190
|
+
expect(() => parseTaskInput("#tag #only")).toThrow(
|
|
191
|
+
"Task title cannot be empty after parsing"
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("throws error when input contains only metadata", () => {
|
|
196
|
+
expect(() => parseTaskInput("#dev !high @focus")).toThrow(
|
|
197
|
+
InputValidationError
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("throws error for empty input", () => {
|
|
202
|
+
expect(() => parseTaskInput("")).toThrow(InputValidationError);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("throws error for whitespace-only input", () => {
|
|
206
|
+
expect(() => parseTaskInput(" ")).toThrow(InputValidationError);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("parseInboxInput", () => {
|
|
212
|
+
describe("basic parsing", () => {
|
|
213
|
+
test("parses content with tags", () => {
|
|
214
|
+
const result = parseInboxInput("GraphQL 도입 검토 #backend #연구");
|
|
215
|
+
expect(result.content).toBe("GraphQL 도입 검토");
|
|
216
|
+
expect(result.tags).toEqual(["backend", "연구"]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("parses content without tags", () => {
|
|
220
|
+
const result = parseInboxInput("Simple idea to capture");
|
|
221
|
+
expect(result.content).toBe("Simple idea to capture");
|
|
222
|
+
expect(result.tags).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("empty content validation", () => {
|
|
227
|
+
test("throws error when input contains only tags", () => {
|
|
228
|
+
expect(() => parseInboxInput("#tag #only")).toThrow(InputValidationError);
|
|
229
|
+
expect(() => parseInboxInput("#tag #only")).toThrow(
|
|
230
|
+
"Content cannot be empty after parsing"
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("throws error for empty input", () => {
|
|
235
|
+
expect(() => parseInboxInput("")).toThrow(InputValidationError);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("throws error for whitespace-only input", () => {
|
|
239
|
+
expect(() => parseInboxInput(" ")).toThrow(InputValidationError);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
182
242
|
});
|
|
@@ -110,6 +110,12 @@ export function parseInboxInput(input: string): InboxCreateInput {
|
|
|
110
110
|
// Clean up and set content
|
|
111
111
|
result.content = remaining.replace(/\s+/g, " ").trim();
|
|
112
112
|
|
|
113
|
+
if (!result.content) {
|
|
114
|
+
throw new InputValidationError(
|
|
115
|
+
"Content cannot be empty after parsing. Input contained only metadata (tags)."
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
113
119
|
return result;
|
|
114
120
|
}
|
|
115
121
|
|
|
@@ -223,6 +229,12 @@ export function parseTaskInput(input: string): TaskCreateInput {
|
|
|
223
229
|
// Clean up extra spaces
|
|
224
230
|
result.title = remaining.replace(/\s+/g, " ").trim();
|
|
225
231
|
|
|
232
|
+
if (!result.title) {
|
|
233
|
+
throw new InputValidationError(
|
|
234
|
+
"Task title cannot be empty after parsing. Input contained only metadata."
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
226
238
|
return result;
|
|
227
239
|
}
|
|
228
240
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { PriorityQueue } from "./priority-queue";
|
|
3
|
+
|
|
4
|
+
describe("PriorityQueue", () => {
|
|
5
|
+
test("creates empty queue", () => {
|
|
6
|
+
const pq = new PriorityQueue<number>((a, b) => a - b);
|
|
7
|
+
expect(pq.length).toBe(0);
|
|
8
|
+
expect(pq.isEmpty()).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("pop from empty queue returns undefined", () => {
|
|
12
|
+
const pq = new PriorityQueue<number>((a, b) => a - b);
|
|
13
|
+
expect(pq.pop()).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("peek from empty queue returns undefined", () => {
|
|
17
|
+
const pq = new PriorityQueue<number>((a, b) => a - b);
|
|
18
|
+
expect(pq.peek()).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("single element operations", () => {
|
|
22
|
+
const pq = new PriorityQueue<number>((a, b) => a - b);
|
|
23
|
+
pq.push(42);
|
|
24
|
+
expect(pq.length).toBe(1);
|
|
25
|
+
expect(pq.peek()).toBe(42);
|
|
26
|
+
expect(pq.pop()).toBe(42);
|
|
27
|
+
expect(pq.isEmpty()).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("maintains max-heap property (higher value first with a-b comparator)", () => {
|
|
31
|
+
const pq = new PriorityQueue<number>((a, b) => a - b);
|
|
32
|
+
pq.push(5);
|
|
33
|
+
pq.push(2);
|
|
34
|
+
pq.push(8);
|
|
35
|
+
pq.push(1);
|
|
36
|
+
pq.push(9);
|
|
37
|
+
|
|
38
|
+
// Max-heap: higher values come first
|
|
39
|
+
expect(pq.pop()).toBe(9);
|
|
40
|
+
expect(pq.pop()).toBe(8);
|
|
41
|
+
expect(pq.pop()).toBe(5);
|
|
42
|
+
expect(pq.pop()).toBe(2);
|
|
43
|
+
expect(pq.pop()).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("works with reversed comparator (min-heap behavior)", () => {
|
|
47
|
+
const pq = new PriorityQueue<number>((a, b) => b - a);
|
|
48
|
+
pq.push(5);
|
|
49
|
+
pq.push(2);
|
|
50
|
+
pq.push(8);
|
|
51
|
+
|
|
52
|
+
// Reversed comparator: smaller values come first
|
|
53
|
+
expect(pq.pop()).toBe(2);
|
|
54
|
+
expect(pq.pop()).toBe(5);
|
|
55
|
+
expect(pq.pop()).toBe(8);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("works with objects", () => {
|
|
59
|
+
interface Task {
|
|
60
|
+
priority: number;
|
|
61
|
+
name: string;
|
|
62
|
+
}
|
|
63
|
+
// Higher priority value = higher priority (max-heap)
|
|
64
|
+
const pq = new PriorityQueue<Task>((a, b) => a.priority - b.priority);
|
|
65
|
+
|
|
66
|
+
pq.push({ priority: 3, name: "high" });
|
|
67
|
+
pq.push({ priority: 1, name: "low" });
|
|
68
|
+
pq.push({ priority: 2, name: "medium" });
|
|
69
|
+
|
|
70
|
+
expect(pq.pop()?.name).toBe("high");
|
|
71
|
+
expect(pq.pop()?.name).toBe("medium");
|
|
72
|
+
expect(pq.pop()?.name).toBe("low");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("length property tracks queue size correctly", () => {
|
|
76
|
+
const pq = new PriorityQueue<number>((a, b) => a - b);
|
|
77
|
+
expect(pq.length).toBe(0);
|
|
78
|
+
|
|
79
|
+
pq.push(1);
|
|
80
|
+
expect(pq.length).toBe(1);
|
|
81
|
+
|
|
82
|
+
pq.push(2);
|
|
83
|
+
pq.push(3);
|
|
84
|
+
expect(pq.length).toBe(3);
|
|
85
|
+
|
|
86
|
+
pq.pop();
|
|
87
|
+
expect(pq.length).toBe(2);
|
|
88
|
+
|
|
89
|
+
pq.pop();
|
|
90
|
+
pq.pop();
|
|
91
|
+
expect(pq.length).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("peek does not remove element", () => {
|
|
95
|
+
const pq = new PriorityQueue<number>((a, b) => a - b);
|
|
96
|
+
pq.push(5);
|
|
97
|
+
pq.push(10);
|
|
98
|
+
|
|
99
|
+
expect(pq.peek()).toBe(10);
|
|
100
|
+
expect(pq.peek()).toBe(10);
|
|
101
|
+
expect(pq.length).toBe(2);
|
|
102
|
+
});
|
|
103
|
+
});
|
package/src/utils/projection.ts
CHANGED
|
@@ -16,6 +16,14 @@ import type {
|
|
|
16
16
|
PaginatedResponse,
|
|
17
17
|
} from "../schemas/response-format.js";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Exhaustiveness check helper for switch statements.
|
|
21
|
+
* TypeScript will error if a case is not handled.
|
|
22
|
+
*/
|
|
23
|
+
function assertNever(value: never): never {
|
|
24
|
+
throw new Error(`Unexpected value: ${value}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
/**
|
|
20
28
|
* Project a single task to the specified format
|
|
21
29
|
*/
|
|
@@ -42,8 +50,9 @@ export function projectTask(task: Task, format: ResponseFormat): TaskSummary | T
|
|
|
42
50
|
};
|
|
43
51
|
|
|
44
52
|
case "detailed":
|
|
45
|
-
default:
|
|
46
53
|
return task;
|
|
54
|
+
default:
|
|
55
|
+
return assertNever(format);
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
58
|
|
|
@@ -106,8 +115,9 @@ export function projectInboxItem(
|
|
|
106
115
|
};
|
|
107
116
|
|
|
108
117
|
case "detailed":
|
|
109
|
-
default:
|
|
110
118
|
return item;
|
|
119
|
+
default:
|
|
120
|
+
return assertNever(format);
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
123
|
|
|
@@ -226,8 +236,9 @@ export function sortTasks(
|
|
|
226
236
|
}
|
|
227
237
|
|
|
228
238
|
case "updatedAt":
|
|
229
|
-
default:
|
|
230
239
|
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
|
240
|
+
default:
|
|
241
|
+
return assertNever(secondarySort);
|
|
231
242
|
}
|
|
232
243
|
});
|
|
233
244
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
normalizeWorkspace,
|
|
4
|
+
detectWorkspaceSync,
|
|
5
|
+
detectWorkspace,
|
|
6
|
+
getWorkspaceFromPath,
|
|
7
|
+
getGitRepoRoot,
|
|
8
|
+
getGitRepoRootSync,
|
|
9
|
+
} from "./workspace";
|
|
10
|
+
|
|
11
|
+
describe("normalizeWorkspace", () => {
|
|
12
|
+
test("converts to lowercase", () => {
|
|
13
|
+
expect(normalizeWorkspace("MyProject")).toBe("myproject");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("replaces spaces with hyphens", () => {
|
|
17
|
+
expect(normalizeWorkspace("my project")).toBe("my-project");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("removes special characters", () => {
|
|
21
|
+
expect(normalizeWorkspace("my@project!")).toBe("myproject");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("handles empty string", () => {
|
|
25
|
+
expect(normalizeWorkspace("")).toBe("default");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("keeps hyphens and underscores", () => {
|
|
29
|
+
expect(normalizeWorkspace("my-project_name")).toBe("my-project_name");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("trims leading and trailing hyphens", () => {
|
|
33
|
+
expect(normalizeWorkspace("--my-project--")).toBe("my-project");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("handles multiple spaces", () => {
|
|
37
|
+
// Multiple spaces become multiple hyphens, then cleaned by special char removal
|
|
38
|
+
expect(normalizeWorkspace("my project")).toBe("my-project");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("handles mixed case with special chars", () => {
|
|
42
|
+
expect(normalizeWorkspace("My@Project#Name")).toBe("myprojectname");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("getWorkspaceFromPath", () => {
|
|
47
|
+
test("extracts workspace from path", () => {
|
|
48
|
+
expect(getWorkspaceFromPath("/home/user/projects/my-app")).toBe("my-app");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("handles path with .tasks suffix", () => {
|
|
52
|
+
expect(getWorkspaceFromPath("/projects/my-app/.tasks")).toBe("my-app");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("handles path with trailing slash", () => {
|
|
56
|
+
expect(getWorkspaceFromPath("/projects/my-app/.tasks/")).toBe("my-app");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("normalizes the extracted name", () => {
|
|
60
|
+
expect(getWorkspaceFromPath("/projects/My App")).toBe("my-app");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("getGitRepoRootSync", () => {
|
|
65
|
+
test("returns string or null for current directory", () => {
|
|
66
|
+
const result = getGitRepoRootSync();
|
|
67
|
+
expect(result === null || typeof result === "string").toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("returns null for non-git directory", () => {
|
|
71
|
+
const result = getGitRepoRootSync("/tmp");
|
|
72
|
+
expect(result).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("getGitRepoRoot", () => {
|
|
77
|
+
test("returns string or null for current directory", async () => {
|
|
78
|
+
const result = await getGitRepoRoot();
|
|
79
|
+
expect(result === null || typeof result === "string").toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("returns null for non-git directory", async () => {
|
|
83
|
+
const result = await getGitRepoRoot("/tmp");
|
|
84
|
+
expect(result).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("detectWorkspaceSync", () => {
|
|
89
|
+
test("returns a workspace name", () => {
|
|
90
|
+
const workspace = detectWorkspaceSync();
|
|
91
|
+
expect(typeof workspace).toBe("string");
|
|
92
|
+
expect(workspace.length).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("uses tasksDir path when provided and no git repo", () => {
|
|
96
|
+
// This tests the fallback to tasksDir when in a non-git directory
|
|
97
|
+
const workspace = detectWorkspaceSync("/some/path/my-project/.tasks");
|
|
98
|
+
expect(typeof workspace).toBe("string");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("returns normalized workspace name", () => {
|
|
102
|
+
const workspace = detectWorkspaceSync();
|
|
103
|
+
// Should be lowercase with no special characters except - and _
|
|
104
|
+
expect(workspace).toMatch(/^[a-z0-9_-]+$/);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("detectWorkspace", () => {
|
|
109
|
+
test("returns a workspace name asynchronously", async () => {
|
|
110
|
+
const workspace = await detectWorkspace();
|
|
111
|
+
expect(typeof workspace).toBe("string");
|
|
112
|
+
expect(workspace.length).toBeGreaterThan(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("uses tasksDir path when provided", async () => {
|
|
116
|
+
const workspace = await detectWorkspace("/some/path/test-project/.tasks");
|
|
117
|
+
expect(typeof workspace).toBe("string");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("returns normalized workspace name", async () => {
|
|
121
|
+
const workspace = await detectWorkspace();
|
|
122
|
+
// Should be lowercase with no special characters except - and _
|
|
123
|
+
expect(workspace).toMatch(/^[a-z0-9_-]+$/);
|
|
124
|
+
});
|
|
125
|
+
});
|