@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.
@@ -1,24 +1,97 @@
1
1
  import { buildUrl, toServiceException } from "./_util.js";
2
2
  import { StructuredError } from "../error.js";
3
3
  import { safeStringify } from "../json.js";
4
+ /** API version string used for task CRUD, comment, tag, and attachment endpoints. */
4
5
  const TASK_API_VERSION = '2026-02-24';
6
+ /** API version string used for the task activity analytics endpoint. */
5
7
  const TASK_ACTIVITY_API_VERSION = '2026-02-28';
8
+ /** Thrown when a task ID parameter is empty or not a string. */
6
9
  const TaskIdRequiredError = StructuredError('TaskIdRequiredError', 'Task ID is required and must be a non-empty string');
10
+ /** Thrown when a task title is empty or not a string. */
7
11
  const TaskTitleRequiredError = StructuredError('TaskTitleRequiredError', 'Task title is required and must be a non-empty string');
12
+ /** Thrown when a comment ID parameter is empty or not a string. */
8
13
  const CommentIdRequiredError = StructuredError('CommentIdRequiredError', 'Comment ID is required and must be a non-empty string');
14
+ /** Thrown when a comment body is empty or not a string. */
9
15
  const CommentBodyRequiredError = StructuredError('CommentBodyRequiredError', 'Comment body is required and must be a non-empty string');
16
+ /** Thrown when a tag ID parameter is empty or not a string. */
10
17
  const TagIdRequiredError = StructuredError('TagIdRequiredError', 'Tag ID is required and must be a non-empty string');
18
+ /** Thrown when a tag name is empty or not a string. */
11
19
  const TagNameRequiredError = StructuredError('TagNameRequiredError', 'Tag name is required and must be a non-empty string');
20
+ /** Thrown when an attachment ID parameter is empty or not a string. */
12
21
  const AttachmentIdRequiredError = StructuredError('AttachmentIdRequiredError', 'Attachment ID is required and must be a non-empty string');
22
+ /** Thrown when a user ID parameter is empty or not a string. */
13
23
  const UserIdRequiredError = StructuredError('UserIdRequiredError', 'User ID is required and must be a non-empty string');
24
+ /**
25
+ * Thrown when the API returns a success HTTP status but the response body indicates failure.
26
+ */
14
27
  const TaskStorageResponseError = StructuredError('TaskStorageResponseError')();
28
+ /**
29
+ * Client for the Agentuity Task management service.
30
+ *
31
+ * Provides a full-featured project management API including task CRUD, hierarchical
32
+ * organization (epics → features → tasks), comments, tags, file attachments via
33
+ * presigned S3 URLs, changelog tracking, and activity analytics.
34
+ *
35
+ * Tasks support lifecycle management through status transitions (`open` → `in_progress`
36
+ * → `done`/`closed`/`cancelled`) with automatic date tracking for each transition.
37
+ *
38
+ * All methods validate inputs client-side and throw structured errors for invalid
39
+ * parameters. API errors throw {@link ServiceException}.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const tasks = new TaskStorageService(baseUrl, adapter);
44
+ *
45
+ * // Create a task
46
+ * const task = await tasks.create({
47
+ * title: 'Implement login flow',
48
+ * type: 'feature',
49
+ * created_id: 'user_123',
50
+ * creator: { id: 'user_123', name: 'Alice' },
51
+ * priority: 'high',
52
+ * });
53
+ *
54
+ * // Add a comment
55
+ * await tasks.createComment(task.id, 'Started working on this', 'user_123');
56
+ *
57
+ * // List open tasks
58
+ * const { tasks: openTasks } = await tasks.list({ status: 'open' });
59
+ * ```
60
+ */
15
61
  export class TaskStorageService {
16
62
  #adapter;
17
63
  #baseUrl;
64
+ /**
65
+ * Creates a new TaskStorageService instance.
66
+ *
67
+ * @param baseUrl - The base URL of the task management API
68
+ * @param adapter - The HTTP fetch adapter used for making API requests
69
+ */
18
70
  constructor(baseUrl, adapter) {
19
71
  this.#adapter = adapter;
20
72
  this.#baseUrl = baseUrl;
21
73
  }
74
+ /**
75
+ * Create a new task.
76
+ *
77
+ * @param params - The task creation parameters including title, type, and optional fields
78
+ * @returns The newly created task
79
+ * @throws {@link TaskTitleRequiredError} if the title is empty or not a string
80
+ * @throws {@link ServiceException} if the API request fails
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * const task = await tasks.create({
85
+ * title: 'Fix login bug',
86
+ * type: 'bug',
87
+ * created_id: 'user_123',
88
+ * priority: 'high',
89
+ * creator: { id: 'user_123', name: 'Alice' },
90
+ * project: { id: 'proj_456', name: 'Auth Service' },
91
+ * });
92
+ * console.log('Created:', task.id);
93
+ * ```
94
+ */
22
95
  async create(params) {
23
96
  if (!params?.title || typeof params.title !== 'string' || params.title.trim().length === 0) {
24
97
  throw new TaskTitleRequiredError();
@@ -50,6 +123,24 @@ export class TaskStorageService {
50
123
  }
51
124
  throw await toServiceException('POST', url, res.response);
52
125
  }
126
+ /**
127
+ * Get a task by its ID.
128
+ *
129
+ * @param id - The unique task identifier
130
+ * @returns The task if found, or `null` if the task does not exist
131
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
132
+ * @throws {@link ServiceException} if the API request fails
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * const task = await tasks.get('task_abc123');
137
+ * if (task) {
138
+ * console.log(task.title, task.status);
139
+ * } else {
140
+ * console.log('Task not found');
141
+ * }
142
+ * ```
143
+ */
53
144
  async get(id) {
54
145
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
55
146
  throw new TaskIdRequiredError();
@@ -78,6 +169,26 @@ export class TaskStorageService {
78
169
  }
79
170
  throw await toServiceException('GET', url, res.response);
80
171
  }
172
+ /**
173
+ * List tasks with optional filtering and pagination.
174
+ *
175
+ * @param params - Optional filter and pagination parameters
176
+ * @returns Paginated list of tasks matching the filters
177
+ * @throws {@link ServiceException} if the API request fails
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * // List all open high-priority bugs
182
+ * const result = await tasks.list({
183
+ * status: 'open',
184
+ * type: 'bug',
185
+ * priority: 'high',
186
+ * sort: '-created_at',
187
+ * limit: 20,
188
+ * });
189
+ * console.log(`Found ${result.total} bugs, showing ${result.tasks.length}`);
190
+ * ```
191
+ */
81
192
  async list(params) {
82
193
  const queryParams = new URLSearchParams();
83
194
  if (params?.status)
@@ -130,6 +241,26 @@ export class TaskStorageService {
130
241
  }
131
242
  throw await toServiceException('GET', url, res.response);
132
243
  }
244
+ /**
245
+ * Partially update an existing task.
246
+ *
247
+ * @param id - The unique task identifier
248
+ * @param params - Fields to update; only provided fields are changed
249
+ * @returns The updated task
250
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
251
+ * @throws {@link TaskTitleRequiredError} if a title is provided but is empty
252
+ * @throws {@link ServiceException} if the API request fails
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const updated = await tasks.update('task_abc123', {
257
+ * status: 'in_progress',
258
+ * priority: 'high',
259
+ * assignee: { id: 'user_456', name: 'Bob' },
260
+ * });
261
+ * console.log('Updated status:', updated.status);
262
+ * ```
263
+ */
133
264
  async update(id, params) {
134
265
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
135
266
  throw new TaskIdRequiredError();
@@ -161,6 +292,20 @@ export class TaskStorageService {
161
292
  }
162
293
  throw await toServiceException('PATCH', url, res.response);
163
294
  }
295
+ /**
296
+ * Close a task by setting its status to closed.
297
+ *
298
+ * @param id - The unique task identifier
299
+ * @returns The closed task with updated `closed_date`
300
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
301
+ * @throws {@link ServiceException} if the API request fails
302
+ *
303
+ * @example
304
+ * ```typescript
305
+ * const closed = await tasks.close('task_abc123');
306
+ * console.log('Closed at:', closed.closed_date);
307
+ * ```
308
+ */
164
309
  async close(id) {
165
310
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
166
311
  throw new TaskIdRequiredError();
@@ -186,6 +331,26 @@ export class TaskStorageService {
186
331
  }
187
332
  throw await toServiceException('DELETE', url, res.response);
188
333
  }
334
+ /**
335
+ * Get the changelog (audit trail) for a task, showing all field changes over time.
336
+ *
337
+ * @param id - The unique task identifier
338
+ * @param params - Optional pagination parameters
339
+ * @returns Paginated list of changelog entries ordered by most recent first
340
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
341
+ * @throws {@link ServiceException} if the API request fails
342
+ *
343
+ * @example
344
+ * ```typescript
345
+ * const { changelog, total } = await tasks.changelog('task_abc123', {
346
+ * limit: 10,
347
+ * offset: 0,
348
+ * });
349
+ * for (const entry of changelog) {
350
+ * console.log(`${entry.field}: ${entry.old_value} → ${entry.new_value}`);
351
+ * }
352
+ * ```
353
+ */
189
354
  async changelog(id, params) {
190
355
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
191
356
  throw new TaskIdRequiredError();
@@ -217,6 +382,20 @@ export class TaskStorageService {
217
382
  }
218
383
  throw await toServiceException('GET', url, res.response);
219
384
  }
385
+ /**
386
+ * Soft-delete a task, marking it as deleted without permanent removal.
387
+ *
388
+ * @param id - The unique task identifier
389
+ * @returns The soft-deleted task
390
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
391
+ * @throws {@link ServiceException} if the API request fails
392
+ *
393
+ * @example
394
+ * ```typescript
395
+ * const deleted = await tasks.softDelete('task_abc123');
396
+ * console.log('Soft-deleted task:', deleted.id);
397
+ * ```
398
+ */
220
399
  async softDelete(id) {
221
400
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
222
401
  throw new TaskIdRequiredError();
@@ -242,6 +421,30 @@ export class TaskStorageService {
242
421
  }
243
422
  throw await toServiceException('POST', url, res.response);
244
423
  }
424
+ /**
425
+ * Create a comment on a task.
426
+ *
427
+ * @param taskId - The ID of the task to comment on
428
+ * @param body - The comment text content (must be non-empty)
429
+ * @param userId - The ID of the user authoring the comment
430
+ * @param author - Optional entity reference with the author's display name
431
+ * @returns The newly created comment
432
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
433
+ * @throws {@link CommentBodyRequiredError} if the body is empty or not a string
434
+ * @throws {@link UserIdRequiredError} if the user ID is empty or not a string
435
+ * @throws {@link ServiceException} if the API request fails
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * const comment = await tasks.createComment(
440
+ * 'task_abc123',
441
+ * 'This is ready for review.',
442
+ * 'user_456',
443
+ * { id: 'user_456', name: 'Bob' },
444
+ * );
445
+ * console.log('Comment created:', comment.id);
446
+ * ```
447
+ */
245
448
  async createComment(taskId, body, userId, author) {
246
449
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
247
450
  throw new TaskIdRequiredError();
@@ -278,6 +481,20 @@ export class TaskStorageService {
278
481
  }
279
482
  throw await toServiceException('POST', url, res.response);
280
483
  }
484
+ /**
485
+ * Get a comment by its ID.
486
+ *
487
+ * @param commentId - The unique comment identifier
488
+ * @returns The comment
489
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
490
+ * @throws {@link ServiceException} if the API request fails
491
+ *
492
+ * @example
493
+ * ```typescript
494
+ * const comment = await tasks.getComment('comment_xyz789');
495
+ * console.log(`${comment.author?.name}: ${comment.body}`);
496
+ * ```
497
+ */
281
498
  async getComment(commentId) {
282
499
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
283
500
  throw new CommentIdRequiredError();
@@ -303,6 +520,25 @@ export class TaskStorageService {
303
520
  }
304
521
  throw await toServiceException('GET', url, res.response);
305
522
  }
523
+ /**
524
+ * Update a comment's body text.
525
+ *
526
+ * @param commentId - The unique comment identifier
527
+ * @param body - The new comment text (must be non-empty)
528
+ * @returns The updated comment
529
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
530
+ * @throws {@link CommentBodyRequiredError} if the body is empty or not a string
531
+ * @throws {@link ServiceException} if the API request fails
532
+ *
533
+ * @example
534
+ * ```typescript
535
+ * const updated = await tasks.updateComment(
536
+ * 'comment_xyz789',
537
+ * 'Updated: This is now ready for final review.',
538
+ * );
539
+ * console.log('Updated at:', updated.updated_at);
540
+ * ```
541
+ */
306
542
  async updateComment(commentId, body) {
307
543
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
308
544
  throw new CommentIdRequiredError();
@@ -333,6 +569,19 @@ export class TaskStorageService {
333
569
  }
334
570
  throw await toServiceException('PATCH', url, res.response);
335
571
  }
572
+ /**
573
+ * Delete a comment permanently.
574
+ *
575
+ * @param commentId - The unique comment identifier
576
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
577
+ * @throws {@link ServiceException} if the API request fails
578
+ *
579
+ * @example
580
+ * ```typescript
581
+ * await tasks.deleteComment('comment_xyz789');
582
+ * console.log('Comment deleted');
583
+ * ```
584
+ */
336
585
  async deleteComment(commentId) {
337
586
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
338
587
  throw new CommentIdRequiredError();
@@ -358,6 +607,26 @@ export class TaskStorageService {
358
607
  }
359
608
  throw await toServiceException('DELETE', url, res.response);
360
609
  }
610
+ /**
611
+ * List comments on a task with optional pagination.
612
+ *
613
+ * @param taskId - The ID of the task whose comments to list
614
+ * @param params - Optional pagination parameters
615
+ * @returns Paginated list of comments
616
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
617
+ * @throws {@link ServiceException} if the API request fails
618
+ *
619
+ * @example
620
+ * ```typescript
621
+ * const { comments, total } = await tasks.listComments('task_abc123', {
622
+ * limit: 25,
623
+ * offset: 0,
624
+ * });
625
+ * for (const c of comments) {
626
+ * console.log(`${c.author?.name}: ${c.body}`);
627
+ * }
628
+ * ```
629
+ */
361
630
  async listComments(taskId, params) {
362
631
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
363
632
  throw new TaskIdRequiredError();
@@ -389,6 +658,21 @@ export class TaskStorageService {
389
658
  }
390
659
  throw await toServiceException('GET', url, res.response);
391
660
  }
661
+ /**
662
+ * Create a new tag for categorizing tasks.
663
+ *
664
+ * @param name - The tag display name (must be non-empty)
665
+ * @param color - Optional hex color code (e.g., `'#ff0000'`)
666
+ * @returns The newly created tag
667
+ * @throws {@link TagNameRequiredError} if the name is empty or not a string
668
+ * @throws {@link ServiceException} if the API request fails
669
+ *
670
+ * @example
671
+ * ```typescript
672
+ * const tag = await tasks.createTag('urgent', '#ff0000');
673
+ * console.log('Created tag:', tag.id, tag.name);
674
+ * ```
675
+ */
392
676
  async createTag(name, color) {
393
677
  if (!name || typeof name !== 'string' || name.trim().length === 0) {
394
678
  throw new TagNameRequiredError();
@@ -419,6 +703,20 @@ export class TaskStorageService {
419
703
  }
420
704
  throw await toServiceException('POST', url, res.response);
421
705
  }
706
+ /**
707
+ * Get a tag by its ID.
708
+ *
709
+ * @param tagId - The unique tag identifier
710
+ * @returns The tag
711
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
712
+ * @throws {@link ServiceException} if the API request fails
713
+ *
714
+ * @example
715
+ * ```typescript
716
+ * const tag = await tasks.getTag('tag_def456');
717
+ * console.log(`${tag.name} (${tag.color})`);
718
+ * ```
719
+ */
422
720
  async getTag(tagId) {
423
721
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
424
722
  throw new TagIdRequiredError();
@@ -444,6 +742,23 @@ export class TaskStorageService {
444
742
  }
445
743
  throw await toServiceException('GET', url, res.response);
446
744
  }
745
+ /**
746
+ * Update a tag's name and optionally its color.
747
+ *
748
+ * @param tagId - The unique tag identifier
749
+ * @param name - The new tag name (must be non-empty)
750
+ * @param color - Optional new hex color code
751
+ * @returns The updated tag
752
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
753
+ * @throws {@link TagNameRequiredError} if the name is empty or not a string
754
+ * @throws {@link ServiceException} if the API request fails
755
+ *
756
+ * @example
757
+ * ```typescript
758
+ * const updated = await tasks.updateTag('tag_def456', 'critical', '#cc0000');
759
+ * console.log('Updated:', updated.name);
760
+ * ```
761
+ */
447
762
  async updateTag(tagId, name, color) {
448
763
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
449
764
  throw new TagIdRequiredError();
@@ -477,6 +792,19 @@ export class TaskStorageService {
477
792
  }
478
793
  throw await toServiceException('PATCH', url, res.response);
479
794
  }
795
+ /**
796
+ * Delete a tag permanently.
797
+ *
798
+ * @param tagId - The unique tag identifier
799
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
800
+ * @throws {@link ServiceException} if the API request fails
801
+ *
802
+ * @example
803
+ * ```typescript
804
+ * await tasks.deleteTag('tag_def456');
805
+ * console.log('Tag deleted');
806
+ * ```
807
+ */
480
808
  async deleteTag(tagId) {
481
809
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
482
810
  throw new TagIdRequiredError();
@@ -502,6 +830,20 @@ export class TaskStorageService {
502
830
  }
503
831
  throw await toServiceException('DELETE', url, res.response);
504
832
  }
833
+ /**
834
+ * List all tags in the organization.
835
+ *
836
+ * @returns List of all tags
837
+ * @throws {@link ServiceException} if the API request fails
838
+ *
839
+ * @example
840
+ * ```typescript
841
+ * const { tags } = await tasks.listTags();
842
+ * for (const tag of tags) {
843
+ * console.log(`${tag.name} (${tag.color ?? 'no color'})`);
844
+ * }
845
+ * ```
846
+ */
505
847
  async listTags() {
506
848
  const url = buildUrl(this.#baseUrl, `/task/tags/list/${TASK_API_VERSION}`);
507
849
  const signal = AbortSignal.timeout(30_000);
@@ -524,6 +866,21 @@ export class TaskStorageService {
524
866
  }
525
867
  throw await toServiceException('GET', url, res.response);
526
868
  }
869
+ /**
870
+ * Associate a tag with a task.
871
+ *
872
+ * @param taskId - The ID of the task
873
+ * @param tagId - The ID of the tag to add
874
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
875
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
876
+ * @throws {@link ServiceException} if the API request fails
877
+ *
878
+ * @example
879
+ * ```typescript
880
+ * await tasks.addTagToTask('task_abc123', 'tag_def456');
881
+ * console.log('Tag added to task');
882
+ * ```
883
+ */
527
884
  async addTagToTask(taskId, tagId) {
528
885
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
529
886
  throw new TaskIdRequiredError();
@@ -552,6 +909,21 @@ export class TaskStorageService {
552
909
  }
553
910
  throw await toServiceException('POST', url, res.response);
554
911
  }
912
+ /**
913
+ * Remove a tag association from a task.
914
+ *
915
+ * @param taskId - The ID of the task
916
+ * @param tagId - The ID of the tag to remove
917
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
918
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
919
+ * @throws {@link ServiceException} if the API request fails
920
+ *
921
+ * @example
922
+ * ```typescript
923
+ * await tasks.removeTagFromTask('task_abc123', 'tag_def456');
924
+ * console.log('Tag removed from task');
925
+ * ```
926
+ */
555
927
  async removeTagFromTask(taskId, tagId) {
556
928
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
557
929
  throw new TaskIdRequiredError();
@@ -580,6 +952,20 @@ export class TaskStorageService {
580
952
  }
581
953
  throw await toServiceException('DELETE', url, res.response);
582
954
  }
955
+ /**
956
+ * List all tags associated with a specific task.
957
+ *
958
+ * @param taskId - The ID of the task
959
+ * @returns Array of tags on the task
960
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
961
+ * @throws {@link ServiceException} if the API request fails
962
+ *
963
+ * @example
964
+ * ```typescript
965
+ * const tags = await tasks.listTagsForTask('task_abc123');
966
+ * console.log('Tags:', tags.map((t) => t.name).join(', '));
967
+ * ```
968
+ */
583
969
  async listTagsForTask(taskId) {
584
970
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
585
971
  throw new TaskIdRequiredError();
@@ -605,7 +991,33 @@ export class TaskStorageService {
605
991
  }
606
992
  throw await toServiceException('GET', url, res.response);
607
993
  }
608
- // Attachment methods
994
+ /**
995
+ * Initiate a file upload to a task. Returns a presigned S3 URL for direct upload.
996
+ *
997
+ * @remarks
998
+ * After receiving the presigned URL, upload the file content via HTTP PUT to that URL.
999
+ * Then call {@link TaskStorageService.confirmAttachment | confirmAttachment} to finalize.
1000
+ *
1001
+ * @param taskId - The ID of the task to attach the file to
1002
+ * @param params - Attachment metadata including filename, content type, and size
1003
+ * @returns The created attachment record and a presigned upload URL
1004
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1005
+ * @throws {@link ServiceException} if the API request fails
1006
+ *
1007
+ * @example
1008
+ * ```typescript
1009
+ * const { attachment, presigned_url } = await tasks.uploadAttachment(
1010
+ * 'task_abc123',
1011
+ * { filename: 'report.pdf', content_type: 'application/pdf', size: 102400 },
1012
+ * );
1013
+ *
1014
+ * // Upload the file to S3
1015
+ * await fetch(presigned_url, { method: 'PUT', body: fileContent });
1016
+ *
1017
+ * // Confirm the upload
1018
+ * await tasks.confirmAttachment(attachment.id);
1019
+ * ```
1020
+ */
609
1021
  async uploadAttachment(taskId, params) {
610
1022
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
611
1023
  throw new TaskIdRequiredError();
@@ -633,6 +1045,24 @@ export class TaskStorageService {
633
1045
  }
634
1046
  throw await toServiceException('POST', url, res.response);
635
1047
  }
1048
+ /**
1049
+ * Confirm that a file upload has completed successfully.
1050
+ *
1051
+ * @remarks
1052
+ * Call this after successfully uploading the file to the presigned URL
1053
+ * returned by {@link TaskStorageService.uploadAttachment | uploadAttachment}.
1054
+ *
1055
+ * @param attachmentId - The unique attachment identifier
1056
+ * @returns The confirmed attachment record
1057
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
1058
+ * @throws {@link ServiceException} if the API request fails
1059
+ *
1060
+ * @example
1061
+ * ```typescript
1062
+ * const confirmed = await tasks.confirmAttachment('att_ghi789');
1063
+ * console.log('Confirmed:', confirmed.filename);
1064
+ * ```
1065
+ */
636
1066
  async confirmAttachment(attachmentId) {
637
1067
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
638
1068
  throw new AttachmentIdRequiredError();
@@ -658,6 +1088,20 @@ export class TaskStorageService {
658
1088
  }
659
1089
  throw await toServiceException('POST', url, res.response);
660
1090
  }
1091
+ /**
1092
+ * Get a presigned S3 URL for downloading an attachment.
1093
+ *
1094
+ * @param attachmentId - The unique attachment identifier
1095
+ * @returns A presigned download URL with expiry information
1096
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
1097
+ * @throws {@link ServiceException} if the API request fails
1098
+ *
1099
+ * @example
1100
+ * ```typescript
1101
+ * const { presigned_url, expiry_seconds } = await tasks.downloadAttachment('att_ghi789');
1102
+ * console.log(`Download URL (expires in ${expiry_seconds}s):`, presigned_url);
1103
+ * ```
1104
+ */
661
1105
  async downloadAttachment(attachmentId) {
662
1106
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
663
1107
  throw new AttachmentIdRequiredError();
@@ -683,6 +1127,22 @@ export class TaskStorageService {
683
1127
  }
684
1128
  throw await toServiceException('POST', url, res.response);
685
1129
  }
1130
+ /**
1131
+ * List all attachments on a task.
1132
+ *
1133
+ * @param taskId - The ID of the task
1134
+ * @returns List of attachments with total count
1135
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1136
+ * @throws {@link ServiceException} if the API request fails
1137
+ *
1138
+ * @example
1139
+ * ```typescript
1140
+ * const { attachments, total } = await tasks.listAttachments('task_abc123');
1141
+ * for (const att of attachments) {
1142
+ * console.log(`${att.filename} (${att.content_type}, ${att.size} bytes)`);
1143
+ * }
1144
+ * ```
1145
+ */
686
1146
  async listAttachments(taskId) {
687
1147
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
688
1148
  throw new TaskIdRequiredError();
@@ -708,6 +1168,19 @@ export class TaskStorageService {
708
1168
  }
709
1169
  throw await toServiceException('GET', url, res.response);
710
1170
  }
1171
+ /**
1172
+ * Delete an attachment permanently.
1173
+ *
1174
+ * @param attachmentId - The unique attachment identifier
1175
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
1176
+ * @throws {@link ServiceException} if the API request fails
1177
+ *
1178
+ * @example
1179
+ * ```typescript
1180
+ * await tasks.deleteAttachment('att_ghi789');
1181
+ * console.log('Attachment deleted');
1182
+ * ```
1183
+ */
711
1184
  async deleteAttachment(attachmentId) {
712
1185
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
713
1186
  throw new AttachmentIdRequiredError();
@@ -733,6 +1206,20 @@ export class TaskStorageService {
733
1206
  }
734
1207
  throw await toServiceException('DELETE', url, res.response);
735
1208
  }
1209
+ /**
1210
+ * List all users who have been referenced in tasks (as creators, assignees, or closers).
1211
+ *
1212
+ * @returns List of user entity references
1213
+ * @throws {@link ServiceException} if the API request fails
1214
+ *
1215
+ * @example
1216
+ * ```typescript
1217
+ * const { users } = await tasks.listUsers();
1218
+ * for (const user of users) {
1219
+ * console.log(`${user.name} (${user.id})`);
1220
+ * }
1221
+ * ```
1222
+ */
736
1223
  async listUsers() {
737
1224
  const url = buildUrl(this.#baseUrl, `/task/users/${TASK_API_VERSION}`);
738
1225
  const signal = AbortSignal.timeout(30_000);
@@ -755,6 +1242,20 @@ export class TaskStorageService {
755
1242
  }
756
1243
  throw await toServiceException('GET', url, res.response);
757
1244
  }
1245
+ /**
1246
+ * List all projects that have been referenced in tasks.
1247
+ *
1248
+ * @returns List of project entity references
1249
+ * @throws {@link ServiceException} if the API request fails
1250
+ *
1251
+ * @example
1252
+ * ```typescript
1253
+ * const { projects } = await tasks.listProjects();
1254
+ * for (const project of projects) {
1255
+ * console.log(`${project.name} (${project.id})`);
1256
+ * }
1257
+ * ```
1258
+ */
758
1259
  async listProjects() {
759
1260
  const url = buildUrl(this.#baseUrl, `/task/projects/${TASK_API_VERSION}`);
760
1261
  const signal = AbortSignal.timeout(30_000);
@@ -777,6 +1278,22 @@ export class TaskStorageService {
777
1278
  }
778
1279
  throw await toServiceException('GET', url, res.response);
779
1280
  }
1281
+ /**
1282
+ * Get task activity time-series data showing daily task counts by status.
1283
+ *
1284
+ * @param params - Optional parameters controlling the number of days to retrieve
1285
+ * @returns Time-series activity data with daily snapshots
1286
+ * @throws {@link ServiceException} if the API request fails
1287
+ *
1288
+ * @example
1289
+ * ```typescript
1290
+ * const { activity, days } = await tasks.getActivity({ days: 30 });
1291
+ * console.log(`Activity over ${days} days:`);
1292
+ * for (const point of activity) {
1293
+ * console.log(`${point.date}: ${point.open} open, ${point.inProgress} in progress`);
1294
+ * }
1295
+ * ```
1296
+ */
780
1297
  async getActivity(params) {
781
1298
  const queryParams = new URLSearchParams();
782
1299
  if (params?.days !== undefined)