@agentuity/core 1.0.29 → 1.0.30

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,1018 @@ 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
+ * A work item in the task management system.
47
+ *
48
+ * Tasks can represent epics, features, bugs, enhancements, or generic tasks.
49
+ * They support hierarchical organization via {@link Task.parent_id | parent_id},
50
+ * assignment tracking, and lifecycle management through status transitions.
51
+ *
52
+ * @remarks
53
+ * Status transitions are tracked automatically — when a task moves to a new status,
54
+ * the corresponding date field (e.g., {@link Task.open_date | open_date},
55
+ * {@link Task.in_progress_date | in_progress_date}) is set by the server.
56
+ */
18
57
  export interface Task {
58
+ /** Unique identifier for the task. */
19
59
  id: string;
60
+
61
+ /** ISO 8601 timestamp when the task was created. */
20
62
  created_at: string;
63
+
64
+ /** ISO 8601 timestamp when the task was last modified. */
21
65
  updated_at: string;
66
+
67
+ /**
68
+ * The task title.
69
+ *
70
+ * @remarks Must be non-empty and at most 1024 characters.
71
+ */
22
72
  title: string;
73
+
74
+ /**
75
+ * Detailed description of the task.
76
+ *
77
+ * @remarks Maximum 65,536 characters.
78
+ */
23
79
  description?: string;
80
+
81
+ /**
82
+ * Arbitrary key-value metadata attached to the task.
83
+ * Can be used for custom fields, integrations, or filtering.
84
+ */
24
85
  metadata?: Record<string, unknown>;
86
+
87
+ /** The priority level of the task. */
25
88
  priority: TaskPriority;
89
+
90
+ /**
91
+ * ID of the parent task, enabling hierarchical task organization
92
+ * (e.g., an epic containing features).
93
+ */
26
94
  parent_id?: string;
95
+
96
+ /** The classification of this task. */
27
97
  type: TaskType;
98
+
99
+ /** The current lifecycle status of the task. */
28
100
  status: TaskStatus;
101
+
102
+ /** ISO 8601 timestamp when the task was moved to `'open'` status. */
29
103
  open_date?: string;
104
+
105
+ /** ISO 8601 timestamp when the task was moved to `'in_progress'` status. */
30
106
  in_progress_date?: string;
107
+
108
+ /** ISO 8601 timestamp when the task was closed. */
31
109
  closed_date?: string;
110
+
111
+ /**
112
+ * ID of the user who created the task.
113
+ *
114
+ * @remarks Legacy field; prefer {@link Task.creator | creator}.
115
+ */
32
116
  created_id: string;
117
+
118
+ /**
119
+ * ID of the user the task is assigned to.
120
+ *
121
+ * @remarks Legacy field; prefer {@link Task.assignee | assignee}.
122
+ */
33
123
  assigned_id?: string;
124
+
125
+ /**
126
+ * ID of the user who closed the task.
127
+ *
128
+ * @remarks Legacy field; prefer {@link Task.closer | closer}.
129
+ */
34
130
  closed_id?: string;
131
+
132
+ /** Reference to the user who created the task. */
35
133
  creator?: EntityRef;
134
+
135
+ /** Reference to the user the task is assigned to. */
36
136
  assignee?: EntityRef;
137
+
138
+ /** Reference to the user who closed the task. */
37
139
  closer?: EntityRef;
140
+
141
+ /** Reference to the project this task belongs to. */
38
142
  project?: EntityRef;
143
+
144
+ /** ISO 8601 timestamp when the task was cancelled. */
39
145
  cancelled_date?: string;
40
- deleted: boolean;
146
+
147
+ /** Array of tags associated with this task. */
41
148
  tags?: Tag[];
149
+
150
+ /** Array of comments on this task. */
42
151
  comments?: Comment[];
43
152
  }
44
153
 
45
- // Comment object (returned from API)
154
+ /**
155
+ * A comment on a task, supporting threaded discussion.
156
+ */
46
157
  export interface Comment {
158
+ /** Unique identifier for the comment. */
47
159
  id: string;
160
+
161
+ /** ISO 8601 timestamp when the comment was created. */
48
162
  created_at: string;
163
+
164
+ /** ISO 8601 timestamp when the comment was last edited. */
49
165
  updated_at: string;
166
+
167
+ /** ID of the task this comment belongs to. */
50
168
  task_id: string;
169
+
170
+ /** ID of the user who authored the comment. */
51
171
  user_id: string;
172
+
173
+ /** Reference to the comment author with display name. */
52
174
  author?: EntityRef;
175
+
176
+ /**
177
+ * The comment text content.
178
+ *
179
+ * @remarks Must be non-empty.
180
+ */
53
181
  body: string;
54
182
  }
55
183
 
56
- // Tag object (returned from API)
184
+ /**
185
+ * A label that can be applied to tasks for categorization and filtering.
186
+ */
57
187
  export interface Tag {
188
+ /** Unique identifier for the tag. */
58
189
  id: string;
190
+
191
+ /** ISO 8601 timestamp when the tag was created. */
59
192
  created_at: string;
193
+
194
+ /** Display name of the tag. */
60
195
  name: string;
196
+
197
+ /**
198
+ * Optional hex color code for the tag.
199
+ *
200
+ * @example '#ff0000'
201
+ */
61
202
  color?: string;
62
203
  }
63
204
 
64
- // Changelog entry
205
+ /**
206
+ * A record of a single field change on a task, providing an audit trail.
207
+ */
65
208
  export interface TaskChangelogEntry {
209
+ /** Unique identifier for the changelog entry. */
66
210
  id: string;
211
+
212
+ /** ISO 8601 timestamp when the change occurred. */
67
213
  created_at: string;
214
+
215
+ /** ID of the task that was changed. */
68
216
  task_id: string;
217
+
218
+ /**
219
+ * Name of the field that was changed.
220
+ *
221
+ * @example 'status'
222
+ * @example 'priority'
223
+ * @example 'assigned_id'
224
+ */
69
225
  field: string;
226
+
227
+ /** The previous value of the field (as a string), or `undefined` if the field was newly set. */
70
228
  old_value?: string;
229
+
230
+ /** The new value of the field (as a string), or `undefined` if the field was cleared. */
71
231
  new_value?: string;
72
232
  }
73
233
 
74
- // Request params
234
+ /**
235
+ * Parameters for creating a new task.
236
+ */
75
237
  export interface CreateTaskParams {
238
+ /**
239
+ * The task title (required).
240
+ *
241
+ * @remarks Must be non-empty and at most 1024 characters.
242
+ */
76
243
  title: string;
244
+
245
+ /**
246
+ * Detailed description of the task.
247
+ *
248
+ * @remarks Maximum 65,536 characters.
249
+ */
77
250
  description?: string;
251
+
252
+ /** Arbitrary key-value metadata. */
78
253
  metadata?: Record<string, unknown>;
254
+
255
+ /**
256
+ * Priority level. Defaults to `'none'` if not provided.
257
+ *
258
+ * @default 'none'
259
+ */
79
260
  priority?: TaskPriority;
261
+
262
+ /** ID of the parent task for hierarchical organization. */
80
263
  parent_id?: string;
264
+
265
+ /** The task classification (required). */
81
266
  type: TaskType;
267
+
268
+ /**
269
+ * Initial status. Defaults to `'open'` if not provided.
270
+ *
271
+ * @default 'open'
272
+ */
82
273
  status?: TaskStatus;
274
+
275
+ /**
276
+ * ID of the creator.
277
+ *
278
+ * @remarks Legacy field; prefer {@link CreateTaskParams.creator | creator}.
279
+ */
83
280
  created_id: string;
281
+
282
+ /**
283
+ * ID of the assigned user.
284
+ *
285
+ * @remarks Legacy field; prefer {@link CreateTaskParams.assignee | assignee}.
286
+ */
84
287
  assigned_id?: string;
288
+
289
+ /** Reference to the user creating the task (id and name). */
85
290
  creator?: EntityRef;
291
+
292
+ /** Reference to the user being assigned the task. */
86
293
  assignee?: EntityRef;
294
+
295
+ /** Reference to the project this task belongs to. */
87
296
  project?: EntityRef;
297
+
298
+ /** Array of tag IDs to associate with the task at creation. */
88
299
  tag_ids?: string[];
89
300
  }
90
301
 
302
+ /**
303
+ * Parameters for partially updating an existing task.
304
+ *
305
+ * @remarks Only provided fields are modified; omitted fields remain unchanged.
306
+ */
91
307
  export interface UpdateTaskParams {
308
+ /**
309
+ * Updated task title.
310
+ *
311
+ * @remarks Must be non-empty and at most 1024 characters if provided.
312
+ */
92
313
  title?: string;
314
+
315
+ /**
316
+ * Updated description.
317
+ *
318
+ * @remarks Maximum 65,536 characters.
319
+ */
93
320
  description?: string;
321
+
322
+ /** Updated key-value metadata. */
94
323
  metadata?: Record<string, unknown>;
324
+
325
+ /** Updated priority level. */
95
326
  priority?: TaskPriority;
327
+
328
+ /** Updated parent task ID. */
96
329
  parent_id?: string;
330
+
331
+ /** Updated task classification. */
97
332
  type?: TaskType;
333
+
334
+ /** Updated lifecycle status. */
98
335
  status?: TaskStatus;
336
+
337
+ /**
338
+ * Updated assigned user ID.
339
+ *
340
+ * @remarks Legacy field; prefer {@link UpdateTaskParams.assignee | assignee}.
341
+ */
99
342
  assigned_id?: string;
343
+
344
+ /**
345
+ * ID of the user closing the task.
346
+ *
347
+ * @remarks Legacy field; prefer {@link UpdateTaskParams.closer | closer}.
348
+ */
100
349
  closed_id?: string;
350
+
351
+ /** Reference to the user being assigned the task. */
101
352
  assignee?: EntityRef;
353
+
354
+ /** Reference to the user closing the task. */
102
355
  closer?: EntityRef;
356
+
357
+ /** Reference to the project this task belongs to. */
103
358
  project?: EntityRef;
104
359
  }
105
360
 
361
+ /**
362
+ * Parameters for filtering and paginating the task list.
363
+ */
106
364
  export interface ListTasksParams {
365
+ /** Filter by task status. */
107
366
  status?: TaskStatus;
367
+
368
+ /** Filter by task type. */
108
369
  type?: TaskType;
370
+
371
+ /** Filter by priority level. */
109
372
  priority?: TaskPriority;
373
+
374
+ /** Filter by assigned user ID. */
110
375
  assigned_id?: string;
376
+
377
+ /** Filter by parent task ID (get subtasks). */
111
378
  parent_id?: string;
379
+
380
+ /** Filter by project ID. */
112
381
  project_id?: string;
382
+
383
+ /** Filter by tag ID. */
113
384
  tag_id?: string;
385
+
386
+ /**
387
+ * Filter for soft-deleted tasks.
388
+ *
389
+ * @default false
390
+ */
114
391
  deleted?: boolean;
392
+
393
+ /**
394
+ * Sort field. Prefix with `-` for descending order.
395
+ *
396
+ * @remarks Supported values: `'created_at'`, `'updated_at'`, `'priority'`.
397
+ * Prefix with `-` for descending (e.g., `'-created_at'`).
398
+ */
115
399
  sort?: string;
400
+
401
+ /** Sort direction: `'asc'` or `'desc'`. */
116
402
  order?: 'asc' | 'desc';
403
+
404
+ /** Maximum number of results to return. */
117
405
  limit?: number;
406
+
407
+ /** Number of results to skip for pagination. */
118
408
  offset?: number;
119
409
  }
120
410
 
411
+ /**
412
+ * Paginated list of tasks with total count.
413
+ */
121
414
  export interface ListTasksResult {
415
+ /** Array of tasks matching the query. */
122
416
  tasks: Task[];
417
+
418
+ /** Total number of tasks matching the filters (before pagination). */
123
419
  total: number;
420
+
421
+ /** The limit that was applied. */
124
422
  limit: number;
423
+
424
+ /** The offset that was applied. */
125
425
  offset: number;
126
426
  }
127
427
 
428
+ /**
429
+ * Paginated list of changelog entries for a task.
430
+ */
128
431
  export interface TaskChangelogResult {
432
+ /** Array of change records. */
129
433
  changelog: TaskChangelogEntry[];
434
+
435
+ /** Total number of changelog entries. */
130
436
  total: number;
437
+
438
+ /** Applied limit. */
131
439
  limit: number;
440
+
441
+ /** Applied offset. */
132
442
  offset: number;
133
443
  }
134
444
 
445
+ /**
446
+ * Paginated list of comments on a task.
447
+ */
135
448
  export interface ListCommentsResult {
449
+ /** Array of comments. */
136
450
  comments: Comment[];
451
+
452
+ /** Total number of comments. */
137
453
  total: number;
454
+
455
+ /** Applied limit. */
138
456
  limit: number;
457
+
458
+ /** Applied offset. */
139
459
  offset: number;
140
460
  }
141
461
 
462
+ /**
463
+ * List of all tags in the organization.
464
+ */
142
465
  export interface ListTagsResult {
466
+ /** Array of tags. */
143
467
  tags: Tag[];
144
468
  }
145
469
 
146
- // Attachment types
470
+ /**
471
+ * A file attachment on a task. Attachments are stored in S3 and accessed via presigned URLs.
472
+ */
147
473
  export interface Attachment {
474
+ /** Unique identifier for the attachment. */
148
475
  id: string;
476
+
477
+ /** ISO 8601 timestamp when the attachment was uploaded. */
149
478
  created_at: string;
479
+
480
+ /** ID of the task this attachment belongs to. */
150
481
  task_id: string;
482
+
483
+ /** ID of the user who uploaded the attachment. */
151
484
  user_id: string;
485
+
486
+ /** Reference to the uploader with display name. */
152
487
  author?: EntityRef;
488
+
489
+ /** Original filename of the uploaded file. */
153
490
  filename: string;
491
+
492
+ /**
493
+ * MIME type of the file.
494
+ *
495
+ * @example 'application/pdf'
496
+ */
154
497
  content_type?: string;
498
+
499
+ /** File size in bytes. */
155
500
  size?: number;
156
501
  }
157
502
 
503
+ /**
504
+ * Parameters for initiating a file upload to a task.
505
+ */
158
506
  export interface CreateAttachmentParams {
507
+ /** The filename for the attachment (required). */
159
508
  filename: string;
509
+
510
+ /** MIME type of the file. */
160
511
  content_type?: string;
512
+
513
+ /** File size in bytes. */
161
514
  size?: number;
162
515
  }
163
516
 
517
+ /**
518
+ * Response from initiating an attachment upload. Contains a presigned S3 URL for direct upload.
519
+ */
164
520
  export interface PresignUploadResponse {
521
+ /** The created attachment record. */
165
522
  attachment: Attachment;
523
+
524
+ /** A presigned S3 URL to upload the file content via HTTP PUT. */
166
525
  presigned_url: string;
526
+
527
+ /** Number of seconds until the presigned URL expires. */
167
528
  expiry_seconds: number;
168
529
  }
169
530
 
531
+ /**
532
+ * Response containing a presigned S3 URL for downloading an attachment.
533
+ */
170
534
  export interface PresignDownloadResponse {
535
+ /** A presigned S3 URL to download the file via HTTP GET. */
171
536
  presigned_url: string;
537
+
538
+ /** Number of seconds until the presigned URL expires. */
172
539
  expiry_seconds: number;
173
540
  }
174
541
 
542
+ /**
543
+ * List of attachments on a task.
544
+ */
175
545
  export interface ListAttachmentsResult {
546
+ /** Array of attachment records. */
176
547
  attachments: Attachment[];
548
+
549
+ /** Total number of attachments. */
177
550
  total: number;
178
551
  }
179
552
 
553
+ /**
554
+ * List of all users who have been referenced in tasks (as creators, assignees, or closers).
555
+ */
180
556
  export interface ListUsersResult {
557
+ /** Array of user entity references. */
181
558
  users: EntityRef[];
182
559
  }
183
560
 
561
+ /**
562
+ * List of all projects that have been referenced in tasks.
563
+ */
184
564
  export interface ListProjectsResult {
565
+ /** Array of project entity references. */
185
566
  projects: EntityRef[];
186
567
  }
187
568
 
569
+ /**
570
+ * Parameters for querying task activity time-series data.
571
+ */
188
572
  export interface TaskActivityParams {
189
- days?: number; // min 7, max 365, default 90
573
+ /**
574
+ * Number of days of activity to retrieve.
575
+ *
576
+ * @remarks Minimum 7, maximum 365.
577
+ * @default 90
578
+ */
579
+ days?: number;
190
580
  }
191
581
 
582
+ /**
583
+ * A single day's snapshot of task counts by status.
584
+ */
192
585
  export interface TaskActivityDataPoint {
193
- date: string; // "2026-02-28"
586
+ /**
587
+ * The date in `YYYY-MM-DD` format.
588
+ *
589
+ * @example '2026-02-28'
590
+ */
591
+ date: string;
592
+
593
+ /** Number of tasks in `'open'` status on this date. */
194
594
  open: number;
595
+
596
+ /** Number of tasks in `'in_progress'` status on this date. */
195
597
  inProgress: number;
598
+
599
+ /** Number of tasks in `'done'` status on this date. */
196
600
  done: number;
601
+
602
+ /** Number of tasks in `'closed'` status on this date. */
197
603
  closed: number;
604
+
605
+ /** Number of tasks in `'cancelled'` status on this date. */
198
606
  cancelled: number;
199
607
  }
200
608
 
609
+ /**
610
+ * Task activity time-series data.
611
+ */
201
612
  export interface TaskActivityResult {
613
+ /** Array of daily activity snapshots, ordered chronologically. */
202
614
  activity: TaskActivityDataPoint[];
615
+
616
+ /** The number of days of data returned. */
203
617
  days: number;
204
618
  }
205
619
 
620
+ /**
621
+ * Interface defining the contract for task storage operations.
622
+ *
623
+ * Implemented by {@link TaskStorageService}.
624
+ */
206
625
  export interface TaskStorage {
626
+ /**
627
+ * Create a new task.
628
+ *
629
+ * @param params - The task creation parameters
630
+ * @returns The newly created task
631
+ */
207
632
  create(params: CreateTaskParams): Promise<Task>;
633
+
634
+ /**
635
+ * Get a task by its ID.
636
+ *
637
+ * @param id - The unique task identifier
638
+ * @returns The task if found, or `null` if not found
639
+ */
208
640
  get(id: string): Promise<Task | null>;
641
+
642
+ /**
643
+ * List tasks with optional filtering and pagination.
644
+ *
645
+ * @param params - Optional filter and pagination parameters
646
+ * @returns Paginated list of matching tasks
647
+ */
209
648
  list(params?: ListTasksParams): Promise<ListTasksResult>;
649
+
650
+ /**
651
+ * Partially update an existing task.
652
+ *
653
+ * @param id - The unique task identifier
654
+ * @param params - Fields to update (only provided fields are changed)
655
+ * @returns The updated task
656
+ */
210
657
  update(id: string, params: UpdateTaskParams): Promise<Task>;
658
+
659
+ /**
660
+ * Close a task by setting its status to closed.
661
+ *
662
+ * @param id - The unique task identifier
663
+ * @returns The closed task
664
+ */
211
665
  close(id: string): Promise<Task>;
666
+
667
+ /**
668
+ * Soft-delete a task, marking it as deleted without permanent removal.
669
+ *
670
+ * @param id - The unique task identifier
671
+ * @returns The soft-deleted task
672
+ */
212
673
  softDelete(id: string): Promise<Task>;
674
+
675
+ /**
676
+ * Get the changelog (audit trail) for a task.
677
+ *
678
+ * @param id - The unique task identifier
679
+ * @param params - Optional pagination parameters
680
+ * @returns Paginated list of changelog entries
681
+ */
213
682
  changelog(
214
683
  id: string,
215
684
  params?: { limit?: number; offset?: number }
216
685
  ): Promise<TaskChangelogResult>;
686
+
687
+ /**
688
+ * Create a comment on a task.
689
+ *
690
+ * @param taskId - The ID of the task to comment on
691
+ * @param body - The comment text content
692
+ * @param userId - The ID of the user authoring the comment
693
+ * @param author - Optional entity reference with display name
694
+ * @returns The newly created comment
695
+ */
217
696
  createComment(
218
697
  taskId: string,
219
698
  body: string,
220
699
  userId: string,
221
700
  author?: EntityRef
222
701
  ): Promise<Comment>;
702
+
703
+ /**
704
+ * Get a comment by its ID.
705
+ *
706
+ * @param commentId - The unique comment identifier
707
+ * @returns The comment
708
+ */
223
709
  getComment(commentId: string): Promise<Comment>;
710
+
711
+ /**
712
+ * Update a comment's body text.
713
+ *
714
+ * @param commentId - The unique comment identifier
715
+ * @param body - The new comment text
716
+ * @returns The updated comment
717
+ */
224
718
  updateComment(commentId: string, body: string): Promise<Comment>;
719
+
720
+ /**
721
+ * Delete a comment.
722
+ *
723
+ * @param commentId - The unique comment identifier
724
+ */
225
725
  deleteComment(commentId: string): Promise<void>;
726
+
727
+ /**
728
+ * List comments on a task with optional pagination.
729
+ *
730
+ * @param taskId - The ID of the task
731
+ * @param params - Optional pagination parameters
732
+ * @returns Paginated list of comments
733
+ */
226
734
  listComments(
227
735
  taskId: string,
228
736
  params?: { limit?: number; offset?: number }
229
737
  ): Promise<ListCommentsResult>;
738
+
739
+ /**
740
+ * Create a new tag.
741
+ *
742
+ * @param name - The tag display name
743
+ * @param color - Optional hex color code (e.g., `'#ff0000'`)
744
+ * @returns The newly created tag
745
+ */
230
746
  createTag(name: string, color?: string): Promise<Tag>;
747
+
748
+ /**
749
+ * Get a tag by its ID.
750
+ *
751
+ * @param tagId - The unique tag identifier
752
+ * @returns The tag
753
+ */
231
754
  getTag(tagId: string): Promise<Tag>;
755
+
756
+ /**
757
+ * Update a tag's name and optionally its color.
758
+ *
759
+ * @param tagId - The unique tag identifier
760
+ * @param name - The new tag name
761
+ * @param color - Optional new hex color code
762
+ * @returns The updated tag
763
+ */
232
764
  updateTag(tagId: string, name: string, color?: string): Promise<Tag>;
765
+
766
+ /**
767
+ * Delete a tag.
768
+ *
769
+ * @param tagId - The unique tag identifier
770
+ */
233
771
  deleteTag(tagId: string): Promise<void>;
772
+
773
+ /**
774
+ * List all tags in the organization.
775
+ *
776
+ * @returns List of all tags
777
+ */
234
778
  listTags(): Promise<ListTagsResult>;
779
+
780
+ /**
781
+ * Associate a tag with a task.
782
+ *
783
+ * @param taskId - The ID of the task
784
+ * @param tagId - The ID of the tag to add
785
+ */
235
786
  addTagToTask(taskId: string, tagId: string): Promise<void>;
787
+
788
+ /**
789
+ * Remove a tag association from a task.
790
+ *
791
+ * @param taskId - The ID of the task
792
+ * @param tagId - The ID of the tag to remove
793
+ */
236
794
  removeTagFromTask(taskId: string, tagId: string): Promise<void>;
795
+
796
+ /**
797
+ * List all tags associated with a specific task.
798
+ *
799
+ * @param taskId - The ID of the task
800
+ * @returns Array of tags on the task
801
+ */
237
802
  listTagsForTask(taskId: string): Promise<Tag[]>;
803
+
804
+ /**
805
+ * Initiate a file upload to a task. Returns a presigned S3 URL for direct upload.
806
+ *
807
+ * @param taskId - The ID of the task to attach the file to
808
+ * @param params - Attachment metadata (filename, content type, size)
809
+ * @returns The attachment record and a presigned upload URL
810
+ */
238
811
  uploadAttachment(taskId: string, params: CreateAttachmentParams): Promise<PresignUploadResponse>;
812
+
813
+ /**
814
+ * Confirm that a file upload has completed successfully.
815
+ *
816
+ * @param attachmentId - The unique attachment identifier
817
+ * @returns The confirmed attachment record
818
+ */
239
819
  confirmAttachment(attachmentId: string): Promise<Attachment>;
820
+
821
+ /**
822
+ * Get a presigned S3 URL for downloading an attachment.
823
+ *
824
+ * @param attachmentId - The unique attachment identifier
825
+ * @returns A presigned download URL
826
+ */
240
827
  downloadAttachment(attachmentId: string): Promise<PresignDownloadResponse>;
828
+
829
+ /**
830
+ * List all attachments on a task.
831
+ *
832
+ * @param taskId - The ID of the task
833
+ * @returns List of attachments with total count
834
+ */
241
835
  listAttachments(taskId: string): Promise<ListAttachmentsResult>;
836
+
837
+ /**
838
+ * Delete an attachment.
839
+ *
840
+ * @param attachmentId - The unique attachment identifier
841
+ */
242
842
  deleteAttachment(attachmentId: string): Promise<void>;
843
+
844
+ /**
845
+ * List all users who have been referenced in tasks.
846
+ *
847
+ * @returns List of user entity references
848
+ */
243
849
  listUsers(): Promise<ListUsersResult>;
850
+
851
+ /**
852
+ * List all projects that have been referenced in tasks.
853
+ *
854
+ * @returns List of project entity references
855
+ */
244
856
  listProjects(): Promise<ListProjectsResult>;
857
+
858
+ /**
859
+ * Get task activity time-series data showing daily status counts.
860
+ *
861
+ * @param params - Optional parameters controlling the number of days to retrieve
862
+ * @returns Time-series activity data
863
+ */
245
864
  getActivity(params?: TaskActivityParams): Promise<TaskActivityResult>;
246
865
  }
247
866
 
867
+ /** API version string used for task CRUD, comment, tag, and attachment endpoints. */
248
868
  const TASK_API_VERSION = '2026-02-24';
869
+
870
+ /** API version string used for the task activity analytics endpoint. */
249
871
  const TASK_ACTIVITY_API_VERSION = '2026-02-28';
250
872
 
873
+ /** Thrown when a task ID parameter is empty or not a string. */
251
874
  const TaskIdRequiredError = StructuredError(
252
875
  'TaskIdRequiredError',
253
876
  'Task ID is required and must be a non-empty string'
254
877
  );
255
878
 
879
+ /** Thrown when a task title is empty or not a string. */
256
880
  const TaskTitleRequiredError = StructuredError(
257
881
  'TaskTitleRequiredError',
258
882
  'Task title is required and must be a non-empty string'
259
883
  );
260
884
 
885
+ /** Thrown when a comment ID parameter is empty or not a string. */
261
886
  const CommentIdRequiredError = StructuredError(
262
887
  'CommentIdRequiredError',
263
888
  'Comment ID is required and must be a non-empty string'
264
889
  );
265
890
 
891
+ /** Thrown when a comment body is empty or not a string. */
266
892
  const CommentBodyRequiredError = StructuredError(
267
893
  'CommentBodyRequiredError',
268
894
  'Comment body is required and must be a non-empty string'
269
895
  );
270
896
 
897
+ /** Thrown when a tag ID parameter is empty or not a string. */
271
898
  const TagIdRequiredError = StructuredError(
272
899
  'TagIdRequiredError',
273
900
  'Tag ID is required and must be a non-empty string'
274
901
  );
275
902
 
903
+ /** Thrown when a tag name is empty or not a string. */
276
904
  const TagNameRequiredError = StructuredError(
277
905
  'TagNameRequiredError',
278
906
  'Tag name is required and must be a non-empty string'
279
907
  );
280
908
 
909
+ /** Thrown when an attachment ID parameter is empty or not a string. */
281
910
  const AttachmentIdRequiredError = StructuredError(
282
911
  'AttachmentIdRequiredError',
283
912
  'Attachment ID is required and must be a non-empty string'
284
913
  );
285
914
 
915
+ /** Thrown when a user ID parameter is empty or not a string. */
286
916
  const UserIdRequiredError = StructuredError(
287
917
  'UserIdRequiredError',
288
918
  'User ID is required and must be a non-empty string'
289
919
  );
290
920
 
921
+ /**
922
+ * Thrown when the API returns a success HTTP status but the response body indicates failure.
923
+ */
291
924
  const TaskStorageResponseError = StructuredError('TaskStorageResponseError')<{
292
925
  status: number;
293
926
  }>();
294
927
 
928
+ /**
929
+ * Internal API success response envelope for task operations.
930
+ */
295
931
  interface TaskSuccessResponse<T> {
296
932
  success: true;
297
933
  data: T;
298
934
  }
299
935
 
936
+ /**
937
+ * Internal API error response envelope for task operations.
938
+ */
300
939
  interface TaskErrorResponse {
301
940
  success: false;
302
941
  message: string;
303
942
  }
304
943
 
944
+ /**
945
+ * Discriminated union of API success and error responses for task operations.
946
+ */
305
947
  type TaskResponse<T> = TaskSuccessResponse<T> | TaskErrorResponse;
306
948
 
949
+ /**
950
+ * Client for the Agentuity Task management service.
951
+ *
952
+ * Provides a full-featured project management API including task CRUD, hierarchical
953
+ * organization (epics → features → tasks), comments, tags, file attachments via
954
+ * presigned S3 URLs, changelog tracking, and activity analytics.
955
+ *
956
+ * Tasks support lifecycle management through status transitions (`open` → `in_progress`
957
+ * → `done`/`closed`/`cancelled`) with automatic date tracking for each transition.
958
+ *
959
+ * All methods validate inputs client-side and throw structured errors for invalid
960
+ * parameters. API errors throw {@link ServiceException}.
961
+ *
962
+ * @example
963
+ * ```typescript
964
+ * const tasks = new TaskStorageService(baseUrl, adapter);
965
+ *
966
+ * // Create a task
967
+ * const task = await tasks.create({
968
+ * title: 'Implement login flow',
969
+ * type: 'feature',
970
+ * created_id: 'user_123',
971
+ * creator: { id: 'user_123', name: 'Alice' },
972
+ * priority: 'high',
973
+ * });
974
+ *
975
+ * // Add a comment
976
+ * await tasks.createComment(task.id, 'Started working on this', 'user_123');
977
+ *
978
+ * // List open tasks
979
+ * const { tasks: openTasks } = await tasks.list({ status: 'open' });
980
+ * ```
981
+ */
307
982
  export class TaskStorageService implements TaskStorage {
308
983
  #adapter: FetchAdapter;
309
984
  #baseUrl: string;
310
985
 
986
+ /**
987
+ * Creates a new TaskStorageService instance.
988
+ *
989
+ * @param baseUrl - The base URL of the task management API
990
+ * @param adapter - The HTTP fetch adapter used for making API requests
991
+ */
311
992
  constructor(baseUrl: string, adapter: FetchAdapter) {
312
993
  this.#adapter = adapter;
313
994
  this.#baseUrl = baseUrl;
314
995
  }
315
996
 
997
+ /**
998
+ * Create a new task.
999
+ *
1000
+ * @param params - The task creation parameters including title, type, and optional fields
1001
+ * @returns The newly created task
1002
+ * @throws {@link TaskTitleRequiredError} if the title is empty or not a string
1003
+ * @throws {@link ServiceException} if the API request fails
1004
+ *
1005
+ * @example
1006
+ * ```typescript
1007
+ * const task = await tasks.create({
1008
+ * title: 'Fix login bug',
1009
+ * type: 'bug',
1010
+ * created_id: 'user_123',
1011
+ * priority: 'high',
1012
+ * creator: { id: 'user_123', name: 'Alice' },
1013
+ * project: { id: 'proj_456', name: 'Auth Service' },
1014
+ * });
1015
+ * console.log('Created:', task.id);
1016
+ * ```
1017
+ */
316
1018
  async create(params: CreateTaskParams): Promise<Task> {
317
1019
  if (!params?.title || typeof params.title !== 'string' || params.title.trim().length === 0) {
318
1020
  throw new TaskTitleRequiredError();
@@ -349,6 +1051,24 @@ export class TaskStorageService implements TaskStorage {
349
1051
  throw await toServiceException('POST', url, res.response);
350
1052
  }
351
1053
 
1054
+ /**
1055
+ * Get a task by its ID.
1056
+ *
1057
+ * @param id - The unique task identifier
1058
+ * @returns The task if found, or `null` if the task does not exist
1059
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
1060
+ * @throws {@link ServiceException} if the API request fails
1061
+ *
1062
+ * @example
1063
+ * ```typescript
1064
+ * const task = await tasks.get('task_abc123');
1065
+ * if (task) {
1066
+ * console.log(task.title, task.status);
1067
+ * } else {
1068
+ * console.log('Task not found');
1069
+ * }
1070
+ * ```
1071
+ */
352
1072
  async get(id: string): Promise<Task | null> {
353
1073
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
354
1074
  throw new TaskIdRequiredError();
@@ -383,6 +1103,26 @@ export class TaskStorageService implements TaskStorage {
383
1103
  throw await toServiceException('GET', url, res.response);
384
1104
  }
385
1105
 
1106
+ /**
1107
+ * List tasks with optional filtering and pagination.
1108
+ *
1109
+ * @param params - Optional filter and pagination parameters
1110
+ * @returns Paginated list of tasks matching the filters
1111
+ * @throws {@link ServiceException} if the API request fails
1112
+ *
1113
+ * @example
1114
+ * ```typescript
1115
+ * // List all open high-priority bugs
1116
+ * const result = await tasks.list({
1117
+ * status: 'open',
1118
+ * type: 'bug',
1119
+ * priority: 'high',
1120
+ * sort: '-created_at',
1121
+ * limit: 20,
1122
+ * });
1123
+ * console.log(`Found ${result.total} bugs, showing ${result.tasks.length}`);
1124
+ * ```
1125
+ */
386
1126
  async list(params?: ListTasksParams): Promise<ListTasksResult> {
387
1127
  const queryParams = new URLSearchParams();
388
1128
  if (params?.status) queryParams.set('status', params.status);
@@ -431,6 +1171,26 @@ export class TaskStorageService implements TaskStorage {
431
1171
  throw await toServiceException('GET', url, res.response);
432
1172
  }
433
1173
 
1174
+ /**
1175
+ * Partially update an existing task.
1176
+ *
1177
+ * @param id - The unique task identifier
1178
+ * @param params - Fields to update; only provided fields are changed
1179
+ * @returns The updated task
1180
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
1181
+ * @throws {@link TaskTitleRequiredError} if a title is provided but is empty
1182
+ * @throws {@link ServiceException} if the API request fails
1183
+ *
1184
+ * @example
1185
+ * ```typescript
1186
+ * const updated = await tasks.update('task_abc123', {
1187
+ * status: 'in_progress',
1188
+ * priority: 'high',
1189
+ * assignee: { id: 'user_456', name: 'Bob' },
1190
+ * });
1191
+ * console.log('Updated status:', updated.status);
1192
+ * ```
1193
+ */
434
1194
  async update(id: string, params: UpdateTaskParams): Promise<Task> {
435
1195
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
436
1196
  throw new TaskIdRequiredError();
@@ -469,6 +1229,20 @@ export class TaskStorageService implements TaskStorage {
469
1229
  throw await toServiceException('PATCH', url, res.response);
470
1230
  }
471
1231
 
1232
+ /**
1233
+ * Close a task by setting its status to closed.
1234
+ *
1235
+ * @param id - The unique task identifier
1236
+ * @returns The closed task with updated `closed_date`
1237
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
1238
+ * @throws {@link ServiceException} if the API request fails
1239
+ *
1240
+ * @example
1241
+ * ```typescript
1242
+ * const closed = await tasks.close('task_abc123');
1243
+ * console.log('Closed at:', closed.closed_date);
1244
+ * ```
1245
+ */
472
1246
  async close(id: string): Promise<Task> {
473
1247
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
474
1248
  throw new TaskIdRequiredError();
@@ -499,6 +1273,26 @@ export class TaskStorageService implements TaskStorage {
499
1273
  throw await toServiceException('DELETE', url, res.response);
500
1274
  }
501
1275
 
1276
+ /**
1277
+ * Get the changelog (audit trail) for a task, showing all field changes over time.
1278
+ *
1279
+ * @param id - The unique task identifier
1280
+ * @param params - Optional pagination parameters
1281
+ * @returns Paginated list of changelog entries ordered by most recent first
1282
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
1283
+ * @throws {@link ServiceException} if the API request fails
1284
+ *
1285
+ * @example
1286
+ * ```typescript
1287
+ * const { changelog, total } = await tasks.changelog('task_abc123', {
1288
+ * limit: 10,
1289
+ * offset: 0,
1290
+ * });
1291
+ * for (const entry of changelog) {
1292
+ * console.log(`${entry.field}: ${entry.old_value} → ${entry.new_value}`);
1293
+ * }
1294
+ * ```
1295
+ */
502
1296
  async changelog(
503
1297
  id: string,
504
1298
  params?: { limit?: number; offset?: number }
@@ -542,6 +1336,20 @@ export class TaskStorageService implements TaskStorage {
542
1336
  throw await toServiceException('GET', url, res.response);
543
1337
  }
544
1338
 
1339
+ /**
1340
+ * Soft-delete a task, marking it as deleted without permanent removal.
1341
+ *
1342
+ * @param id - The unique task identifier
1343
+ * @returns The soft-deleted task
1344
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
1345
+ * @throws {@link ServiceException} if the API request fails
1346
+ *
1347
+ * @example
1348
+ * ```typescript
1349
+ * const deleted = await tasks.softDelete('task_abc123');
1350
+ * console.log('Soft-deleted task:', deleted.id);
1351
+ * ```
1352
+ */
545
1353
  async softDelete(id: string): Promise<Task> {
546
1354
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
547
1355
  throw new TaskIdRequiredError();
@@ -575,6 +1383,30 @@ export class TaskStorageService implements TaskStorage {
575
1383
  throw await toServiceException('POST', url, res.response);
576
1384
  }
577
1385
 
1386
+ /**
1387
+ * Create a comment on a task.
1388
+ *
1389
+ * @param taskId - The ID of the task to comment on
1390
+ * @param body - The comment text content (must be non-empty)
1391
+ * @param userId - The ID of the user authoring the comment
1392
+ * @param author - Optional entity reference with the author's display name
1393
+ * @returns The newly created comment
1394
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1395
+ * @throws {@link CommentBodyRequiredError} if the body is empty or not a string
1396
+ * @throws {@link UserIdRequiredError} if the user ID is empty or not a string
1397
+ * @throws {@link ServiceException} if the API request fails
1398
+ *
1399
+ * @example
1400
+ * ```typescript
1401
+ * const comment = await tasks.createComment(
1402
+ * 'task_abc123',
1403
+ * 'This is ready for review.',
1404
+ * 'user_456',
1405
+ * { id: 'user_456', name: 'Bob' },
1406
+ * );
1407
+ * console.log('Comment created:', comment.id);
1408
+ * ```
1409
+ */
578
1410
  async createComment(
579
1411
  taskId: string,
580
1412
  body: string,
@@ -624,6 +1456,20 @@ export class TaskStorageService implements TaskStorage {
624
1456
  throw await toServiceException('POST', url, res.response);
625
1457
  }
626
1458
 
1459
+ /**
1460
+ * Get a comment by its ID.
1461
+ *
1462
+ * @param commentId - The unique comment identifier
1463
+ * @returns The comment
1464
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
1465
+ * @throws {@link ServiceException} if the API request fails
1466
+ *
1467
+ * @example
1468
+ * ```typescript
1469
+ * const comment = await tasks.getComment('comment_xyz789');
1470
+ * console.log(`${comment.author?.name}: ${comment.body}`);
1471
+ * ```
1472
+ */
627
1473
  async getComment(commentId: string): Promise<Comment> {
628
1474
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
629
1475
  throw new CommentIdRequiredError();
@@ -657,6 +1503,25 @@ export class TaskStorageService implements TaskStorage {
657
1503
  throw await toServiceException('GET', url, res.response);
658
1504
  }
659
1505
 
1506
+ /**
1507
+ * Update a comment's body text.
1508
+ *
1509
+ * @param commentId - The unique comment identifier
1510
+ * @param body - The new comment text (must be non-empty)
1511
+ * @returns The updated comment
1512
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
1513
+ * @throws {@link CommentBodyRequiredError} if the body is empty or not a string
1514
+ * @throws {@link ServiceException} if the API request fails
1515
+ *
1516
+ * @example
1517
+ * ```typescript
1518
+ * const updated = await tasks.updateComment(
1519
+ * 'comment_xyz789',
1520
+ * 'Updated: This is now ready for final review.',
1521
+ * );
1522
+ * console.log('Updated at:', updated.updated_at);
1523
+ * ```
1524
+ */
660
1525
  async updateComment(commentId: string, body: string): Promise<Comment> {
661
1526
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
662
1527
  throw new CommentIdRequiredError();
@@ -695,6 +1560,19 @@ export class TaskStorageService implements TaskStorage {
695
1560
  throw await toServiceException('PATCH', url, res.response);
696
1561
  }
697
1562
 
1563
+ /**
1564
+ * Delete a comment permanently.
1565
+ *
1566
+ * @param commentId - The unique comment identifier
1567
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
1568
+ * @throws {@link ServiceException} if the API request fails
1569
+ *
1570
+ * @example
1571
+ * ```typescript
1572
+ * await tasks.deleteComment('comment_xyz789');
1573
+ * console.log('Comment deleted');
1574
+ * ```
1575
+ */
698
1576
  async deleteComment(commentId: string): Promise<void> {
699
1577
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
700
1578
  throw new CommentIdRequiredError();
@@ -728,6 +1606,26 @@ export class TaskStorageService implements TaskStorage {
728
1606
  throw await toServiceException('DELETE', url, res.response);
729
1607
  }
730
1608
 
1609
+ /**
1610
+ * List comments on a task with optional pagination.
1611
+ *
1612
+ * @param taskId - The ID of the task whose comments to list
1613
+ * @param params - Optional pagination parameters
1614
+ * @returns Paginated list of comments
1615
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1616
+ * @throws {@link ServiceException} if the API request fails
1617
+ *
1618
+ * @example
1619
+ * ```typescript
1620
+ * const { comments, total } = await tasks.listComments('task_abc123', {
1621
+ * limit: 25,
1622
+ * offset: 0,
1623
+ * });
1624
+ * for (const c of comments) {
1625
+ * console.log(`${c.author?.name}: ${c.body}`);
1626
+ * }
1627
+ * ```
1628
+ */
731
1629
  async listComments(
732
1630
  taskId: string,
733
1631
  params?: { limit?: number; offset?: number }
@@ -771,6 +1669,21 @@ export class TaskStorageService implements TaskStorage {
771
1669
  throw await toServiceException('GET', url, res.response);
772
1670
  }
773
1671
 
1672
+ /**
1673
+ * Create a new tag for categorizing tasks.
1674
+ *
1675
+ * @param name - The tag display name (must be non-empty)
1676
+ * @param color - Optional hex color code (e.g., `'#ff0000'`)
1677
+ * @returns The newly created tag
1678
+ * @throws {@link TagNameRequiredError} if the name is empty or not a string
1679
+ * @throws {@link ServiceException} if the API request fails
1680
+ *
1681
+ * @example
1682
+ * ```typescript
1683
+ * const tag = await tasks.createTag('urgent', '#ff0000');
1684
+ * console.log('Created tag:', tag.id, tag.name);
1685
+ * ```
1686
+ */
774
1687
  async createTag(name: string, color?: string): Promise<Tag> {
775
1688
  if (!name || typeof name !== 'string' || name.trim().length === 0) {
776
1689
  throw new TagNameRequiredError();
@@ -806,6 +1719,20 @@ export class TaskStorageService implements TaskStorage {
806
1719
  throw await toServiceException('POST', url, res.response);
807
1720
  }
808
1721
 
1722
+ /**
1723
+ * Get a tag by its ID.
1724
+ *
1725
+ * @param tagId - The unique tag identifier
1726
+ * @returns The tag
1727
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
1728
+ * @throws {@link ServiceException} if the API request fails
1729
+ *
1730
+ * @example
1731
+ * ```typescript
1732
+ * const tag = await tasks.getTag('tag_def456');
1733
+ * console.log(`${tag.name} (${tag.color})`);
1734
+ * ```
1735
+ */
809
1736
  async getTag(tagId: string): Promise<Tag> {
810
1737
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
811
1738
  throw new TagIdRequiredError();
@@ -839,6 +1766,23 @@ export class TaskStorageService implements TaskStorage {
839
1766
  throw await toServiceException('GET', url, res.response);
840
1767
  }
841
1768
 
1769
+ /**
1770
+ * Update a tag's name and optionally its color.
1771
+ *
1772
+ * @param tagId - The unique tag identifier
1773
+ * @param name - The new tag name (must be non-empty)
1774
+ * @param color - Optional new hex color code
1775
+ * @returns The updated tag
1776
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
1777
+ * @throws {@link TagNameRequiredError} if the name is empty or not a string
1778
+ * @throws {@link ServiceException} if the API request fails
1779
+ *
1780
+ * @example
1781
+ * ```typescript
1782
+ * const updated = await tasks.updateTag('tag_def456', 'critical', '#cc0000');
1783
+ * console.log('Updated:', updated.name);
1784
+ * ```
1785
+ */
842
1786
  async updateTag(tagId: string, name: string, color?: string): Promise<Tag> {
843
1787
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
844
1788
  throw new TagIdRequiredError();
@@ -880,6 +1824,19 @@ export class TaskStorageService implements TaskStorage {
880
1824
  throw await toServiceException('PATCH', url, res.response);
881
1825
  }
882
1826
 
1827
+ /**
1828
+ * Delete a tag permanently.
1829
+ *
1830
+ * @param tagId - The unique tag identifier
1831
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
1832
+ * @throws {@link ServiceException} if the API request fails
1833
+ *
1834
+ * @example
1835
+ * ```typescript
1836
+ * await tasks.deleteTag('tag_def456');
1837
+ * console.log('Tag deleted');
1838
+ * ```
1839
+ */
883
1840
  async deleteTag(tagId: string): Promise<void> {
884
1841
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
885
1842
  throw new TagIdRequiredError();
@@ -913,6 +1870,20 @@ export class TaskStorageService implements TaskStorage {
913
1870
  throw await toServiceException('DELETE', url, res.response);
914
1871
  }
915
1872
 
1873
+ /**
1874
+ * List all tags in the organization.
1875
+ *
1876
+ * @returns List of all tags
1877
+ * @throws {@link ServiceException} if the API request fails
1878
+ *
1879
+ * @example
1880
+ * ```typescript
1881
+ * const { tags } = await tasks.listTags();
1882
+ * for (const tag of tags) {
1883
+ * console.log(`${tag.name} (${tag.color ?? 'no color'})`);
1884
+ * }
1885
+ * ```
1886
+ */
916
1887
  async listTags(): Promise<ListTagsResult> {
917
1888
  const url = buildUrl(this.#baseUrl, `/task/tags/list/${TASK_API_VERSION}`);
918
1889
  const signal = AbortSignal.timeout(30_000);
@@ -939,6 +1910,21 @@ export class TaskStorageService implements TaskStorage {
939
1910
  throw await toServiceException('GET', url, res.response);
940
1911
  }
941
1912
 
1913
+ /**
1914
+ * Associate a tag with a task.
1915
+ *
1916
+ * @param taskId - The ID of the task
1917
+ * @param tagId - The ID of the tag to add
1918
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1919
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
1920
+ * @throws {@link ServiceException} if the API request fails
1921
+ *
1922
+ * @example
1923
+ * ```typescript
1924
+ * await tasks.addTagToTask('task_abc123', 'tag_def456');
1925
+ * console.log('Tag added to task');
1926
+ * ```
1927
+ */
942
1928
  async addTagToTask(taskId: string, tagId: string): Promise<void> {
943
1929
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
944
1930
  throw new TaskIdRequiredError();
@@ -975,6 +1961,21 @@ export class TaskStorageService implements TaskStorage {
975
1961
  throw await toServiceException('POST', url, res.response);
976
1962
  }
977
1963
 
1964
+ /**
1965
+ * Remove a tag association from a task.
1966
+ *
1967
+ * @param taskId - The ID of the task
1968
+ * @param tagId - The ID of the tag to remove
1969
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1970
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
1971
+ * @throws {@link ServiceException} if the API request fails
1972
+ *
1973
+ * @example
1974
+ * ```typescript
1975
+ * await tasks.removeTagFromTask('task_abc123', 'tag_def456');
1976
+ * console.log('Tag removed from task');
1977
+ * ```
1978
+ */
978
1979
  async removeTagFromTask(taskId: string, tagId: string): Promise<void> {
979
1980
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
980
1981
  throw new TaskIdRequiredError();
@@ -1011,6 +2012,20 @@ export class TaskStorageService implements TaskStorage {
1011
2012
  throw await toServiceException('DELETE', url, res.response);
1012
2013
  }
1013
2014
 
2015
+ /**
2016
+ * List all tags associated with a specific task.
2017
+ *
2018
+ * @param taskId - The ID of the task
2019
+ * @returns Array of tags on the task
2020
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
2021
+ * @throws {@link ServiceException} if the API request fails
2022
+ *
2023
+ * @example
2024
+ * ```typescript
2025
+ * const tags = await tasks.listTagsForTask('task_abc123');
2026
+ * console.log('Tags:', tags.map((t) => t.name).join(', '));
2027
+ * ```
2028
+ */
1014
2029
  async listTagsForTask(taskId: string): Promise<Tag[]> {
1015
2030
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
1016
2031
  throw new TaskIdRequiredError();
@@ -1044,8 +2059,33 @@ export class TaskStorageService implements TaskStorage {
1044
2059
  throw await toServiceException('GET', url, res.response);
1045
2060
  }
1046
2061
 
1047
- // Attachment methods
1048
-
2062
+ /**
2063
+ * Initiate a file upload to a task. Returns a presigned S3 URL for direct upload.
2064
+ *
2065
+ * @remarks
2066
+ * After receiving the presigned URL, upload the file content via HTTP PUT to that URL.
2067
+ * Then call {@link TaskStorageService.confirmAttachment | confirmAttachment} to finalize.
2068
+ *
2069
+ * @param taskId - The ID of the task to attach the file to
2070
+ * @param params - Attachment metadata including filename, content type, and size
2071
+ * @returns The created attachment record and a presigned upload URL
2072
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
2073
+ * @throws {@link ServiceException} if the API request fails
2074
+ *
2075
+ * @example
2076
+ * ```typescript
2077
+ * const { attachment, presigned_url } = await tasks.uploadAttachment(
2078
+ * 'task_abc123',
2079
+ * { filename: 'report.pdf', content_type: 'application/pdf', size: 102400 },
2080
+ * );
2081
+ *
2082
+ * // Upload the file to S3
2083
+ * await fetch(presigned_url, { method: 'PUT', body: fileContent });
2084
+ *
2085
+ * // Confirm the upload
2086
+ * await tasks.confirmAttachment(attachment.id);
2087
+ * ```
2088
+ */
1049
2089
  async uploadAttachment(
1050
2090
  taskId: string,
1051
2091
  params: CreateAttachmentParams
@@ -1084,6 +2124,24 @@ export class TaskStorageService implements TaskStorage {
1084
2124
  throw await toServiceException('POST', url, res.response);
1085
2125
  }
1086
2126
 
2127
+ /**
2128
+ * Confirm that a file upload has completed successfully.
2129
+ *
2130
+ * @remarks
2131
+ * Call this after successfully uploading the file to the presigned URL
2132
+ * returned by {@link TaskStorageService.uploadAttachment | uploadAttachment}.
2133
+ *
2134
+ * @param attachmentId - The unique attachment identifier
2135
+ * @returns The confirmed attachment record
2136
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
2137
+ * @throws {@link ServiceException} if the API request fails
2138
+ *
2139
+ * @example
2140
+ * ```typescript
2141
+ * const confirmed = await tasks.confirmAttachment('att_ghi789');
2142
+ * console.log('Confirmed:', confirmed.filename);
2143
+ * ```
2144
+ */
1087
2145
  async confirmAttachment(attachmentId: string): Promise<Attachment> {
1088
2146
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
1089
2147
  throw new AttachmentIdRequiredError();
@@ -1117,6 +2175,20 @@ export class TaskStorageService implements TaskStorage {
1117
2175
  throw await toServiceException('POST', url, res.response);
1118
2176
  }
1119
2177
 
2178
+ /**
2179
+ * Get a presigned S3 URL for downloading an attachment.
2180
+ *
2181
+ * @param attachmentId - The unique attachment identifier
2182
+ * @returns A presigned download URL with expiry information
2183
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
2184
+ * @throws {@link ServiceException} if the API request fails
2185
+ *
2186
+ * @example
2187
+ * ```typescript
2188
+ * const { presigned_url, expiry_seconds } = await tasks.downloadAttachment('att_ghi789');
2189
+ * console.log(`Download URL (expires in ${expiry_seconds}s):`, presigned_url);
2190
+ * ```
2191
+ */
1120
2192
  async downloadAttachment(attachmentId: string): Promise<PresignDownloadResponse> {
1121
2193
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
1122
2194
  throw new AttachmentIdRequiredError();
@@ -1150,6 +2222,22 @@ export class TaskStorageService implements TaskStorage {
1150
2222
  throw await toServiceException('POST', url, res.response);
1151
2223
  }
1152
2224
 
2225
+ /**
2226
+ * List all attachments on a task.
2227
+ *
2228
+ * @param taskId - The ID of the task
2229
+ * @returns List of attachments with total count
2230
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
2231
+ * @throws {@link ServiceException} if the API request fails
2232
+ *
2233
+ * @example
2234
+ * ```typescript
2235
+ * const { attachments, total } = await tasks.listAttachments('task_abc123');
2236
+ * for (const att of attachments) {
2237
+ * console.log(`${att.filename} (${att.content_type}, ${att.size} bytes)`);
2238
+ * }
2239
+ * ```
2240
+ */
1153
2241
  async listAttachments(taskId: string): Promise<ListAttachmentsResult> {
1154
2242
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
1155
2243
  throw new TaskIdRequiredError();
@@ -1183,6 +2271,19 @@ export class TaskStorageService implements TaskStorage {
1183
2271
  throw await toServiceException('GET', url, res.response);
1184
2272
  }
1185
2273
 
2274
+ /**
2275
+ * Delete an attachment permanently.
2276
+ *
2277
+ * @param attachmentId - The unique attachment identifier
2278
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
2279
+ * @throws {@link ServiceException} if the API request fails
2280
+ *
2281
+ * @example
2282
+ * ```typescript
2283
+ * await tasks.deleteAttachment('att_ghi789');
2284
+ * console.log('Attachment deleted');
2285
+ * ```
2286
+ */
1186
2287
  async deleteAttachment(attachmentId: string): Promise<void> {
1187
2288
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
1188
2289
  throw new AttachmentIdRequiredError();
@@ -1216,6 +2317,20 @@ export class TaskStorageService implements TaskStorage {
1216
2317
  throw await toServiceException('DELETE', url, res.response);
1217
2318
  }
1218
2319
 
2320
+ /**
2321
+ * List all users who have been referenced in tasks (as creators, assignees, or closers).
2322
+ *
2323
+ * @returns List of user entity references
2324
+ * @throws {@link ServiceException} if the API request fails
2325
+ *
2326
+ * @example
2327
+ * ```typescript
2328
+ * const { users } = await tasks.listUsers();
2329
+ * for (const user of users) {
2330
+ * console.log(`${user.name} (${user.id})`);
2331
+ * }
2332
+ * ```
2333
+ */
1219
2334
  async listUsers(): Promise<ListUsersResult> {
1220
2335
  const url = buildUrl(this.#baseUrl, `/task/users/${TASK_API_VERSION}`);
1221
2336
  const signal = AbortSignal.timeout(30_000);
@@ -1242,6 +2357,20 @@ export class TaskStorageService implements TaskStorage {
1242
2357
  throw await toServiceException('GET', url, res.response);
1243
2358
  }
1244
2359
 
2360
+ /**
2361
+ * List all projects that have been referenced in tasks.
2362
+ *
2363
+ * @returns List of project entity references
2364
+ * @throws {@link ServiceException} if the API request fails
2365
+ *
2366
+ * @example
2367
+ * ```typescript
2368
+ * const { projects } = await tasks.listProjects();
2369
+ * for (const project of projects) {
2370
+ * console.log(`${project.name} (${project.id})`);
2371
+ * }
2372
+ * ```
2373
+ */
1245
2374
  async listProjects(): Promise<ListProjectsResult> {
1246
2375
  const url = buildUrl(this.#baseUrl, `/task/projects/${TASK_API_VERSION}`);
1247
2376
  const signal = AbortSignal.timeout(30_000);
@@ -1268,6 +2397,22 @@ export class TaskStorageService implements TaskStorage {
1268
2397
  throw await toServiceException('GET', url, res.response);
1269
2398
  }
1270
2399
 
2400
+ /**
2401
+ * Get task activity time-series data showing daily task counts by status.
2402
+ *
2403
+ * @param params - Optional parameters controlling the number of days to retrieve
2404
+ * @returns Time-series activity data with daily snapshots
2405
+ * @throws {@link ServiceException} if the API request fails
2406
+ *
2407
+ * @example
2408
+ * ```typescript
2409
+ * const { activity, days } = await tasks.getActivity({ days: 30 });
2410
+ * console.log(`Activity over ${days} days:`);
2411
+ * for (const point of activity) {
2412
+ * console.log(`${point.date}: ${point.open} open, ${point.inProgress} in progress`);
2413
+ * }
2414
+ * ```
2415
+ */
1271
2416
  async getActivity(params?: TaskActivityParams): Promise<TaskActivityResult> {
1272
2417
  const queryParams = new URLSearchParams();
1273
2418
  if (params?.days !== undefined) queryParams.set('days', String(params.days));