@task-mcp/shared 1.0.4 → 1.0.7

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 (98) hide show
  1. package/dist/algorithms/critical-path.d.ts.map +1 -1
  2. package/dist/algorithms/critical-path.js +50 -26
  3. package/dist/algorithms/critical-path.js.map +1 -1
  4. package/dist/algorithms/dependency-integrity.d.ts +73 -0
  5. package/dist/algorithms/dependency-integrity.d.ts.map +1 -0
  6. package/dist/algorithms/dependency-integrity.js +189 -0
  7. package/dist/algorithms/dependency-integrity.js.map +1 -0
  8. package/dist/algorithms/index.d.ts +2 -0
  9. package/dist/algorithms/index.d.ts.map +1 -1
  10. package/dist/algorithms/index.js +2 -0
  11. package/dist/algorithms/index.js.map +1 -1
  12. package/dist/algorithms/tech-analysis.d.ts +106 -0
  13. package/dist/algorithms/tech-analysis.d.ts.map +1 -0
  14. package/dist/algorithms/tech-analysis.js +296 -0
  15. package/dist/algorithms/tech-analysis.js.map +1 -0
  16. package/dist/algorithms/tech-analysis.test.d.ts +2 -0
  17. package/dist/algorithms/tech-analysis.test.d.ts.map +1 -0
  18. package/dist/algorithms/tech-analysis.test.js +338 -0
  19. package/dist/algorithms/tech-analysis.test.js.map +1 -0
  20. package/dist/algorithms/topological-sort.d.ts.map +1 -1
  21. package/dist/algorithms/topological-sort.js +60 -8
  22. package/dist/algorithms/topological-sort.js.map +1 -1
  23. package/dist/schemas/inbox.d.ts +55 -0
  24. package/dist/schemas/inbox.d.ts.map +1 -0
  25. package/dist/schemas/inbox.js +25 -0
  26. package/dist/schemas/inbox.js.map +1 -0
  27. package/dist/schemas/index.d.ts +3 -1
  28. package/dist/schemas/index.d.ts.map +1 -1
  29. package/dist/schemas/index.js +9 -1
  30. package/dist/schemas/index.js.map +1 -1
  31. package/dist/schemas/project.d.ts +154 -41
  32. package/dist/schemas/project.d.ts.map +1 -1
  33. package/dist/schemas/project.js +38 -33
  34. package/dist/schemas/project.js.map +1 -1
  35. package/dist/schemas/response-format.d.ts +80 -0
  36. package/dist/schemas/response-format.d.ts.map +1 -0
  37. package/dist/schemas/response-format.js +17 -0
  38. package/dist/schemas/response-format.js.map +1 -0
  39. package/dist/schemas/task.d.ts +592 -94
  40. package/dist/schemas/task.d.ts.map +1 -1
  41. package/dist/schemas/task.js +124 -64
  42. package/dist/schemas/task.js.map +1 -1
  43. package/dist/schemas/view.d.ts +128 -37
  44. package/dist/schemas/view.d.ts.map +1 -1
  45. package/dist/schemas/view.js +38 -24
  46. package/dist/schemas/view.js.map +1 -1
  47. package/dist/utils/date.d.ts.map +1 -1
  48. package/dist/utils/date.js +17 -2
  49. package/dist/utils/date.js.map +1 -1
  50. package/dist/utils/hierarchy.d.ts +75 -0
  51. package/dist/utils/hierarchy.d.ts.map +1 -0
  52. package/dist/utils/hierarchy.js +179 -0
  53. package/dist/utils/hierarchy.js.map +1 -0
  54. package/dist/utils/id.d.ts +51 -1
  55. package/dist/utils/id.d.ts.map +1 -1
  56. package/dist/utils/id.js +124 -4
  57. package/dist/utils/id.js.map +1 -1
  58. package/dist/utils/id.test.d.ts +2 -0
  59. package/dist/utils/id.test.d.ts.map +1 -0
  60. package/dist/utils/id.test.js +228 -0
  61. package/dist/utils/id.test.js.map +1 -0
  62. package/dist/utils/index.d.ts +4 -2
  63. package/dist/utils/index.d.ts.map +1 -1
  64. package/dist/utils/index.js +7 -2
  65. package/dist/utils/index.js.map +1 -1
  66. package/dist/utils/natural-language.d.ts +45 -0
  67. package/dist/utils/natural-language.d.ts.map +1 -1
  68. package/dist/utils/natural-language.js +86 -0
  69. package/dist/utils/natural-language.js.map +1 -1
  70. package/dist/utils/projection.d.ts +65 -0
  71. package/dist/utils/projection.d.ts.map +1 -0
  72. package/dist/utils/projection.js +181 -0
  73. package/dist/utils/projection.js.map +1 -0
  74. package/dist/utils/projection.test.d.ts +2 -0
  75. package/dist/utils/projection.test.d.ts.map +1 -0
  76. package/dist/utils/projection.test.js +400 -0
  77. package/dist/utils/projection.test.js.map +1 -0
  78. package/package.json +2 -2
  79. package/src/algorithms/critical-path.ts +56 -24
  80. package/src/algorithms/dependency-integrity.ts +270 -0
  81. package/src/algorithms/index.ts +28 -0
  82. package/src/algorithms/tech-analysis.test.ts +413 -0
  83. package/src/algorithms/tech-analysis.ts +412 -0
  84. package/src/algorithms/topological-sort.ts +66 -9
  85. package/src/schemas/inbox.ts +32 -0
  86. package/src/schemas/index.ts +31 -0
  87. package/src/schemas/project.ts +43 -40
  88. package/src/schemas/response-format.ts +108 -0
  89. package/src/schemas/task.ts +145 -77
  90. package/src/schemas/view.ts +43 -33
  91. package/src/utils/date.ts +18 -2
  92. package/src/utils/hierarchy.ts +224 -0
  93. package/src/utils/id.test.ts +281 -0
  94. package/src/utils/id.ts +139 -4
  95. package/src/utils/index.ts +46 -2
  96. package/src/utils/natural-language.ts +113 -0
  97. package/src/utils/projection.test.ts +505 -0
  98. package/src/utils/projection.ts +251 -0
@@ -0,0 +1,224 @@
1
+ import type { Task } from "../schemas/task.js";
2
+
3
+ /**
4
+ * Maximum allowed hierarchy depth (0-indexed)
5
+ * 0 = root task
6
+ * 1 = subtask
7
+ * 2 = sub-subtask
8
+ * 3 = sub-sub-subtask (maximum)
9
+ */
10
+ export const MAX_HIERARCHY_DEPTH = 3;
11
+
12
+ /**
13
+ * Calculates the hierarchy level of a task.
14
+ * Level 0 = root task (no parent)
15
+ * Level 1 = direct subtask
16
+ * Level 2 = sub-subtask
17
+ * etc.
18
+ *
19
+ * @param tasks - Array of all tasks
20
+ * @param taskId - ID of the task to get level for
21
+ * @returns The hierarchy level (0 for root), or -1 if task not found
22
+ */
23
+ export function getTaskLevel(tasks: Task[], taskId: string): number {
24
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
25
+ const task = taskMap.get(taskId);
26
+
27
+ if (!task) {
28
+ return -1;
29
+ }
30
+
31
+ let level = 0;
32
+ let currentTask = task;
33
+
34
+ while (currentTask.parentId) {
35
+ const parent = taskMap.get(currentTask.parentId);
36
+ if (!parent) {
37
+ break; // Parent not found, stop traversal
38
+ }
39
+ level++;
40
+ currentTask = parent;
41
+
42
+ // Safety check to prevent infinite loops
43
+ if (level > MAX_HIERARCHY_DEPTH + 1) {
44
+ break;
45
+ }
46
+ }
47
+
48
+ return level;
49
+ }
50
+
51
+ /**
52
+ * Validates whether adding a child task under the given parent
53
+ * would exceed the maximum hierarchy depth.
54
+ *
55
+ * @param tasks - Array of all tasks
56
+ * @param parentId - ID of the proposed parent task
57
+ * @returns true if a child can be added, false if it would exceed max depth
58
+ */
59
+ export function validateHierarchyDepth(
60
+ tasks: Task[],
61
+ parentId: string
62
+ ): boolean {
63
+ const parentLevel = getTaskLevel(tasks, parentId);
64
+
65
+ if (parentLevel === -1) {
66
+ return false; // Parent task not found
67
+ }
68
+
69
+ // Child would be at parentLevel + 1
70
+ // If parent is at level 3, child would be at level 4 which exceeds max
71
+ return parentLevel < MAX_HIERARCHY_DEPTH;
72
+ }
73
+
74
+ /**
75
+ * Gets all ancestor task IDs for a given task (from immediate parent to root).
76
+ *
77
+ * @param tasks - Array of all tasks
78
+ * @param taskId - ID of the task
79
+ * @returns Array of ancestor task IDs, ordered from immediate parent to root
80
+ */
81
+ export function getAncestorIds(tasks: Task[], taskId: string): string[] {
82
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
83
+ const task = taskMap.get(taskId);
84
+
85
+ if (!task) {
86
+ return [];
87
+ }
88
+
89
+ const ancestors: string[] = [];
90
+ let currentTask = task;
91
+
92
+ while (currentTask.parentId) {
93
+ ancestors.push(currentTask.parentId);
94
+ const parent = taskMap.get(currentTask.parentId);
95
+ if (!parent) {
96
+ break;
97
+ }
98
+ currentTask = parent;
99
+
100
+ // Safety check
101
+ if (ancestors.length > MAX_HIERARCHY_DEPTH + 1) {
102
+ break;
103
+ }
104
+ }
105
+
106
+ return ancestors;
107
+ }
108
+
109
+ /**
110
+ * Gets all descendant task IDs for a given task (children, grandchildren, etc.).
111
+ *
112
+ * @param tasks - Array of all tasks
113
+ * @param taskId - ID of the parent task
114
+ * @returns Array of descendant task IDs
115
+ */
116
+ export function getDescendantIds(tasks: Task[], taskId: string): string[] {
117
+ const descendants: string[] = [];
118
+ const childrenMap = new Map<string, Task[]>();
119
+
120
+ // Build parent -> children map
121
+ for (const task of tasks) {
122
+ if (task.parentId) {
123
+ const children = childrenMap.get(task.parentId) || [];
124
+ children.push(task);
125
+ childrenMap.set(task.parentId, children);
126
+ }
127
+ }
128
+
129
+ // BFS to collect all descendants
130
+ const queue = [taskId];
131
+ while (queue.length > 0) {
132
+ const currentId = queue.shift()!;
133
+ const children = childrenMap.get(currentId) || [];
134
+ for (const child of children) {
135
+ descendants.push(child.id);
136
+ queue.push(child.id);
137
+ }
138
+ }
139
+
140
+ return descendants;
141
+ }
142
+
143
+ /**
144
+ * Gets direct children of a task.
145
+ *
146
+ * @param tasks - Array of all tasks
147
+ * @param taskId - ID of the parent task
148
+ * @returns Array of direct child tasks
149
+ */
150
+ export function getChildTasks(tasks: Task[], taskId: string): Task[] {
151
+ return tasks.filter((t) => t.parentId === taskId);
152
+ }
153
+
154
+ /**
155
+ * Gets the root task of a hierarchy.
156
+ *
157
+ * @param tasks - Array of all tasks
158
+ * @param taskId - ID of any task in the hierarchy
159
+ * @returns The root task, or null if not found
160
+ */
161
+ export function getRootTask(tasks: Task[], taskId: string): Task | null {
162
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
163
+ let currentTask = taskMap.get(taskId);
164
+
165
+ if (!currentTask) {
166
+ return null;
167
+ }
168
+
169
+ while (currentTask.parentId) {
170
+ const parent = taskMap.get(currentTask.parentId);
171
+ if (!parent) {
172
+ break;
173
+ }
174
+ currentTask = parent;
175
+ }
176
+
177
+ return currentTask;
178
+ }
179
+
180
+ /**
181
+ * Builds a tree structure from flat task list.
182
+ *
183
+ * @param tasks - Array of all tasks
184
+ * @param rootId - Optional root task ID (if not provided, returns all root tasks)
185
+ * @returns Tree structure with children arrays
186
+ */
187
+ export interface TaskTreeNode {
188
+ task: Task;
189
+ children: TaskTreeNode[];
190
+ }
191
+
192
+ export function buildTaskTree(tasks: Task[], rootId?: string): TaskTreeNode[] {
193
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
194
+ const childrenMap = new Map<string, Task[]>();
195
+
196
+ // Build parent -> children map
197
+ for (const task of tasks) {
198
+ if (task.parentId) {
199
+ const children = childrenMap.get(task.parentId) || [];
200
+ children.push(task);
201
+ childrenMap.set(task.parentId, children);
202
+ }
203
+ }
204
+
205
+ function buildNode(task: Task): TaskTreeNode {
206
+ const children = childrenMap.get(task.id) || [];
207
+ return {
208
+ task,
209
+ children: children.map(buildNode),
210
+ };
211
+ }
212
+
213
+ if (rootId) {
214
+ const rootTask = taskMap.get(rootId);
215
+ if (!rootTask) {
216
+ return [];
217
+ }
218
+ return [buildNode(rootTask)];
219
+ }
220
+
221
+ // Return all root tasks (no parent)
222
+ const rootTasks = tasks.filter((t) => !t.parentId);
223
+ return rootTasks.map(buildNode);
224
+ }
@@ -0,0 +1,281 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ generateId,
4
+ generateTaskId,
5
+ generateProjectId,
6
+ generateInboxId,
7
+ isValidTaskId,
8
+ isValidProjectId,
9
+ isValidInboxId,
10
+ validateTaskId,
11
+ validateProjectId,
12
+ validateInboxId,
13
+ assertValidTaskId,
14
+ assertValidProjectId,
15
+ assertValidInboxId,
16
+ InvalidIdError,
17
+ } from "./id.js";
18
+
19
+ describe("ID Generation", () => {
20
+ describe("generateId", () => {
21
+ test("generates ID without prefix", () => {
22
+ const id = generateId();
23
+ expect(id.length).toBe(12);
24
+ expect(/^[a-z0-9]+$/.test(id)).toBe(true);
25
+ });
26
+
27
+ test("generates ID with prefix", () => {
28
+ const id = generateId("test");
29
+ expect(id).toMatch(/^test_[a-z0-9]+$/);
30
+ });
31
+
32
+ test("generates unique IDs", () => {
33
+ const ids = new Set(Array.from({ length: 100 }, () => generateId("test")));
34
+ expect(ids.size).toBe(100);
35
+ });
36
+ });
37
+
38
+ describe("generateTaskId", () => {
39
+ test("generates task ID with correct prefix", () => {
40
+ const id = generateTaskId();
41
+ expect(id).toMatch(/^task_[a-z0-9]+$/);
42
+ });
43
+ });
44
+
45
+ describe("generateProjectId", () => {
46
+ test("generates project ID with correct prefix", () => {
47
+ const id = generateProjectId();
48
+ expect(id).toMatch(/^proj_[a-z0-9]+$/);
49
+ });
50
+ });
51
+
52
+ describe("generateInboxId", () => {
53
+ test("generates inbox ID with correct prefix", () => {
54
+ const id = generateInboxId();
55
+ expect(id).toMatch(/^inbox_[a-z0-9]+$/);
56
+ });
57
+ });
58
+ });
59
+
60
+ describe("Simple ID Validation (boolean)", () => {
61
+ describe("isValidTaskId", () => {
62
+ test("returns true for valid task IDs", () => {
63
+ expect(isValidTaskId("task_abc123")).toBe(true);
64
+ expect(isValidTaskId("task_a")).toBe(true);
65
+ expect(isValidTaskId("task_123")).toBe(true);
66
+ expect(isValidTaskId("task_abc123def456")).toBe(true);
67
+ });
68
+
69
+ test("returns false for invalid task IDs", () => {
70
+ expect(isValidTaskId("")).toBe(false);
71
+ expect(isValidTaskId("task_")).toBe(false);
72
+ expect(isValidTaskId("abc123")).toBe(false);
73
+ expect(isValidTaskId("proj_abc123")).toBe(false);
74
+ expect(isValidTaskId("task_ABC123")).toBe(false);
75
+ expect(isValidTaskId("task_abc-123")).toBe(false);
76
+ expect(isValidTaskId("task_abc.123")).toBe(false);
77
+ expect(isValidTaskId("task_abc/123")).toBe(false);
78
+ expect(isValidTaskId("task_abc 123")).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe("isValidProjectId", () => {
83
+ test("returns true for valid project IDs", () => {
84
+ expect(isValidProjectId("proj_abc123")).toBe(true);
85
+ expect(isValidProjectId("proj_a")).toBe(true);
86
+ });
87
+
88
+ test("returns false for invalid project IDs", () => {
89
+ expect(isValidProjectId("")).toBe(false);
90
+ expect(isValidProjectId("project_abc123")).toBe(false);
91
+ expect(isValidProjectId("task_abc123")).toBe(false);
92
+ });
93
+ });
94
+
95
+ describe("isValidInboxId", () => {
96
+ test("returns true for valid inbox IDs", () => {
97
+ expect(isValidInboxId("inbox_abc123")).toBe(true);
98
+ expect(isValidInboxId("inbox_a")).toBe(true);
99
+ });
100
+
101
+ test("returns false for invalid inbox IDs", () => {
102
+ expect(isValidInboxId("")).toBe(false);
103
+ expect(isValidInboxId("in_abc123")).toBe(false);
104
+ expect(isValidInboxId("task_abc123")).toBe(false);
105
+ });
106
+ });
107
+ });
108
+
109
+ describe("Detailed ID Validation", () => {
110
+ describe("validateTaskId", () => {
111
+ test("returns valid: true for valid task IDs", () => {
112
+ expect(validateTaskId("task_abc123")).toEqual({ valid: true });
113
+ expect(validateTaskId("task_a1b2c3")).toEqual({ valid: true });
114
+ });
115
+
116
+ test("returns error for null/undefined", () => {
117
+ const result = validateTaskId(null);
118
+ expect(result.valid).toBe(false);
119
+ expect(result.reason).toContain("required");
120
+
121
+ const result2 = validateTaskId(undefined);
122
+ expect(result2.valid).toBe(false);
123
+ });
124
+
125
+ test("returns error for non-string", () => {
126
+ const result = validateTaskId(123);
127
+ expect(result.valid).toBe(false);
128
+ expect(result.reason).toContain("string");
129
+ expect(result.reason).toContain("number");
130
+ });
131
+
132
+ test("returns error for empty string", () => {
133
+ const result = validateTaskId("");
134
+ expect(result.valid).toBe(false);
135
+ expect(result.reason).toContain("empty");
136
+ });
137
+
138
+ test("returns error for missing prefix", () => {
139
+ const result = validateTaskId("abc123");
140
+ expect(result.valid).toBe(false);
141
+ expect(result.reason).toContain("task_");
142
+ });
143
+
144
+ test("returns error for wrong prefix", () => {
145
+ const result = validateTaskId("proj_abc123");
146
+ expect(result.valid).toBe(false);
147
+ expect(result.reason).toContain("task_");
148
+ });
149
+
150
+ test("returns error for invalid characters", () => {
151
+ const result = validateTaskId("task_abc-123");
152
+ expect(result.valid).toBe(false);
153
+ expect(result.reason).toContain("invalid characters");
154
+ });
155
+ });
156
+
157
+ describe("validateProjectId", () => {
158
+ test("returns valid: true for valid project IDs", () => {
159
+ expect(validateProjectId("proj_abc123")).toEqual({ valid: true });
160
+ });
161
+
162
+ test("returns error for missing prefix", () => {
163
+ const result = validateProjectId("project_abc");
164
+ expect(result.valid).toBe(false);
165
+ expect(result.reason).toContain("proj_");
166
+ });
167
+ });
168
+
169
+ describe("validateInboxId", () => {
170
+ test("returns valid: true for valid inbox IDs", () => {
171
+ expect(validateInboxId("inbox_abc123")).toEqual({ valid: true });
172
+ });
173
+
174
+ test("returns error for missing prefix", () => {
175
+ const result = validateInboxId("in_abc");
176
+ expect(result.valid).toBe(false);
177
+ expect(result.reason).toContain("inbox_");
178
+ });
179
+ });
180
+ });
181
+
182
+ describe("Assert Functions", () => {
183
+ describe("assertValidTaskId", () => {
184
+ test("does not throw for valid ID", () => {
185
+ expect(() => assertValidTaskId("task_abc123")).not.toThrow();
186
+ });
187
+
188
+ test("throws InvalidIdError for invalid ID", () => {
189
+ expect(() => assertValidTaskId("")).toThrow(InvalidIdError);
190
+ expect(() => assertValidTaskId("abc123")).toThrow(InvalidIdError);
191
+ expect(() => assertValidTaskId(null)).toThrow(InvalidIdError);
192
+ });
193
+
194
+ test("InvalidIdError contains correct properties", () => {
195
+ try {
196
+ assertValidTaskId("invalid");
197
+ } catch (error) {
198
+ expect(error).toBeInstanceOf(InvalidIdError);
199
+ if (error instanceof InvalidIdError) {
200
+ expect(error.idType).toBe("task");
201
+ expect(error.invalidValue).toBe("invalid");
202
+ expect(error.reason).toBeDefined();
203
+ expect(error.message).toContain("task");
204
+ }
205
+ }
206
+ });
207
+ });
208
+
209
+ describe("assertValidProjectId", () => {
210
+ test("does not throw for valid ID", () => {
211
+ expect(() => assertValidProjectId("proj_abc123")).not.toThrow();
212
+ });
213
+
214
+ test("throws InvalidIdError for invalid ID", () => {
215
+ expect(() => assertValidProjectId("project_abc")).toThrow(InvalidIdError);
216
+ });
217
+ });
218
+
219
+ describe("assertValidInboxId", () => {
220
+ test("does not throw for valid ID", () => {
221
+ expect(() => assertValidInboxId("inbox_abc123")).not.toThrow();
222
+ });
223
+
224
+ test("throws InvalidIdError for invalid ID", () => {
225
+ expect(() => assertValidInboxId("in_abc")).toThrow(InvalidIdError);
226
+ });
227
+ });
228
+ });
229
+
230
+ describe("InvalidIdError", () => {
231
+ test("extends Error", () => {
232
+ const error = new InvalidIdError("task", "bad_id", "test reason");
233
+ expect(error).toBeInstanceOf(Error);
234
+ expect(error).toBeInstanceOf(InvalidIdError);
235
+ });
236
+
237
+ test("has correct name", () => {
238
+ const error = new InvalidIdError("task", "bad_id", "test reason");
239
+ expect(error.name).toBe("InvalidIdError");
240
+ });
241
+
242
+ test("has correct message format", () => {
243
+ const error = new InvalidIdError("task", "bad_id", "test reason");
244
+ expect(error.message).toBe("Invalid task ID: test reason");
245
+ });
246
+
247
+ test("exposes idType, invalidValue, and reason", () => {
248
+ const error = new InvalidIdError("project", "wrong", "missing prefix");
249
+ expect(error.idType).toBe("project");
250
+ expect(error.invalidValue).toBe("wrong");
251
+ expect(error.reason).toBe("missing prefix");
252
+ });
253
+ });
254
+
255
+ describe("Security: ID Injection Prevention", () => {
256
+ test("rejects path traversal attempts", () => {
257
+ expect(validateTaskId("task_../etc/passwd").valid).toBe(false);
258
+ expect(validateTaskId("task_..%2F..%2Fetc").valid).toBe(false);
259
+ });
260
+
261
+ test("rejects null bytes", () => {
262
+ expect(validateTaskId("task_abc\0def").valid).toBe(false);
263
+ });
264
+
265
+ test("rejects SQL injection attempts", () => {
266
+ expect(validateTaskId("task_'; DROP TABLE tasks;--").valid).toBe(false);
267
+ expect(validateTaskId("task_1 OR 1=1").valid).toBe(false);
268
+ });
269
+
270
+ test("rejects script injection attempts", () => {
271
+ expect(validateTaskId("task_<script>alert(1)</script>").valid).toBe(false);
272
+ });
273
+
274
+ test("only allows safe alphanumeric characters", () => {
275
+ // The regex ^task_[a-z0-9]+$ only allows lowercase letters and numbers
276
+ expect(validateTaskId("task_abc123").valid).toBe(true);
277
+ expect(validateTaskId("task_UPPERCASE").valid).toBe(false);
278
+ expect(validateTaskId("task_with_underscore").valid).toBe(false);
279
+ expect(validateTaskId("task_with-dash").valid).toBe(false);
280
+ });
281
+ });
package/src/utils/id.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Generate a unique ID with optional prefix
2
+ * Generate a unique ID with optional prefix.
3
+ * Uses crypto.randomUUID for secure, collision-resistant ID generation.
3
4
  */
4
5
  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}`;
6
+ // Use crypto.randomUUID for better randomness and collision resistance
7
+ // Format: prefix_xxxxxxxx (8 chars from UUID, sufficient for local uniqueness)
8
+ const uuid = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
9
+ return prefix ? `${prefix}_${uuid}` : uuid;
8
10
  }
9
11
 
10
12
  /**
@@ -43,3 +45,136 @@ export function isValidProjectId(id: string): boolean {
43
45
  export function isValidTaskId(id: string): boolean {
44
46
  return /^task_[a-z0-9]+$/.test(id);
45
47
  }
48
+
49
+ /**
50
+ * Generate an inbox item ID
51
+ */
52
+ export function generateInboxId(): string {
53
+ return generateId("inbox");
54
+ }
55
+
56
+ /**
57
+ * Validate an inbox ID format
58
+ * @returns true if valid format (inbox_[alphanumeric])
59
+ */
60
+ export function isValidInboxId(id: string): boolean {
61
+ return /^inbox_[a-z0-9]+$/.test(id);
62
+ }
63
+
64
+ /**
65
+ * ID validation error with detailed context
66
+ */
67
+ export class InvalidIdError extends Error {
68
+ constructor(
69
+ public readonly idType: "task" | "project" | "inbox" | "view",
70
+ public readonly invalidValue: unknown,
71
+ public readonly reason: string
72
+ ) {
73
+ super(`Invalid ${idType} ID: ${reason}`);
74
+ this.name = "InvalidIdError";
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Validation result with reason if invalid
80
+ */
81
+ export interface IdValidationResult {
82
+ valid: boolean;
83
+ reason?: string;
84
+ }
85
+
86
+ /**
87
+ * Validate task ID with detailed error information
88
+ */
89
+ export function validateTaskId(id: unknown): IdValidationResult {
90
+ if (id === null || id === undefined) {
91
+ return { valid: false, reason: "Task ID is required" };
92
+ }
93
+ if (typeof id !== "string") {
94
+ return { valid: false, reason: `Task ID must be a string (received ${typeof id})` };
95
+ }
96
+ if (id.length === 0) {
97
+ return { valid: false, reason: "Task ID cannot be empty" };
98
+ }
99
+ if (!id.startsWith("task_")) {
100
+ return { valid: false, reason: "Task ID must start with 'task_' prefix" };
101
+ }
102
+ if (!/^task_[a-z0-9]+$/.test(id)) {
103
+ return { valid: false, reason: "Task ID contains invalid characters" };
104
+ }
105
+ return { valid: true };
106
+ }
107
+
108
+ /**
109
+ * Validate project ID with detailed error information
110
+ */
111
+ export function validateProjectId(id: unknown): IdValidationResult {
112
+ if (id === null || id === undefined) {
113
+ return { valid: false, reason: "Project ID is required" };
114
+ }
115
+ if (typeof id !== "string") {
116
+ return { valid: false, reason: `Project ID must be a string (received ${typeof id})` };
117
+ }
118
+ if (id.length === 0) {
119
+ return { valid: false, reason: "Project ID cannot be empty" };
120
+ }
121
+ if (!id.startsWith("proj_")) {
122
+ return { valid: false, reason: "Project ID must start with 'proj_' prefix" };
123
+ }
124
+ if (!/^proj_[a-z0-9]+$/.test(id)) {
125
+ return { valid: false, reason: "Project ID contains invalid characters" };
126
+ }
127
+ return { valid: true };
128
+ }
129
+
130
+ /**
131
+ * Validate inbox ID with detailed error information
132
+ */
133
+ export function validateInboxId(id: unknown): IdValidationResult {
134
+ if (id === null || id === undefined) {
135
+ return { valid: false, reason: "Inbox ID is required" };
136
+ }
137
+ if (typeof id !== "string") {
138
+ return { valid: false, reason: `Inbox ID must be a string (received ${typeof id})` };
139
+ }
140
+ if (id.length === 0) {
141
+ return { valid: false, reason: "Inbox ID cannot be empty" };
142
+ }
143
+ if (!id.startsWith("inbox_")) {
144
+ return { valid: false, reason: "Inbox ID must start with 'inbox_' prefix" };
145
+ }
146
+ if (!/^inbox_[a-z0-9]+$/.test(id)) {
147
+ return { valid: false, reason: "Inbox ID contains invalid characters" };
148
+ }
149
+ return { valid: true };
150
+ }
151
+
152
+ /**
153
+ * Assert valid task ID, throw if invalid
154
+ */
155
+ export function assertValidTaskId(id: unknown): asserts id is string {
156
+ const result = validateTaskId(id);
157
+ if (!result.valid) {
158
+ throw new InvalidIdError("task", id, result.reason!);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Assert valid project ID, throw if invalid
164
+ */
165
+ export function assertValidProjectId(id: unknown): asserts id is string {
166
+ const result = validateProjectId(id);
167
+ if (!result.valid) {
168
+ throw new InvalidIdError("project", id, result.reason!);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Assert valid inbox ID, throw if invalid
174
+ */
175
+ export function assertValidInboxId(id: unknown): asserts id is string {
176
+ const result = validateInboxId(id);
177
+ if (!result.valid) {
178
+ throw new InvalidIdError("inbox", id, result.reason!);
179
+ }
180
+ }
@@ -1,3 +1,47 @@
1
- export { generateId, generateTaskId, generateProjectId, generateViewId, isValidProjectId, isValidTaskId } from "./id.js";
1
+ export {
2
+ generateId,
3
+ generateTaskId,
4
+ generateProjectId,
5
+ generateViewId,
6
+ isValidProjectId,
7
+ isValidTaskId,
8
+ generateInboxId,
9
+ isValidInboxId,
10
+ // Validation utilities with detailed error information
11
+ InvalidIdError,
12
+ type IdValidationResult,
13
+ validateTaskId,
14
+ validateProjectId,
15
+ validateInboxId,
16
+ assertValidTaskId,
17
+ assertValidProjectId,
18
+ assertValidInboxId,
19
+ } from "./id.js";
2
20
  export { now, parseRelativeDate, formatDate, formatDisplayDate, isToday, isPastDue, isWithinDays } from "./date.js";
3
- export { parseTaskInput } from "./natural-language.js";
21
+ export { parseTaskInput, parseInboxInput, parseInput, type ParseTarget, type ParsedInput } from "./natural-language.js";
22
+ export {
23
+ MAX_HIERARCHY_DEPTH,
24
+ getTaskLevel,
25
+ validateHierarchyDepth,
26
+ getAncestorIds,
27
+ getDescendantIds,
28
+ getChildTasks,
29
+ getRootTask,
30
+ buildTaskTree,
31
+ type TaskTreeNode,
32
+ } from "./hierarchy.js";
33
+
34
+ // Projection utilities (token optimization)
35
+ export {
36
+ projectTask,
37
+ projectTasks,
38
+ projectTasksPaginated,
39
+ projectProject,
40
+ projectProjects,
41
+ projectInboxItem,
42
+ projectInboxItems,
43
+ formatResponse,
44
+ applyPagination,
45
+ truncate,
46
+ summarizeList,
47
+ } from "./projection.js";