@agentuity/core 1.0.29 → 1.0.31

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.
@@ -3,316 +3,1108 @@ import { buildUrl, toServiceException } from './_util.ts';
3
3
  import { StructuredError } from '../error.ts';
4
4
  import { safeStringify } from '../json.ts';
5
5
 
6
- // Task type enums
6
+ /**
7
+ * Priority level for a task, from highest (`'high'`) to no priority (`'none'`).
8
+ */
7
9
  export type TaskPriority = 'high' | 'medium' | 'low' | 'none';
10
+
11
+ /**
12
+ * The classification of a task.
13
+ *
14
+ * - `'epic'` — Large initiatives that span multiple features or tasks.
15
+ * - `'feature'` — New capabilities to be built.
16
+ * - `'enhancement'` — Improvements to existing features.
17
+ * - `'bug'` — Defects to be fixed.
18
+ * - `'task'` — General work items.
19
+ */
8
20
  export type TaskType = 'epic' | 'feature' | 'enhancement' | 'bug' | 'task';
21
+
22
+ /**
23
+ * The lifecycle status of a task.
24
+ *
25
+ * - `'open'` — Created, not yet started.
26
+ * - `'in_progress'` — Actively being worked on.
27
+ * - `'done'` — Work completed.
28
+ * - `'closed'` — Resolved and closed.
29
+ * - `'cancelled'` — Abandoned.
30
+ */
9
31
  export type TaskStatus = 'open' | 'in_progress' | 'closed' | 'done' | 'cancelled';
10
32
 
11
- // Entity reference (user/project with id + name)
33
+ /**
34
+ * A lightweight reference to a user or project entity, containing just the ID
35
+ * and display name. Used for creator, assignee, closer, and project associations.
36
+ */
12
37
  export interface EntityRef {
38
+ /** Unique identifier of the referenced entity. */
13
39
  id: string;
40
+
41
+ /** Human-readable display name of the entity. */
14
42
  name: string;
15
43
  }
16
44
 
17
- // Task object (returned from API)
45
+ /**
46
+ * The type of user entity.
47
+ *
48
+ * - `'human'` — A human user.
49
+ * - `'agent'` — An AI agent.
50
+ */
51
+ export type UserType = 'human' | 'agent';
52
+
53
+ /**
54
+ * A reference to a user entity with type discrimination.
55
+ * Extends {@link EntityRef} with a {@link UserEntityRef.type | type} field
56
+ * to distinguish between human users and AI agents.
57
+ */
58
+ export interface UserEntityRef extends EntityRef {
59
+ /**
60
+ * The type of user. Defaults to `'human'` if not specified.
61
+ *
62
+ * @default 'human'
63
+ */
64
+ type?: UserType;
65
+ }
66
+
67
+ /**
68
+ * A work item in the task management system.
69
+ *
70
+ * Tasks can represent epics, features, bugs, enhancements, or generic tasks.
71
+ * They support hierarchical organization via {@link Task.parent_id | parent_id},
72
+ * assignment tracking, and lifecycle management through status transitions.
73
+ *
74
+ * @remarks
75
+ * Status transitions are tracked automatically — when a task moves to a new status,
76
+ * the corresponding date field (e.g., {@link Task.open_date | open_date},
77
+ * {@link Task.in_progress_date | in_progress_date}) is set by the server.
78
+ */
18
79
  export interface Task {
80
+ /** Unique identifier for the task. */
19
81
  id: string;
82
+
83
+ /** ISO 8601 timestamp when the task was created. */
20
84
  created_at: string;
85
+
86
+ /** ISO 8601 timestamp when the task was last modified. */
21
87
  updated_at: string;
88
+
89
+ /**
90
+ * The task title.
91
+ *
92
+ * @remarks Must be non-empty and at most 1024 characters.
93
+ */
22
94
  title: string;
95
+
96
+ /**
97
+ * Detailed description of the task.
98
+ *
99
+ * @remarks Maximum 65,536 characters.
100
+ */
23
101
  description?: string;
102
+
103
+ /**
104
+ * Arbitrary key-value metadata attached to the task.
105
+ * Can be used for custom fields, integrations, or filtering.
106
+ */
24
107
  metadata?: Record<string, unknown>;
108
+
109
+ /** The priority level of the task. */
25
110
  priority: TaskPriority;
111
+
112
+ /**
113
+ * ID of the parent task, enabling hierarchical task organization
114
+ * (e.g., an epic containing features).
115
+ */
26
116
  parent_id?: string;
117
+
118
+ /** The classification of this task. */
27
119
  type: TaskType;
120
+
121
+ /** The current lifecycle status of the task. */
28
122
  status: TaskStatus;
123
+
124
+ /** ISO 8601 timestamp when the task was moved to `'open'` status. */
29
125
  open_date?: string;
126
+
127
+ /** ISO 8601 timestamp when the task was moved to `'in_progress'` status. */
30
128
  in_progress_date?: string;
129
+
130
+ /** ISO 8601 timestamp when the task was closed. */
31
131
  closed_date?: string;
132
+
133
+ /**
134
+ * ID of the user who created the task.
135
+ *
136
+ * @remarks Legacy field; prefer {@link Task.creator | creator}.
137
+ */
32
138
  created_id: string;
139
+
140
+ /**
141
+ * ID of the user the task is assigned to.
142
+ *
143
+ * @remarks Legacy field; prefer {@link Task.assignee | assignee}.
144
+ */
33
145
  assigned_id?: string;
146
+
147
+ /**
148
+ * ID of the user who closed the task.
149
+ *
150
+ * @remarks Legacy field; prefer {@link Task.closer | closer}.
151
+ */
34
152
  closed_id?: string;
35
- creator?: EntityRef;
36
- assignee?: EntityRef;
37
- closer?: EntityRef;
153
+
154
+ /** Reference to the user who created the task. */
155
+ creator?: UserEntityRef;
156
+
157
+ /** Reference to the user the task is assigned to. */
158
+ assignee?: UserEntityRef;
159
+
160
+ /** Reference to the user who closed the task. */
161
+ closer?: UserEntityRef;
162
+
163
+ /** Reference to the project this task belongs to. */
38
164
  project?: EntityRef;
165
+
166
+ /** ISO 8601 timestamp when the task was cancelled. */
39
167
  cancelled_date?: string;
40
- deleted: boolean;
168
+
169
+ /** Array of tags associated with this task. */
41
170
  tags?: Tag[];
171
+
172
+ /** Array of comments on this task. */
42
173
  comments?: Comment[];
43
174
  }
44
175
 
45
- // Comment object (returned from API)
176
+ /**
177
+ * A comment on a task, supporting threaded discussion.
178
+ */
46
179
  export interface Comment {
180
+ /** Unique identifier for the comment. */
47
181
  id: string;
182
+
183
+ /** ISO 8601 timestamp when the comment was created. */
48
184
  created_at: string;
185
+
186
+ /** ISO 8601 timestamp when the comment was last edited. */
49
187
  updated_at: string;
188
+
189
+ /** ID of the task this comment belongs to. */
50
190
  task_id: string;
191
+
192
+ /** ID of the user who authored the comment. */
51
193
  user_id: string;
52
- author?: EntityRef;
194
+
195
+ /** Reference to the comment author with display name. */
196
+ author?: UserEntityRef;
197
+
198
+ /**
199
+ * The comment text content.
200
+ *
201
+ * @remarks Must be non-empty.
202
+ */
53
203
  body: string;
54
204
  }
55
205
 
56
- // Tag object (returned from API)
206
+ /**
207
+ * A label that can be applied to tasks for categorization and filtering.
208
+ */
57
209
  export interface Tag {
210
+ /** Unique identifier for the tag. */
58
211
  id: string;
212
+
213
+ /** ISO 8601 timestamp when the tag was created. */
59
214
  created_at: string;
215
+
216
+ /** Display name of the tag. */
60
217
  name: string;
218
+
219
+ /**
220
+ * Optional hex color code for the tag.
221
+ *
222
+ * @example '#ff0000'
223
+ */
61
224
  color?: string;
62
225
  }
63
226
 
64
- // Changelog entry
227
+ /**
228
+ * A record of a single field change on a task, providing an audit trail.
229
+ */
65
230
  export interface TaskChangelogEntry {
231
+ /** Unique identifier for the changelog entry. */
66
232
  id: string;
233
+
234
+ /** ISO 8601 timestamp when the change occurred. */
67
235
  created_at: string;
236
+
237
+ /** ID of the task that was changed. */
68
238
  task_id: string;
239
+
240
+ /**
241
+ * Name of the field that was changed.
242
+ *
243
+ * @example 'status'
244
+ * @example 'priority'
245
+ * @example 'assigned_id'
246
+ */
69
247
  field: string;
248
+
249
+ /** The previous value of the field (as a string), or `undefined` if the field was newly set. */
70
250
  old_value?: string;
251
+
252
+ /** The new value of the field (as a string), or `undefined` if the field was cleared. */
71
253
  new_value?: string;
72
254
  }
73
255
 
74
- // Request params
256
+ /**
257
+ * Parameters for creating a new task.
258
+ */
75
259
  export interface CreateTaskParams {
260
+ /**
261
+ * The task title (required).
262
+ *
263
+ * @remarks Must be non-empty and at most 1024 characters.
264
+ */
76
265
  title: string;
266
+
267
+ /**
268
+ * Detailed description of the task.
269
+ *
270
+ * @remarks Maximum 65,536 characters.
271
+ */
77
272
  description?: string;
273
+
274
+ /** Arbitrary key-value metadata. */
78
275
  metadata?: Record<string, unknown>;
276
+
277
+ /**
278
+ * Priority level. Defaults to `'none'` if not provided.
279
+ *
280
+ * @default 'none'
281
+ */
79
282
  priority?: TaskPriority;
283
+
284
+ /** ID of the parent task for hierarchical organization. */
80
285
  parent_id?: string;
286
+
287
+ /** The task classification (required). */
81
288
  type: TaskType;
289
+
290
+ /**
291
+ * Initial status. Defaults to `'open'` if not provided.
292
+ *
293
+ * @default 'open'
294
+ */
82
295
  status?: TaskStatus;
296
+
297
+ /**
298
+ * ID of the creator.
299
+ *
300
+ * @remarks Legacy field; prefer {@link CreateTaskParams.creator | creator}.
301
+ */
83
302
  created_id: string;
303
+
304
+ /**
305
+ * ID of the assigned user.
306
+ *
307
+ * @remarks Legacy field; prefer {@link CreateTaskParams.assignee | assignee}.
308
+ */
84
309
  assigned_id?: string;
85
- creator?: EntityRef;
86
- assignee?: EntityRef;
310
+
311
+ /** Reference to the user creating the task (id, name, and optional type). */
312
+ creator?: UserEntityRef;
313
+
314
+ /** Reference to the user being assigned the task. */
315
+ assignee?: UserEntityRef;
316
+
317
+ /** Reference to the project this task belongs to. */
87
318
  project?: EntityRef;
319
+
320
+ /** Array of tag IDs to associate with the task at creation. */
88
321
  tag_ids?: string[];
89
322
  }
90
323
 
324
+ /**
325
+ * Parameters for partially updating an existing task.
326
+ *
327
+ * @remarks Only provided fields are modified; omitted fields remain unchanged.
328
+ */
91
329
  export interface UpdateTaskParams {
330
+ /**
331
+ * Updated task title.
332
+ *
333
+ * @remarks Must be non-empty and at most 1024 characters if provided.
334
+ */
92
335
  title?: string;
336
+
337
+ /**
338
+ * Updated description.
339
+ *
340
+ * @remarks Maximum 65,536 characters.
341
+ */
93
342
  description?: string;
343
+
344
+ /** Updated key-value metadata. */
94
345
  metadata?: Record<string, unknown>;
346
+
347
+ /** Updated priority level. */
95
348
  priority?: TaskPriority;
349
+
350
+ /** Updated parent task ID. */
96
351
  parent_id?: string;
352
+
353
+ /** Updated task classification. */
97
354
  type?: TaskType;
355
+
356
+ /** Updated lifecycle status. */
98
357
  status?: TaskStatus;
358
+
359
+ /**
360
+ * Updated assigned user ID.
361
+ *
362
+ * @remarks Legacy field; prefer {@link UpdateTaskParams.assignee | assignee}.
363
+ */
99
364
  assigned_id?: string;
365
+
366
+ /**
367
+ * ID of the user closing the task.
368
+ *
369
+ * @remarks Legacy field; prefer {@link UpdateTaskParams.closer | closer}.
370
+ */
100
371
  closed_id?: string;
101
- assignee?: EntityRef;
102
- closer?: EntityRef;
372
+
373
+ /** Reference to the user being assigned the task. */
374
+ assignee?: UserEntityRef;
375
+
376
+ /** Reference to the user closing the task. */
377
+ closer?: UserEntityRef;
378
+
379
+ /** Reference to the project this task belongs to. */
103
380
  project?: EntityRef;
104
381
  }
105
382
 
383
+ /**
384
+ * Parameters for filtering and paginating the task list.
385
+ */
106
386
  export interface ListTasksParams {
387
+ /** Filter by task status. */
107
388
  status?: TaskStatus;
389
+
390
+ /** Filter by task type. */
108
391
  type?: TaskType;
392
+
393
+ /** Filter by priority level. */
109
394
  priority?: TaskPriority;
395
+
396
+ /** Filter by assigned user ID. */
110
397
  assigned_id?: string;
398
+
399
+ /** Filter by parent task ID (get subtasks). */
111
400
  parent_id?: string;
401
+
402
+ /** Filter by project ID. */
112
403
  project_id?: string;
404
+
405
+ /** Filter by tag ID. */
113
406
  tag_id?: string;
407
+
408
+ /**
409
+ * Filter for soft-deleted tasks.
410
+ *
411
+ * @default false
412
+ */
114
413
  deleted?: boolean;
414
+
415
+ /**
416
+ * Sort field. Prefix with `-` for descending order.
417
+ *
418
+ * @remarks Supported values: `'created_at'`, `'updated_at'`, `'priority'`.
419
+ * Prefix with `-` for descending (e.g., `'-created_at'`).
420
+ */
115
421
  sort?: string;
422
+
423
+ /** Sort direction: `'asc'` or `'desc'`. */
116
424
  order?: 'asc' | 'desc';
425
+
426
+ /** Maximum number of results to return. */
117
427
  limit?: number;
428
+
429
+ /** Number of results to skip for pagination. */
118
430
  offset?: number;
119
431
  }
120
432
 
433
+ /**
434
+ * Paginated list of tasks with total count.
435
+ */
121
436
  export interface ListTasksResult {
437
+ /** Array of tasks matching the query. */
122
438
  tasks: Task[];
439
+
440
+ /** Total number of tasks matching the filters (before pagination). */
123
441
  total: number;
442
+
443
+ /** The limit that was applied. */
124
444
  limit: number;
445
+
446
+ /** The offset that was applied. */
125
447
  offset: number;
126
448
  }
127
449
 
450
+ /**
451
+ * Parameters for batch-deleting tasks by filter.
452
+ * At least one filter must be provided.
453
+ */
454
+ export interface BatchDeleteTasksParams {
455
+ /** Filter by task status. */
456
+ status?: TaskStatus;
457
+
458
+ /** Filter by task type. */
459
+ type?: TaskType;
460
+
461
+ /** Filter by priority level. */
462
+ priority?: TaskPriority;
463
+
464
+ /** Filter by parent task ID (delete subtasks). */
465
+ parent_id?: string;
466
+
467
+ /** Filter by creator ID. */
468
+ created_id?: string;
469
+
470
+ /**
471
+ * Delete tasks older than this duration.
472
+ * Accepts Go-style duration strings: `'30m'`, `'24h'`, `'7d'`, `'2w'`.
473
+ */
474
+ older_than?: string;
475
+
476
+ /**
477
+ * Maximum number of tasks to delete.
478
+ * @default 50
479
+ * @maximum 200
480
+ */
481
+ limit?: number;
482
+ }
483
+
484
+ /**
485
+ * A single task that was deleted in a batch operation.
486
+ */
487
+ export interface BatchDeletedTask {
488
+ /** The ID of the deleted task. */
489
+ id: string;
490
+
491
+ /** The title of the deleted task. */
492
+ title: string;
493
+ }
494
+
495
+ /**
496
+ * Result of a batch delete operation.
497
+ */
498
+ export interface BatchDeleteTasksResult {
499
+ /** Array of tasks that were deleted. */
500
+ deleted: BatchDeletedTask[];
501
+
502
+ /** Total number of tasks deleted. */
503
+ count: number;
504
+ }
505
+
506
+ /**
507
+ * Paginated list of changelog entries for a task.
508
+ */
128
509
  export interface TaskChangelogResult {
510
+ /** Array of change records. */
129
511
  changelog: TaskChangelogEntry[];
512
+
513
+ /** Total number of changelog entries. */
130
514
  total: number;
515
+
516
+ /** Applied limit. */
131
517
  limit: number;
518
+
519
+ /** Applied offset. */
132
520
  offset: number;
133
521
  }
134
522
 
523
+ /**
524
+ * Paginated list of comments on a task.
525
+ */
135
526
  export interface ListCommentsResult {
527
+ /** Array of comments. */
136
528
  comments: Comment[];
529
+
530
+ /** Total number of comments. */
137
531
  total: number;
532
+
533
+ /** Applied limit. */
138
534
  limit: number;
535
+
536
+ /** Applied offset. */
139
537
  offset: number;
140
538
  }
141
539
 
540
+ /**
541
+ * List of all tags in the organization.
542
+ */
142
543
  export interface ListTagsResult {
544
+ /** Array of tags. */
143
545
  tags: Tag[];
144
546
  }
145
547
 
146
- // Attachment types
548
+ /**
549
+ * A file attachment on a task. Attachments are stored in S3 and accessed via presigned URLs.
550
+ */
147
551
  export interface Attachment {
552
+ /** Unique identifier for the attachment. */
148
553
  id: string;
554
+
555
+ /** ISO 8601 timestamp when the attachment was uploaded. */
149
556
  created_at: string;
557
+
558
+ /** ID of the task this attachment belongs to. */
150
559
  task_id: string;
560
+
561
+ /** ID of the user who uploaded the attachment. */
151
562
  user_id: string;
152
- author?: EntityRef;
563
+
564
+ /** Reference to the uploader with display name. */
565
+ author?: UserEntityRef;
566
+
567
+ /** Original filename of the uploaded file. */
153
568
  filename: string;
569
+
570
+ /**
571
+ * MIME type of the file.
572
+ *
573
+ * @example 'application/pdf'
574
+ */
154
575
  content_type?: string;
576
+
577
+ /** File size in bytes. */
155
578
  size?: number;
156
579
  }
157
580
 
581
+ /**
582
+ * Parameters for initiating a file upload to a task.
583
+ */
158
584
  export interface CreateAttachmentParams {
585
+ /** The filename for the attachment (required). */
159
586
  filename: string;
587
+
588
+ /** MIME type of the file. */
160
589
  content_type?: string;
590
+
591
+ /** File size in bytes. */
161
592
  size?: number;
162
593
  }
163
594
 
595
+ /**
596
+ * Response from initiating an attachment upload. Contains a presigned S3 URL for direct upload.
597
+ */
164
598
  export interface PresignUploadResponse {
599
+ /** The created attachment record. */
165
600
  attachment: Attachment;
601
+
602
+ /** A presigned S3 URL to upload the file content via HTTP PUT. */
166
603
  presigned_url: string;
604
+
605
+ /** Number of seconds until the presigned URL expires. */
167
606
  expiry_seconds: number;
168
607
  }
169
608
 
609
+ /**
610
+ * Response containing a presigned S3 URL for downloading an attachment.
611
+ */
170
612
  export interface PresignDownloadResponse {
613
+ /** A presigned S3 URL to download the file via HTTP GET. */
171
614
  presigned_url: string;
615
+
616
+ /** Number of seconds until the presigned URL expires. */
172
617
  expiry_seconds: number;
173
618
  }
174
619
 
620
+ /**
621
+ * List of attachments on a task.
622
+ */
175
623
  export interface ListAttachmentsResult {
624
+ /** Array of attachment records. */
176
625
  attachments: Attachment[];
626
+
627
+ /** Total number of attachments. */
177
628
  total: number;
178
629
  }
179
630
 
631
+ /**
632
+ * List of all users who have been referenced in tasks (as creators, assignees, or closers).
633
+ */
180
634
  export interface ListUsersResult {
181
- users: EntityRef[];
635
+ /** Array of user entity references with type information. */
636
+ users: UserEntityRef[];
182
637
  }
183
638
 
639
+ /**
640
+ * List of all projects that have been referenced in tasks.
641
+ */
184
642
  export interface ListProjectsResult {
643
+ /** Array of project entity references. */
185
644
  projects: EntityRef[];
186
645
  }
187
646
 
647
+ /**
648
+ * Parameters for querying task activity time-series data.
649
+ */
188
650
  export interface TaskActivityParams {
189
- days?: number; // min 7, max 365, default 90
651
+ /**
652
+ * Number of days of activity to retrieve.
653
+ *
654
+ * @remarks Minimum 7, maximum 365.
655
+ * @default 90
656
+ */
657
+ days?: number;
190
658
  }
191
659
 
660
+ /**
661
+ * A single day's snapshot of task counts by status.
662
+ */
192
663
  export interface TaskActivityDataPoint {
193
- date: string; // "2026-02-28"
664
+ /**
665
+ * The date in `YYYY-MM-DD` format.
666
+ *
667
+ * @example '2026-02-28'
668
+ */
669
+ date: string;
670
+
671
+ /** Number of tasks in `'open'` status on this date. */
194
672
  open: number;
673
+
674
+ /** Number of tasks in `'in_progress'` status on this date. */
195
675
  inProgress: number;
676
+
677
+ /** Number of tasks in `'done'` status on this date. */
196
678
  done: number;
679
+
680
+ /** Number of tasks in `'closed'` status on this date. */
197
681
  closed: number;
682
+
683
+ /** Number of tasks in `'cancelled'` status on this date. */
198
684
  cancelled: number;
199
685
  }
200
686
 
687
+ /**
688
+ * Task activity time-series data.
689
+ */
201
690
  export interface TaskActivityResult {
691
+ /** Array of daily activity snapshots, ordered chronologically. */
202
692
  activity: TaskActivityDataPoint[];
693
+
694
+ /** The number of days of data returned. */
203
695
  days: number;
204
696
  }
205
697
 
698
+ /**
699
+ * Interface defining the contract for task storage operations.
700
+ *
701
+ * Implemented by {@link TaskStorageService}.
702
+ */
206
703
  export interface TaskStorage {
704
+ /**
705
+ * Create a new task.
706
+ *
707
+ * @param params - The task creation parameters
708
+ * @returns The newly created task
709
+ */
207
710
  create(params: CreateTaskParams): Promise<Task>;
711
+
712
+ /**
713
+ * Get a task by its ID.
714
+ *
715
+ * @param id - The unique task identifier
716
+ * @returns The task if found, or `null` if not found
717
+ */
208
718
  get(id: string): Promise<Task | null>;
719
+
720
+ /**
721
+ * List tasks with optional filtering and pagination.
722
+ *
723
+ * @param params - Optional filter and pagination parameters
724
+ * @returns Paginated list of matching tasks
725
+ */
209
726
  list(params?: ListTasksParams): Promise<ListTasksResult>;
727
+
728
+ /**
729
+ * Partially update an existing task.
730
+ *
731
+ * @param id - The unique task identifier
732
+ * @param params - Fields to update (only provided fields are changed)
733
+ * @returns The updated task
734
+ */
210
735
  update(id: string, params: UpdateTaskParams): Promise<Task>;
736
+
737
+ /**
738
+ * Close a task by setting its status to closed.
739
+ *
740
+ * @param id - The unique task identifier
741
+ * @returns The closed task
742
+ */
211
743
  close(id: string): Promise<Task>;
744
+
745
+ /**
746
+ * Soft-delete a task, marking it as deleted without permanent removal.
747
+ *
748
+ * @param id - The unique task identifier
749
+ * @returns The soft-deleted task
750
+ */
212
751
  softDelete(id: string): Promise<Task>;
752
+
753
+ /**
754
+ * Batch soft-delete tasks matching the given filters.
755
+ * At least one filter must be provided.
756
+ *
757
+ * @param params - Filters to select which tasks to delete
758
+ * @returns The list of deleted tasks and count
759
+ */
760
+ batchDelete(params: BatchDeleteTasksParams): Promise<BatchDeleteTasksResult>;
761
+
762
+ /**
763
+ * Get the changelog (audit trail) for a task.
764
+ *
765
+ * @param id - The unique task identifier
766
+ * @param params - Optional pagination parameters
767
+ * @returns Paginated list of changelog entries
768
+ */
213
769
  changelog(
214
770
  id: string,
215
771
  params?: { limit?: number; offset?: number }
216
772
  ): Promise<TaskChangelogResult>;
773
+
774
+ /**
775
+ * Create a comment on a task.
776
+ *
777
+ * @param taskId - The ID of the task to comment on
778
+ * @param body - The comment text content
779
+ * @param userId - The ID of the user authoring the comment
780
+ * @param author - Optional entity reference with display name
781
+ * @returns The newly created comment
782
+ */
217
783
  createComment(
218
784
  taskId: string,
219
785
  body: string,
220
786
  userId: string,
221
787
  author?: EntityRef
222
788
  ): Promise<Comment>;
789
+
790
+ /**
791
+ * Get a comment by its ID.
792
+ *
793
+ * @param commentId - The unique comment identifier
794
+ * @returns The comment
795
+ */
223
796
  getComment(commentId: string): Promise<Comment>;
797
+
798
+ /**
799
+ * Update a comment's body text.
800
+ *
801
+ * @param commentId - The unique comment identifier
802
+ * @param body - The new comment text
803
+ * @returns The updated comment
804
+ */
224
805
  updateComment(commentId: string, body: string): Promise<Comment>;
806
+
807
+ /**
808
+ * Delete a comment.
809
+ *
810
+ * @param commentId - The unique comment identifier
811
+ */
225
812
  deleteComment(commentId: string): Promise<void>;
813
+
814
+ /**
815
+ * List comments on a task with optional pagination.
816
+ *
817
+ * @param taskId - The ID of the task
818
+ * @param params - Optional pagination parameters
819
+ * @returns Paginated list of comments
820
+ */
226
821
  listComments(
227
822
  taskId: string,
228
823
  params?: { limit?: number; offset?: number }
229
824
  ): Promise<ListCommentsResult>;
825
+
826
+ /**
827
+ * Create a new tag.
828
+ *
829
+ * @param name - The tag display name
830
+ * @param color - Optional hex color code (e.g., `'#ff0000'`)
831
+ * @returns The newly created tag
832
+ */
230
833
  createTag(name: string, color?: string): Promise<Tag>;
834
+
835
+ /**
836
+ * Get a tag by its ID.
837
+ *
838
+ * @param tagId - The unique tag identifier
839
+ * @returns The tag
840
+ */
231
841
  getTag(tagId: string): Promise<Tag>;
842
+
843
+ /**
844
+ * Update a tag's name and optionally its color.
845
+ *
846
+ * @param tagId - The unique tag identifier
847
+ * @param name - The new tag name
848
+ * @param color - Optional new hex color code
849
+ * @returns The updated tag
850
+ */
232
851
  updateTag(tagId: string, name: string, color?: string): Promise<Tag>;
852
+
853
+ /**
854
+ * Delete a tag.
855
+ *
856
+ * @param tagId - The unique tag identifier
857
+ */
233
858
  deleteTag(tagId: string): Promise<void>;
859
+
860
+ /**
861
+ * List all tags in the organization.
862
+ *
863
+ * @returns List of all tags
864
+ */
234
865
  listTags(): Promise<ListTagsResult>;
866
+
867
+ /**
868
+ * Associate a tag with a task.
869
+ *
870
+ * @param taskId - The ID of the task
871
+ * @param tagId - The ID of the tag to add
872
+ */
235
873
  addTagToTask(taskId: string, tagId: string): Promise<void>;
874
+
875
+ /**
876
+ * Remove a tag association from a task.
877
+ *
878
+ * @param taskId - The ID of the task
879
+ * @param tagId - The ID of the tag to remove
880
+ */
236
881
  removeTagFromTask(taskId: string, tagId: string): Promise<void>;
882
+
883
+ /**
884
+ * List all tags associated with a specific task.
885
+ *
886
+ * @param taskId - The ID of the task
887
+ * @returns Array of tags on the task
888
+ */
237
889
  listTagsForTask(taskId: string): Promise<Tag[]>;
890
+
891
+ /**
892
+ * Initiate a file upload to a task. Returns a presigned S3 URL for direct upload.
893
+ *
894
+ * @param taskId - The ID of the task to attach the file to
895
+ * @param params - Attachment metadata (filename, content type, size)
896
+ * @returns The attachment record and a presigned upload URL
897
+ */
238
898
  uploadAttachment(taskId: string, params: CreateAttachmentParams): Promise<PresignUploadResponse>;
899
+
900
+ /**
901
+ * Confirm that a file upload has completed successfully.
902
+ *
903
+ * @param attachmentId - The unique attachment identifier
904
+ * @returns The confirmed attachment record
905
+ */
239
906
  confirmAttachment(attachmentId: string): Promise<Attachment>;
907
+
908
+ /**
909
+ * Get a presigned S3 URL for downloading an attachment.
910
+ *
911
+ * @param attachmentId - The unique attachment identifier
912
+ * @returns A presigned download URL
913
+ */
240
914
  downloadAttachment(attachmentId: string): Promise<PresignDownloadResponse>;
915
+
916
+ /**
917
+ * List all attachments on a task.
918
+ *
919
+ * @param taskId - The ID of the task
920
+ * @returns List of attachments with total count
921
+ */
241
922
  listAttachments(taskId: string): Promise<ListAttachmentsResult>;
923
+
924
+ /**
925
+ * Delete an attachment.
926
+ *
927
+ * @param attachmentId - The unique attachment identifier
928
+ */
242
929
  deleteAttachment(attachmentId: string): Promise<void>;
930
+
931
+ /**
932
+ * List all users who have been referenced in tasks.
933
+ *
934
+ * @returns List of user entity references
935
+ */
243
936
  listUsers(): Promise<ListUsersResult>;
937
+
938
+ /**
939
+ * List all projects that have been referenced in tasks.
940
+ *
941
+ * @returns List of project entity references
942
+ */
244
943
  listProjects(): Promise<ListProjectsResult>;
944
+
945
+ /**
946
+ * Get task activity time-series data showing daily status counts.
947
+ *
948
+ * @param params - Optional parameters controlling the number of days to retrieve
949
+ * @returns Time-series activity data
950
+ */
245
951
  getActivity(params?: TaskActivityParams): Promise<TaskActivityResult>;
246
952
  }
247
953
 
954
+ /** API version string used for task CRUD, comment, tag, and attachment endpoints. */
248
955
  const TASK_API_VERSION = '2026-02-24';
956
+
957
+ /** Maximum number of tasks that can be deleted in a single batch request. */
958
+ const MAX_BATCH_DELETE_LIMIT = 200;
959
+
960
+ /** API version string used for the task activity analytics endpoint. */
249
961
  const TASK_ACTIVITY_API_VERSION = '2026-02-28';
250
962
 
963
+ /** Thrown when a task ID parameter is empty or not a string. */
251
964
  const TaskIdRequiredError = StructuredError(
252
965
  'TaskIdRequiredError',
253
966
  'Task ID is required and must be a non-empty string'
254
967
  );
255
968
 
969
+ /** Thrown when a task title is empty or not a string. */
256
970
  const TaskTitleRequiredError = StructuredError(
257
971
  'TaskTitleRequiredError',
258
972
  'Task title is required and must be a non-empty string'
259
973
  );
260
974
 
975
+ /** Thrown when a comment ID parameter is empty or not a string. */
261
976
  const CommentIdRequiredError = StructuredError(
262
977
  'CommentIdRequiredError',
263
978
  'Comment ID is required and must be a non-empty string'
264
979
  );
265
980
 
981
+ /** Thrown when a comment body is empty or not a string. */
266
982
  const CommentBodyRequiredError = StructuredError(
267
983
  'CommentBodyRequiredError',
268
984
  'Comment body is required and must be a non-empty string'
269
985
  );
270
986
 
987
+ /** Thrown when a tag ID parameter is empty or not a string. */
271
988
  const TagIdRequiredError = StructuredError(
272
989
  'TagIdRequiredError',
273
990
  'Tag ID is required and must be a non-empty string'
274
991
  );
275
992
 
993
+ /** Thrown when a tag name is empty or not a string. */
276
994
  const TagNameRequiredError = StructuredError(
277
995
  'TagNameRequiredError',
278
996
  'Tag name is required and must be a non-empty string'
279
997
  );
280
998
 
999
+ /** Thrown when an attachment ID parameter is empty or not a string. */
281
1000
  const AttachmentIdRequiredError = StructuredError(
282
1001
  'AttachmentIdRequiredError',
283
1002
  'Attachment ID is required and must be a non-empty string'
284
1003
  );
285
1004
 
1005
+ /** Thrown when a user ID parameter is empty or not a string. */
286
1006
  const UserIdRequiredError = StructuredError(
287
1007
  'UserIdRequiredError',
288
1008
  'User ID is required and must be a non-empty string'
289
1009
  );
290
1010
 
1011
+ /**
1012
+ * Thrown when the API returns a success HTTP status but the response body indicates failure.
1013
+ */
291
1014
  const TaskStorageResponseError = StructuredError('TaskStorageResponseError')<{
292
1015
  status: number;
293
1016
  }>();
294
1017
 
1018
+ /**
1019
+ * Internal API success response envelope for task operations.
1020
+ */
295
1021
  interface TaskSuccessResponse<T> {
296
1022
  success: true;
297
1023
  data: T;
298
1024
  }
299
1025
 
1026
+ /**
1027
+ * Internal API error response envelope for task operations.
1028
+ */
300
1029
  interface TaskErrorResponse {
301
1030
  success: false;
302
1031
  message: string;
303
1032
  }
304
1033
 
1034
+ /**
1035
+ * Discriminated union of API success and error responses for task operations.
1036
+ */
305
1037
  type TaskResponse<T> = TaskSuccessResponse<T> | TaskErrorResponse;
306
1038
 
1039
+ /**
1040
+ * Client for the Agentuity Task management service.
1041
+ *
1042
+ * Provides a full-featured project management API including task CRUD, hierarchical
1043
+ * organization (epics → features → tasks), comments, tags, file attachments via
1044
+ * presigned S3 URLs, changelog tracking, and activity analytics.
1045
+ *
1046
+ * Tasks support lifecycle management through status transitions (`open` → `in_progress`
1047
+ * → `done`/`closed`/`cancelled`) with automatic date tracking for each transition.
1048
+ *
1049
+ * All methods validate inputs client-side and throw structured errors for invalid
1050
+ * parameters. API errors throw {@link ServiceException}.
1051
+ *
1052
+ * @example
1053
+ * ```typescript
1054
+ * const tasks = new TaskStorageService(baseUrl, adapter);
1055
+ *
1056
+ * // Create a task
1057
+ * const task = await tasks.create({
1058
+ * title: 'Implement login flow',
1059
+ * type: 'feature',
1060
+ * created_id: 'user_123',
1061
+ * creator: { id: 'user_123', name: 'Alice' },
1062
+ * priority: 'high',
1063
+ * });
1064
+ *
1065
+ * // Add a comment
1066
+ * await tasks.createComment(task.id, 'Started working on this', 'user_123');
1067
+ *
1068
+ * // List open tasks
1069
+ * const { tasks: openTasks } = await tasks.list({ status: 'open' });
1070
+ * ```
1071
+ */
307
1072
  export class TaskStorageService implements TaskStorage {
308
1073
  #adapter: FetchAdapter;
309
1074
  #baseUrl: string;
310
1075
 
1076
+ /**
1077
+ * Creates a new TaskStorageService instance.
1078
+ *
1079
+ * @param baseUrl - The base URL of the task management API
1080
+ * @param adapter - The HTTP fetch adapter used for making API requests
1081
+ */
311
1082
  constructor(baseUrl: string, adapter: FetchAdapter) {
312
1083
  this.#adapter = adapter;
313
1084
  this.#baseUrl = baseUrl;
314
1085
  }
315
1086
 
1087
+ /**
1088
+ * Create a new task.
1089
+ *
1090
+ * @param params - The task creation parameters including title, type, and optional fields
1091
+ * @returns The newly created task
1092
+ * @throws {@link TaskTitleRequiredError} if the title is empty or not a string
1093
+ * @throws {@link ServiceException} if the API request fails
1094
+ *
1095
+ * @example
1096
+ * ```typescript
1097
+ * const task = await tasks.create({
1098
+ * title: 'Fix login bug',
1099
+ * type: 'bug',
1100
+ * created_id: 'user_123',
1101
+ * priority: 'high',
1102
+ * creator: { id: 'user_123', name: 'Alice' },
1103
+ * project: { id: 'proj_456', name: 'Auth Service' },
1104
+ * });
1105
+ * console.log('Created:', task.id);
1106
+ * ```
1107
+ */
316
1108
  async create(params: CreateTaskParams): Promise<Task> {
317
1109
  if (!params?.title || typeof params.title !== 'string' || params.title.trim().length === 0) {
318
1110
  throw new TaskTitleRequiredError();
@@ -349,6 +1141,24 @@ export class TaskStorageService implements TaskStorage {
349
1141
  throw await toServiceException('POST', url, res.response);
350
1142
  }
351
1143
 
1144
+ /**
1145
+ * Get a task by its ID.
1146
+ *
1147
+ * @param id - The unique task identifier
1148
+ * @returns The task if found, or `null` if the task does not exist
1149
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
1150
+ * @throws {@link ServiceException} if the API request fails
1151
+ *
1152
+ * @example
1153
+ * ```typescript
1154
+ * const task = await tasks.get('task_abc123');
1155
+ * if (task) {
1156
+ * console.log(task.title, task.status);
1157
+ * } else {
1158
+ * console.log('Task not found');
1159
+ * }
1160
+ * ```
1161
+ */
352
1162
  async get(id: string): Promise<Task | null> {
353
1163
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
354
1164
  throw new TaskIdRequiredError();
@@ -383,6 +1193,26 @@ export class TaskStorageService implements TaskStorage {
383
1193
  throw await toServiceException('GET', url, res.response);
384
1194
  }
385
1195
 
1196
+ /**
1197
+ * List tasks with optional filtering and pagination.
1198
+ *
1199
+ * @param params - Optional filter and pagination parameters
1200
+ * @returns Paginated list of tasks matching the filters
1201
+ * @throws {@link ServiceException} if the API request fails
1202
+ *
1203
+ * @example
1204
+ * ```typescript
1205
+ * // List all open high-priority bugs
1206
+ * const result = await tasks.list({
1207
+ * status: 'open',
1208
+ * type: 'bug',
1209
+ * priority: 'high',
1210
+ * sort: '-created_at',
1211
+ * limit: 20,
1212
+ * });
1213
+ * console.log(`Found ${result.total} bugs, showing ${result.tasks.length}`);
1214
+ * ```
1215
+ */
386
1216
  async list(params?: ListTasksParams): Promise<ListTasksResult> {
387
1217
  const queryParams = new URLSearchParams();
388
1218
  if (params?.status) queryParams.set('status', params.status);
@@ -431,6 +1261,26 @@ export class TaskStorageService implements TaskStorage {
431
1261
  throw await toServiceException('GET', url, res.response);
432
1262
  }
433
1263
 
1264
+ /**
1265
+ * Partially update an existing task.
1266
+ *
1267
+ * @param id - The unique task identifier
1268
+ * @param params - Fields to update; only provided fields are changed
1269
+ * @returns The updated task
1270
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
1271
+ * @throws {@link TaskTitleRequiredError} if a title is provided but is empty
1272
+ * @throws {@link ServiceException} if the API request fails
1273
+ *
1274
+ * @example
1275
+ * ```typescript
1276
+ * const updated = await tasks.update('task_abc123', {
1277
+ * status: 'in_progress',
1278
+ * priority: 'high',
1279
+ * assignee: { id: 'user_456', name: 'Bob' },
1280
+ * });
1281
+ * console.log('Updated status:', updated.status);
1282
+ * ```
1283
+ */
434
1284
  async update(id: string, params: UpdateTaskParams): Promise<Task> {
435
1285
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
436
1286
  throw new TaskIdRequiredError();
@@ -469,6 +1319,20 @@ export class TaskStorageService implements TaskStorage {
469
1319
  throw await toServiceException('PATCH', url, res.response);
470
1320
  }
471
1321
 
1322
+ /**
1323
+ * Close a task by setting its status to closed.
1324
+ *
1325
+ * @param id - The unique task identifier
1326
+ * @returns The closed task with updated `closed_date`
1327
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
1328
+ * @throws {@link ServiceException} if the API request fails
1329
+ *
1330
+ * @example
1331
+ * ```typescript
1332
+ * const closed = await tasks.close('task_abc123');
1333
+ * console.log('Closed at:', closed.closed_date);
1334
+ * ```
1335
+ */
472
1336
  async close(id: string): Promise<Task> {
473
1337
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
474
1338
  throw new TaskIdRequiredError();
@@ -499,6 +1363,26 @@ export class TaskStorageService implements TaskStorage {
499
1363
  throw await toServiceException('DELETE', url, res.response);
500
1364
  }
501
1365
 
1366
+ /**
1367
+ * Get the changelog (audit trail) for a task, showing all field changes over time.
1368
+ *
1369
+ * @param id - The unique task identifier
1370
+ * @param params - Optional pagination parameters
1371
+ * @returns Paginated list of changelog entries ordered by most recent first
1372
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
1373
+ * @throws {@link ServiceException} if the API request fails
1374
+ *
1375
+ * @example
1376
+ * ```typescript
1377
+ * const { changelog, total } = await tasks.changelog('task_abc123', {
1378
+ * limit: 10,
1379
+ * offset: 0,
1380
+ * });
1381
+ * for (const entry of changelog) {
1382
+ * console.log(`${entry.field}: ${entry.old_value} → ${entry.new_value}`);
1383
+ * }
1384
+ * ```
1385
+ */
502
1386
  async changelog(
503
1387
  id: string,
504
1388
  params?: { limit?: number; offset?: number }
@@ -542,6 +1426,20 @@ export class TaskStorageService implements TaskStorage {
542
1426
  throw await toServiceException('GET', url, res.response);
543
1427
  }
544
1428
 
1429
+ /**
1430
+ * Soft-delete a task, marking it as deleted without permanent removal.
1431
+ *
1432
+ * @param id - The unique task identifier
1433
+ * @returns The soft-deleted task
1434
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
1435
+ * @throws {@link ServiceException} if the API request fails
1436
+ *
1437
+ * @example
1438
+ * ```typescript
1439
+ * const deleted = await tasks.softDelete('task_abc123');
1440
+ * console.log('Soft-deleted task:', deleted.id);
1441
+ * ```
1442
+ */
545
1443
  async softDelete(id: string): Promise<Task> {
546
1444
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
547
1445
  throw new TaskIdRequiredError();
@@ -575,6 +1473,101 @@ export class TaskStorageService implements TaskStorage {
575
1473
  throw await toServiceException('POST', url, res.response);
576
1474
  }
577
1475
 
1476
+ /**
1477
+ * Batch soft-delete tasks matching the given filters.
1478
+ * At least one filter must be provided. The server caps the limit at 200.
1479
+ *
1480
+ * @param params - Filters to select which tasks to delete
1481
+ * @returns The list of deleted tasks and count
1482
+ * @throws {@link ServiceException} if the API request fails
1483
+ *
1484
+ * @example
1485
+ * ```typescript
1486
+ * const result = await tasks.batchDelete({ status: 'closed', older_than: '7d', limit: 50 });
1487
+ * console.log(`Deleted ${result.count} tasks`);
1488
+ * ```
1489
+ */
1490
+ async batchDelete(params: BatchDeleteTasksParams): Promise<BatchDeleteTasksResult> {
1491
+ const hasFilter =
1492
+ params.status ||
1493
+ params.type ||
1494
+ params.priority ||
1495
+ params.parent_id ||
1496
+ params.created_id ||
1497
+ params.older_than;
1498
+ if (!hasFilter) {
1499
+ throw new Error('At least one filter is required for batch delete');
1500
+ }
1501
+ if (params.limit !== undefined && params.limit > MAX_BATCH_DELETE_LIMIT) {
1502
+ throw new Error(
1503
+ `Batch delete limit must not exceed ${MAX_BATCH_DELETE_LIMIT} (got ${params.limit})`
1504
+ );
1505
+ }
1506
+
1507
+ const url = buildUrl(this.#baseUrl, `/task/delete/batch/${TASK_API_VERSION}`);
1508
+ const signal = AbortSignal.timeout(60_000);
1509
+
1510
+ const body: Record<string, unknown> = {};
1511
+ if (params.status) body.status = params.status;
1512
+ if (params.type) body.type = params.type;
1513
+ if (params.priority) body.priority = params.priority;
1514
+ if (params.parent_id) body.parent_id = params.parent_id;
1515
+ if (params.created_id) body.created_id = params.created_id;
1516
+ if (params.older_than) body.older_than = params.older_than;
1517
+ if (params.limit !== undefined) body.limit = params.limit;
1518
+
1519
+ const res = await this.#adapter.invoke<TaskResponse<BatchDeleteTasksResult>>(url, {
1520
+ method: 'POST',
1521
+ body: safeStringify(body),
1522
+ headers: { 'Content-Type': 'application/json' },
1523
+ signal,
1524
+ telemetry: {
1525
+ name: 'agentuity.task.batchDelete',
1526
+ attributes: {
1527
+ ...(params.status ? { status: params.status } : {}),
1528
+ ...(params.type ? { type: params.type } : {}),
1529
+ ...(params.older_than ? { older_than: params.older_than } : {}),
1530
+ },
1531
+ },
1532
+ });
1533
+
1534
+ if (res.ok) {
1535
+ if (res.data.success) {
1536
+ return res.data.data;
1537
+ }
1538
+ throw new TaskStorageResponseError({
1539
+ status: res.response.status,
1540
+ message: res.data.message,
1541
+ });
1542
+ }
1543
+
1544
+ throw await toServiceException('POST', url, res.response);
1545
+ }
1546
+
1547
+ /**
1548
+ * Create a comment on a task.
1549
+ *
1550
+ * @param taskId - The ID of the task to comment on
1551
+ * @param body - The comment text content (must be non-empty)
1552
+ * @param userId - The ID of the user authoring the comment
1553
+ * @param author - Optional entity reference with the author's display name
1554
+ * @returns The newly created comment
1555
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1556
+ * @throws {@link CommentBodyRequiredError} if the body is empty or not a string
1557
+ * @throws {@link UserIdRequiredError} if the user ID is empty or not a string
1558
+ * @throws {@link ServiceException} if the API request fails
1559
+ *
1560
+ * @example
1561
+ * ```typescript
1562
+ * const comment = await tasks.createComment(
1563
+ * 'task_abc123',
1564
+ * 'This is ready for review.',
1565
+ * 'user_456',
1566
+ * { id: 'user_456', name: 'Bob' },
1567
+ * );
1568
+ * console.log('Comment created:', comment.id);
1569
+ * ```
1570
+ */
578
1571
  async createComment(
579
1572
  taskId: string,
580
1573
  body: string,
@@ -624,6 +1617,20 @@ export class TaskStorageService implements TaskStorage {
624
1617
  throw await toServiceException('POST', url, res.response);
625
1618
  }
626
1619
 
1620
+ /**
1621
+ * Get a comment by its ID.
1622
+ *
1623
+ * @param commentId - The unique comment identifier
1624
+ * @returns The comment
1625
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
1626
+ * @throws {@link ServiceException} if the API request fails
1627
+ *
1628
+ * @example
1629
+ * ```typescript
1630
+ * const comment = await tasks.getComment('comment_xyz789');
1631
+ * console.log(`${comment.author?.name}: ${comment.body}`);
1632
+ * ```
1633
+ */
627
1634
  async getComment(commentId: string): Promise<Comment> {
628
1635
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
629
1636
  throw new CommentIdRequiredError();
@@ -657,6 +1664,25 @@ export class TaskStorageService implements TaskStorage {
657
1664
  throw await toServiceException('GET', url, res.response);
658
1665
  }
659
1666
 
1667
+ /**
1668
+ * Update a comment's body text.
1669
+ *
1670
+ * @param commentId - The unique comment identifier
1671
+ * @param body - The new comment text (must be non-empty)
1672
+ * @returns The updated comment
1673
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
1674
+ * @throws {@link CommentBodyRequiredError} if the body is empty or not a string
1675
+ * @throws {@link ServiceException} if the API request fails
1676
+ *
1677
+ * @example
1678
+ * ```typescript
1679
+ * const updated = await tasks.updateComment(
1680
+ * 'comment_xyz789',
1681
+ * 'Updated: This is now ready for final review.',
1682
+ * );
1683
+ * console.log('Updated at:', updated.updated_at);
1684
+ * ```
1685
+ */
660
1686
  async updateComment(commentId: string, body: string): Promise<Comment> {
661
1687
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
662
1688
  throw new CommentIdRequiredError();
@@ -695,6 +1721,19 @@ export class TaskStorageService implements TaskStorage {
695
1721
  throw await toServiceException('PATCH', url, res.response);
696
1722
  }
697
1723
 
1724
+ /**
1725
+ * Delete a comment permanently.
1726
+ *
1727
+ * @param commentId - The unique comment identifier
1728
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
1729
+ * @throws {@link ServiceException} if the API request fails
1730
+ *
1731
+ * @example
1732
+ * ```typescript
1733
+ * await tasks.deleteComment('comment_xyz789');
1734
+ * console.log('Comment deleted');
1735
+ * ```
1736
+ */
698
1737
  async deleteComment(commentId: string): Promise<void> {
699
1738
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
700
1739
  throw new CommentIdRequiredError();
@@ -728,6 +1767,26 @@ export class TaskStorageService implements TaskStorage {
728
1767
  throw await toServiceException('DELETE', url, res.response);
729
1768
  }
730
1769
 
1770
+ /**
1771
+ * List comments on a task with optional pagination.
1772
+ *
1773
+ * @param taskId - The ID of the task whose comments to list
1774
+ * @param params - Optional pagination parameters
1775
+ * @returns Paginated list of comments
1776
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1777
+ * @throws {@link ServiceException} if the API request fails
1778
+ *
1779
+ * @example
1780
+ * ```typescript
1781
+ * const { comments, total } = await tasks.listComments('task_abc123', {
1782
+ * limit: 25,
1783
+ * offset: 0,
1784
+ * });
1785
+ * for (const c of comments) {
1786
+ * console.log(`${c.author?.name}: ${c.body}`);
1787
+ * }
1788
+ * ```
1789
+ */
731
1790
  async listComments(
732
1791
  taskId: string,
733
1792
  params?: { limit?: number; offset?: number }
@@ -771,6 +1830,21 @@ export class TaskStorageService implements TaskStorage {
771
1830
  throw await toServiceException('GET', url, res.response);
772
1831
  }
773
1832
 
1833
+ /**
1834
+ * Create a new tag for categorizing tasks.
1835
+ *
1836
+ * @param name - The tag display name (must be non-empty)
1837
+ * @param color - Optional hex color code (e.g., `'#ff0000'`)
1838
+ * @returns The newly created tag
1839
+ * @throws {@link TagNameRequiredError} if the name is empty or not a string
1840
+ * @throws {@link ServiceException} if the API request fails
1841
+ *
1842
+ * @example
1843
+ * ```typescript
1844
+ * const tag = await tasks.createTag('urgent', '#ff0000');
1845
+ * console.log('Created tag:', tag.id, tag.name);
1846
+ * ```
1847
+ */
774
1848
  async createTag(name: string, color?: string): Promise<Tag> {
775
1849
  if (!name || typeof name !== 'string' || name.trim().length === 0) {
776
1850
  throw new TagNameRequiredError();
@@ -806,6 +1880,20 @@ export class TaskStorageService implements TaskStorage {
806
1880
  throw await toServiceException('POST', url, res.response);
807
1881
  }
808
1882
 
1883
+ /**
1884
+ * Get a tag by its ID.
1885
+ *
1886
+ * @param tagId - The unique tag identifier
1887
+ * @returns The tag
1888
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
1889
+ * @throws {@link ServiceException} if the API request fails
1890
+ *
1891
+ * @example
1892
+ * ```typescript
1893
+ * const tag = await tasks.getTag('tag_def456');
1894
+ * console.log(`${tag.name} (${tag.color})`);
1895
+ * ```
1896
+ */
809
1897
  async getTag(tagId: string): Promise<Tag> {
810
1898
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
811
1899
  throw new TagIdRequiredError();
@@ -839,6 +1927,23 @@ export class TaskStorageService implements TaskStorage {
839
1927
  throw await toServiceException('GET', url, res.response);
840
1928
  }
841
1929
 
1930
+ /**
1931
+ * Update a tag's name and optionally its color.
1932
+ *
1933
+ * @param tagId - The unique tag identifier
1934
+ * @param name - The new tag name (must be non-empty)
1935
+ * @param color - Optional new hex color code
1936
+ * @returns The updated tag
1937
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
1938
+ * @throws {@link TagNameRequiredError} if the name is empty or not a string
1939
+ * @throws {@link ServiceException} if the API request fails
1940
+ *
1941
+ * @example
1942
+ * ```typescript
1943
+ * const updated = await tasks.updateTag('tag_def456', 'critical', '#cc0000');
1944
+ * console.log('Updated:', updated.name);
1945
+ * ```
1946
+ */
842
1947
  async updateTag(tagId: string, name: string, color?: string): Promise<Tag> {
843
1948
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
844
1949
  throw new TagIdRequiredError();
@@ -880,6 +1985,19 @@ export class TaskStorageService implements TaskStorage {
880
1985
  throw await toServiceException('PATCH', url, res.response);
881
1986
  }
882
1987
 
1988
+ /**
1989
+ * Delete a tag permanently.
1990
+ *
1991
+ * @param tagId - The unique tag identifier
1992
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
1993
+ * @throws {@link ServiceException} if the API request fails
1994
+ *
1995
+ * @example
1996
+ * ```typescript
1997
+ * await tasks.deleteTag('tag_def456');
1998
+ * console.log('Tag deleted');
1999
+ * ```
2000
+ */
883
2001
  async deleteTag(tagId: string): Promise<void> {
884
2002
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
885
2003
  throw new TagIdRequiredError();
@@ -913,6 +2031,20 @@ export class TaskStorageService implements TaskStorage {
913
2031
  throw await toServiceException('DELETE', url, res.response);
914
2032
  }
915
2033
 
2034
+ /**
2035
+ * List all tags in the organization.
2036
+ *
2037
+ * @returns List of all tags
2038
+ * @throws {@link ServiceException} if the API request fails
2039
+ *
2040
+ * @example
2041
+ * ```typescript
2042
+ * const { tags } = await tasks.listTags();
2043
+ * for (const tag of tags) {
2044
+ * console.log(`${tag.name} (${tag.color ?? 'no color'})`);
2045
+ * }
2046
+ * ```
2047
+ */
916
2048
  async listTags(): Promise<ListTagsResult> {
917
2049
  const url = buildUrl(this.#baseUrl, `/task/tags/list/${TASK_API_VERSION}`);
918
2050
  const signal = AbortSignal.timeout(30_000);
@@ -939,6 +2071,21 @@ export class TaskStorageService implements TaskStorage {
939
2071
  throw await toServiceException('GET', url, res.response);
940
2072
  }
941
2073
 
2074
+ /**
2075
+ * Associate a tag with a task.
2076
+ *
2077
+ * @param taskId - The ID of the task
2078
+ * @param tagId - The ID of the tag to add
2079
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
2080
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
2081
+ * @throws {@link ServiceException} if the API request fails
2082
+ *
2083
+ * @example
2084
+ * ```typescript
2085
+ * await tasks.addTagToTask('task_abc123', 'tag_def456');
2086
+ * console.log('Tag added to task');
2087
+ * ```
2088
+ */
942
2089
  async addTagToTask(taskId: string, tagId: string): Promise<void> {
943
2090
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
944
2091
  throw new TaskIdRequiredError();
@@ -975,6 +2122,21 @@ export class TaskStorageService implements TaskStorage {
975
2122
  throw await toServiceException('POST', url, res.response);
976
2123
  }
977
2124
 
2125
+ /**
2126
+ * Remove a tag association from a task.
2127
+ *
2128
+ * @param taskId - The ID of the task
2129
+ * @param tagId - The ID of the tag to remove
2130
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
2131
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
2132
+ * @throws {@link ServiceException} if the API request fails
2133
+ *
2134
+ * @example
2135
+ * ```typescript
2136
+ * await tasks.removeTagFromTask('task_abc123', 'tag_def456');
2137
+ * console.log('Tag removed from task');
2138
+ * ```
2139
+ */
978
2140
  async removeTagFromTask(taskId: string, tagId: string): Promise<void> {
979
2141
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
980
2142
  throw new TaskIdRequiredError();
@@ -1011,6 +2173,20 @@ export class TaskStorageService implements TaskStorage {
1011
2173
  throw await toServiceException('DELETE', url, res.response);
1012
2174
  }
1013
2175
 
2176
+ /**
2177
+ * List all tags associated with a specific task.
2178
+ *
2179
+ * @param taskId - The ID of the task
2180
+ * @returns Array of tags on the task
2181
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
2182
+ * @throws {@link ServiceException} if the API request fails
2183
+ *
2184
+ * @example
2185
+ * ```typescript
2186
+ * const tags = await tasks.listTagsForTask('task_abc123');
2187
+ * console.log('Tags:', tags.map((t) => t.name).join(', '));
2188
+ * ```
2189
+ */
1014
2190
  async listTagsForTask(taskId: string): Promise<Tag[]> {
1015
2191
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
1016
2192
  throw new TaskIdRequiredError();
@@ -1044,8 +2220,33 @@ export class TaskStorageService implements TaskStorage {
1044
2220
  throw await toServiceException('GET', url, res.response);
1045
2221
  }
1046
2222
 
1047
- // Attachment methods
1048
-
2223
+ /**
2224
+ * Initiate a file upload to a task. Returns a presigned S3 URL for direct upload.
2225
+ *
2226
+ * @remarks
2227
+ * After receiving the presigned URL, upload the file content via HTTP PUT to that URL.
2228
+ * Then call {@link TaskStorageService.confirmAttachment | confirmAttachment} to finalize.
2229
+ *
2230
+ * @param taskId - The ID of the task to attach the file to
2231
+ * @param params - Attachment metadata including filename, content type, and size
2232
+ * @returns The created attachment record and a presigned upload URL
2233
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
2234
+ * @throws {@link ServiceException} if the API request fails
2235
+ *
2236
+ * @example
2237
+ * ```typescript
2238
+ * const { attachment, presigned_url } = await tasks.uploadAttachment(
2239
+ * 'task_abc123',
2240
+ * { filename: 'report.pdf', content_type: 'application/pdf', size: 102400 },
2241
+ * );
2242
+ *
2243
+ * // Upload the file to S3
2244
+ * await fetch(presigned_url, { method: 'PUT', body: fileContent });
2245
+ *
2246
+ * // Confirm the upload
2247
+ * await tasks.confirmAttachment(attachment.id);
2248
+ * ```
2249
+ */
1049
2250
  async uploadAttachment(
1050
2251
  taskId: string,
1051
2252
  params: CreateAttachmentParams
@@ -1084,6 +2285,24 @@ export class TaskStorageService implements TaskStorage {
1084
2285
  throw await toServiceException('POST', url, res.response);
1085
2286
  }
1086
2287
 
2288
+ /**
2289
+ * Confirm that a file upload has completed successfully.
2290
+ *
2291
+ * @remarks
2292
+ * Call this after successfully uploading the file to the presigned URL
2293
+ * returned by {@link TaskStorageService.uploadAttachment | uploadAttachment}.
2294
+ *
2295
+ * @param attachmentId - The unique attachment identifier
2296
+ * @returns The confirmed attachment record
2297
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
2298
+ * @throws {@link ServiceException} if the API request fails
2299
+ *
2300
+ * @example
2301
+ * ```typescript
2302
+ * const confirmed = await tasks.confirmAttachment('att_ghi789');
2303
+ * console.log('Confirmed:', confirmed.filename);
2304
+ * ```
2305
+ */
1087
2306
  async confirmAttachment(attachmentId: string): Promise<Attachment> {
1088
2307
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
1089
2308
  throw new AttachmentIdRequiredError();
@@ -1117,6 +2336,20 @@ export class TaskStorageService implements TaskStorage {
1117
2336
  throw await toServiceException('POST', url, res.response);
1118
2337
  }
1119
2338
 
2339
+ /**
2340
+ * Get a presigned S3 URL for downloading an attachment.
2341
+ *
2342
+ * @param attachmentId - The unique attachment identifier
2343
+ * @returns A presigned download URL with expiry information
2344
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
2345
+ * @throws {@link ServiceException} if the API request fails
2346
+ *
2347
+ * @example
2348
+ * ```typescript
2349
+ * const { presigned_url, expiry_seconds } = await tasks.downloadAttachment('att_ghi789');
2350
+ * console.log(`Download URL (expires in ${expiry_seconds}s):`, presigned_url);
2351
+ * ```
2352
+ */
1120
2353
  async downloadAttachment(attachmentId: string): Promise<PresignDownloadResponse> {
1121
2354
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
1122
2355
  throw new AttachmentIdRequiredError();
@@ -1150,6 +2383,22 @@ export class TaskStorageService implements TaskStorage {
1150
2383
  throw await toServiceException('POST', url, res.response);
1151
2384
  }
1152
2385
 
2386
+ /**
2387
+ * List all attachments on a task.
2388
+ *
2389
+ * @param taskId - The ID of the task
2390
+ * @returns List of attachments with total count
2391
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
2392
+ * @throws {@link ServiceException} if the API request fails
2393
+ *
2394
+ * @example
2395
+ * ```typescript
2396
+ * const { attachments, total } = await tasks.listAttachments('task_abc123');
2397
+ * for (const att of attachments) {
2398
+ * console.log(`${att.filename} (${att.content_type}, ${att.size} bytes)`);
2399
+ * }
2400
+ * ```
2401
+ */
1153
2402
  async listAttachments(taskId: string): Promise<ListAttachmentsResult> {
1154
2403
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
1155
2404
  throw new TaskIdRequiredError();
@@ -1183,6 +2432,19 @@ export class TaskStorageService implements TaskStorage {
1183
2432
  throw await toServiceException('GET', url, res.response);
1184
2433
  }
1185
2434
 
2435
+ /**
2436
+ * Delete an attachment permanently.
2437
+ *
2438
+ * @param attachmentId - The unique attachment identifier
2439
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
2440
+ * @throws {@link ServiceException} if the API request fails
2441
+ *
2442
+ * @example
2443
+ * ```typescript
2444
+ * await tasks.deleteAttachment('att_ghi789');
2445
+ * console.log('Attachment deleted');
2446
+ * ```
2447
+ */
1186
2448
  async deleteAttachment(attachmentId: string): Promise<void> {
1187
2449
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
1188
2450
  throw new AttachmentIdRequiredError();
@@ -1216,6 +2478,20 @@ export class TaskStorageService implements TaskStorage {
1216
2478
  throw await toServiceException('DELETE', url, res.response);
1217
2479
  }
1218
2480
 
2481
+ /**
2482
+ * List all users who have been referenced in tasks (as creators, assignees, or closers).
2483
+ *
2484
+ * @returns List of user entity references
2485
+ * @throws {@link ServiceException} if the API request fails
2486
+ *
2487
+ * @example
2488
+ * ```typescript
2489
+ * const { users } = await tasks.listUsers();
2490
+ * for (const user of users) {
2491
+ * console.log(`${user.name} (${user.id})`);
2492
+ * }
2493
+ * ```
2494
+ */
1219
2495
  async listUsers(): Promise<ListUsersResult> {
1220
2496
  const url = buildUrl(this.#baseUrl, `/task/users/${TASK_API_VERSION}`);
1221
2497
  const signal = AbortSignal.timeout(30_000);
@@ -1242,6 +2518,20 @@ export class TaskStorageService implements TaskStorage {
1242
2518
  throw await toServiceException('GET', url, res.response);
1243
2519
  }
1244
2520
 
2521
+ /**
2522
+ * List all projects that have been referenced in tasks.
2523
+ *
2524
+ * @returns List of project entity references
2525
+ * @throws {@link ServiceException} if the API request fails
2526
+ *
2527
+ * @example
2528
+ * ```typescript
2529
+ * const { projects } = await tasks.listProjects();
2530
+ * for (const project of projects) {
2531
+ * console.log(`${project.name} (${project.id})`);
2532
+ * }
2533
+ * ```
2534
+ */
1245
2535
  async listProjects(): Promise<ListProjectsResult> {
1246
2536
  const url = buildUrl(this.#baseUrl, `/task/projects/${TASK_API_VERSION}`);
1247
2537
  const signal = AbortSignal.timeout(30_000);
@@ -1268,6 +2558,22 @@ export class TaskStorageService implements TaskStorage {
1268
2558
  throw await toServiceException('GET', url, res.response);
1269
2559
  }
1270
2560
 
2561
+ /**
2562
+ * Get task activity time-series data showing daily task counts by status.
2563
+ *
2564
+ * @param params - Optional parameters controlling the number of days to retrieve
2565
+ * @returns Time-series activity data with daily snapshots
2566
+ * @throws {@link ServiceException} if the API request fails
2567
+ *
2568
+ * @example
2569
+ * ```typescript
2570
+ * const { activity, days } = await tasks.getActivity({ days: 30 });
2571
+ * console.log(`Activity over ${days} days:`);
2572
+ * for (const point of activity) {
2573
+ * console.log(`${point.date}: ${point.open} open, ${point.inProgress} in progress`);
2574
+ * }
2575
+ * ```
2576
+ */
1271
2577
  async getActivity(params?: TaskActivityParams): Promise<TaskActivityResult> {
1272
2578
  const queryParams = new URLSearchParams();
1273
2579
  if (params?.days !== undefined) queryParams.set('days', String(params.days));