@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.
@@ -1,24 +1,99 @@
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
+ /** Maximum number of tasks that can be deleted in a single batch request. */
7
+ const MAX_BATCH_DELETE_LIMIT = 200;
8
+ /** API version string used for the task activity analytics endpoint. */
5
9
  const TASK_ACTIVITY_API_VERSION = '2026-02-28';
10
+ /** Thrown when a task ID parameter is empty or not a string. */
6
11
  const TaskIdRequiredError = StructuredError('TaskIdRequiredError', 'Task ID is required and must be a non-empty string');
12
+ /** Thrown when a task title is empty or not a string. */
7
13
  const TaskTitleRequiredError = StructuredError('TaskTitleRequiredError', 'Task title is required and must be a non-empty string');
14
+ /** Thrown when a comment ID parameter is empty or not a string. */
8
15
  const CommentIdRequiredError = StructuredError('CommentIdRequiredError', 'Comment ID is required and must be a non-empty string');
16
+ /** Thrown when a comment body is empty or not a string. */
9
17
  const CommentBodyRequiredError = StructuredError('CommentBodyRequiredError', 'Comment body is required and must be a non-empty string');
18
+ /** Thrown when a tag ID parameter is empty or not a string. */
10
19
  const TagIdRequiredError = StructuredError('TagIdRequiredError', 'Tag ID is required and must be a non-empty string');
20
+ /** Thrown when a tag name is empty or not a string. */
11
21
  const TagNameRequiredError = StructuredError('TagNameRequiredError', 'Tag name is required and must be a non-empty string');
22
+ /** Thrown when an attachment ID parameter is empty or not a string. */
12
23
  const AttachmentIdRequiredError = StructuredError('AttachmentIdRequiredError', 'Attachment ID is required and must be a non-empty string');
24
+ /** Thrown when a user ID parameter is empty or not a string. */
13
25
  const UserIdRequiredError = StructuredError('UserIdRequiredError', 'User ID is required and must be a non-empty string');
26
+ /**
27
+ * Thrown when the API returns a success HTTP status but the response body indicates failure.
28
+ */
14
29
  const TaskStorageResponseError = StructuredError('TaskStorageResponseError')();
30
+ /**
31
+ * Client for the Agentuity Task management service.
32
+ *
33
+ * Provides a full-featured project management API including task CRUD, hierarchical
34
+ * organization (epics → features → tasks), comments, tags, file attachments via
35
+ * presigned S3 URLs, changelog tracking, and activity analytics.
36
+ *
37
+ * Tasks support lifecycle management through status transitions (`open` → `in_progress`
38
+ * → `done`/`closed`/`cancelled`) with automatic date tracking for each transition.
39
+ *
40
+ * All methods validate inputs client-side and throw structured errors for invalid
41
+ * parameters. API errors throw {@link ServiceException}.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const tasks = new TaskStorageService(baseUrl, adapter);
46
+ *
47
+ * // Create a task
48
+ * const task = await tasks.create({
49
+ * title: 'Implement login flow',
50
+ * type: 'feature',
51
+ * created_id: 'user_123',
52
+ * creator: { id: 'user_123', name: 'Alice' },
53
+ * priority: 'high',
54
+ * });
55
+ *
56
+ * // Add a comment
57
+ * await tasks.createComment(task.id, 'Started working on this', 'user_123');
58
+ *
59
+ * // List open tasks
60
+ * const { tasks: openTasks } = await tasks.list({ status: 'open' });
61
+ * ```
62
+ */
15
63
  export class TaskStorageService {
16
64
  #adapter;
17
65
  #baseUrl;
66
+ /**
67
+ * Creates a new TaskStorageService instance.
68
+ *
69
+ * @param baseUrl - The base URL of the task management API
70
+ * @param adapter - The HTTP fetch adapter used for making API requests
71
+ */
18
72
  constructor(baseUrl, adapter) {
19
73
  this.#adapter = adapter;
20
74
  this.#baseUrl = baseUrl;
21
75
  }
76
+ /**
77
+ * Create a new task.
78
+ *
79
+ * @param params - The task creation parameters including title, type, and optional fields
80
+ * @returns The newly created task
81
+ * @throws {@link TaskTitleRequiredError} if the title is empty or not a string
82
+ * @throws {@link ServiceException} if the API request fails
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * const task = await tasks.create({
87
+ * title: 'Fix login bug',
88
+ * type: 'bug',
89
+ * created_id: 'user_123',
90
+ * priority: 'high',
91
+ * creator: { id: 'user_123', name: 'Alice' },
92
+ * project: { id: 'proj_456', name: 'Auth Service' },
93
+ * });
94
+ * console.log('Created:', task.id);
95
+ * ```
96
+ */
22
97
  async create(params) {
23
98
  if (!params?.title || typeof params.title !== 'string' || params.title.trim().length === 0) {
24
99
  throw new TaskTitleRequiredError();
@@ -50,6 +125,24 @@ export class TaskStorageService {
50
125
  }
51
126
  throw await toServiceException('POST', url, res.response);
52
127
  }
128
+ /**
129
+ * Get a task by its ID.
130
+ *
131
+ * @param id - The unique task identifier
132
+ * @returns The task if found, or `null` if the task does not exist
133
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
134
+ * @throws {@link ServiceException} if the API request fails
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const task = await tasks.get('task_abc123');
139
+ * if (task) {
140
+ * console.log(task.title, task.status);
141
+ * } else {
142
+ * console.log('Task not found');
143
+ * }
144
+ * ```
145
+ */
53
146
  async get(id) {
54
147
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
55
148
  throw new TaskIdRequiredError();
@@ -78,6 +171,26 @@ export class TaskStorageService {
78
171
  }
79
172
  throw await toServiceException('GET', url, res.response);
80
173
  }
174
+ /**
175
+ * List tasks with optional filtering and pagination.
176
+ *
177
+ * @param params - Optional filter and pagination parameters
178
+ * @returns Paginated list of tasks matching the filters
179
+ * @throws {@link ServiceException} if the API request fails
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * // List all open high-priority bugs
184
+ * const result = await tasks.list({
185
+ * status: 'open',
186
+ * type: 'bug',
187
+ * priority: 'high',
188
+ * sort: '-created_at',
189
+ * limit: 20,
190
+ * });
191
+ * console.log(`Found ${result.total} bugs, showing ${result.tasks.length}`);
192
+ * ```
193
+ */
81
194
  async list(params) {
82
195
  const queryParams = new URLSearchParams();
83
196
  if (params?.status)
@@ -130,6 +243,26 @@ export class TaskStorageService {
130
243
  }
131
244
  throw await toServiceException('GET', url, res.response);
132
245
  }
246
+ /**
247
+ * Partially update an existing task.
248
+ *
249
+ * @param id - The unique task identifier
250
+ * @param params - Fields to update; only provided fields are changed
251
+ * @returns The updated task
252
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
253
+ * @throws {@link TaskTitleRequiredError} if a title is provided but is empty
254
+ * @throws {@link ServiceException} if the API request fails
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * const updated = await tasks.update('task_abc123', {
259
+ * status: 'in_progress',
260
+ * priority: 'high',
261
+ * assignee: { id: 'user_456', name: 'Bob' },
262
+ * });
263
+ * console.log('Updated status:', updated.status);
264
+ * ```
265
+ */
133
266
  async update(id, params) {
134
267
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
135
268
  throw new TaskIdRequiredError();
@@ -161,6 +294,20 @@ export class TaskStorageService {
161
294
  }
162
295
  throw await toServiceException('PATCH', url, res.response);
163
296
  }
297
+ /**
298
+ * Close a task by setting its status to closed.
299
+ *
300
+ * @param id - The unique task identifier
301
+ * @returns The closed task with updated `closed_date`
302
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
303
+ * @throws {@link ServiceException} if the API request fails
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * const closed = await tasks.close('task_abc123');
308
+ * console.log('Closed at:', closed.closed_date);
309
+ * ```
310
+ */
164
311
  async close(id) {
165
312
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
166
313
  throw new TaskIdRequiredError();
@@ -186,6 +333,26 @@ export class TaskStorageService {
186
333
  }
187
334
  throw await toServiceException('DELETE', url, res.response);
188
335
  }
336
+ /**
337
+ * Get the changelog (audit trail) for a task, showing all field changes over time.
338
+ *
339
+ * @param id - The unique task identifier
340
+ * @param params - Optional pagination parameters
341
+ * @returns Paginated list of changelog entries ordered by most recent first
342
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
343
+ * @throws {@link ServiceException} if the API request fails
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * const { changelog, total } = await tasks.changelog('task_abc123', {
348
+ * limit: 10,
349
+ * offset: 0,
350
+ * });
351
+ * for (const entry of changelog) {
352
+ * console.log(`${entry.field}: ${entry.old_value} → ${entry.new_value}`);
353
+ * }
354
+ * ```
355
+ */
189
356
  async changelog(id, params) {
190
357
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
191
358
  throw new TaskIdRequiredError();
@@ -217,6 +384,20 @@ export class TaskStorageService {
217
384
  }
218
385
  throw await toServiceException('GET', url, res.response);
219
386
  }
387
+ /**
388
+ * Soft-delete a task, marking it as deleted without permanent removal.
389
+ *
390
+ * @param id - The unique task identifier
391
+ * @returns The soft-deleted task
392
+ * @throws {@link TaskIdRequiredError} if the ID is empty or not a string
393
+ * @throws {@link ServiceException} if the API request fails
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * const deleted = await tasks.softDelete('task_abc123');
398
+ * console.log('Soft-deleted task:', deleted.id);
399
+ * ```
400
+ */
220
401
  async softDelete(id) {
221
402
  if (!id || typeof id !== 'string' || id.trim().length === 0) {
222
403
  throw new TaskIdRequiredError();
@@ -242,6 +423,99 @@ export class TaskStorageService {
242
423
  }
243
424
  throw await toServiceException('POST', url, res.response);
244
425
  }
426
+ /**
427
+ * Batch soft-delete tasks matching the given filters.
428
+ * At least one filter must be provided. The server caps the limit at 200.
429
+ *
430
+ * @param params - Filters to select which tasks to delete
431
+ * @returns The list of deleted tasks and count
432
+ * @throws {@link ServiceException} if the API request fails
433
+ *
434
+ * @example
435
+ * ```typescript
436
+ * const result = await tasks.batchDelete({ status: 'closed', older_than: '7d', limit: 50 });
437
+ * console.log(`Deleted ${result.count} tasks`);
438
+ * ```
439
+ */
440
+ async batchDelete(params) {
441
+ const hasFilter = params.status ||
442
+ params.type ||
443
+ params.priority ||
444
+ params.parent_id ||
445
+ params.created_id ||
446
+ params.older_than;
447
+ if (!hasFilter) {
448
+ throw new Error('At least one filter is required for batch delete');
449
+ }
450
+ if (params.limit !== undefined && params.limit > MAX_BATCH_DELETE_LIMIT) {
451
+ throw new Error(`Batch delete limit must not exceed ${MAX_BATCH_DELETE_LIMIT} (got ${params.limit})`);
452
+ }
453
+ const url = buildUrl(this.#baseUrl, `/task/delete/batch/${TASK_API_VERSION}`);
454
+ const signal = AbortSignal.timeout(60_000);
455
+ const body = {};
456
+ if (params.status)
457
+ body.status = params.status;
458
+ if (params.type)
459
+ body.type = params.type;
460
+ if (params.priority)
461
+ body.priority = params.priority;
462
+ if (params.parent_id)
463
+ body.parent_id = params.parent_id;
464
+ if (params.created_id)
465
+ body.created_id = params.created_id;
466
+ if (params.older_than)
467
+ body.older_than = params.older_than;
468
+ if (params.limit !== undefined)
469
+ body.limit = params.limit;
470
+ const res = await this.#adapter.invoke(url, {
471
+ method: 'POST',
472
+ body: safeStringify(body),
473
+ headers: { 'Content-Type': 'application/json' },
474
+ signal,
475
+ telemetry: {
476
+ name: 'agentuity.task.batchDelete',
477
+ attributes: {
478
+ ...(params.status ? { status: params.status } : {}),
479
+ ...(params.type ? { type: params.type } : {}),
480
+ ...(params.older_than ? { older_than: params.older_than } : {}),
481
+ },
482
+ },
483
+ });
484
+ if (res.ok) {
485
+ if (res.data.success) {
486
+ return res.data.data;
487
+ }
488
+ throw new TaskStorageResponseError({
489
+ status: res.response.status,
490
+ message: res.data.message,
491
+ });
492
+ }
493
+ throw await toServiceException('POST', url, res.response);
494
+ }
495
+ /**
496
+ * Create a comment on a task.
497
+ *
498
+ * @param taskId - The ID of the task to comment on
499
+ * @param body - The comment text content (must be non-empty)
500
+ * @param userId - The ID of the user authoring the comment
501
+ * @param author - Optional entity reference with the author's display name
502
+ * @returns The newly created comment
503
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
504
+ * @throws {@link CommentBodyRequiredError} if the body is empty or not a string
505
+ * @throws {@link UserIdRequiredError} if the user ID is empty or not a string
506
+ * @throws {@link ServiceException} if the API request fails
507
+ *
508
+ * @example
509
+ * ```typescript
510
+ * const comment = await tasks.createComment(
511
+ * 'task_abc123',
512
+ * 'This is ready for review.',
513
+ * 'user_456',
514
+ * { id: 'user_456', name: 'Bob' },
515
+ * );
516
+ * console.log('Comment created:', comment.id);
517
+ * ```
518
+ */
245
519
  async createComment(taskId, body, userId, author) {
246
520
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
247
521
  throw new TaskIdRequiredError();
@@ -278,6 +552,20 @@ export class TaskStorageService {
278
552
  }
279
553
  throw await toServiceException('POST', url, res.response);
280
554
  }
555
+ /**
556
+ * Get a comment by its ID.
557
+ *
558
+ * @param commentId - The unique comment identifier
559
+ * @returns The comment
560
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
561
+ * @throws {@link ServiceException} if the API request fails
562
+ *
563
+ * @example
564
+ * ```typescript
565
+ * const comment = await tasks.getComment('comment_xyz789');
566
+ * console.log(`${comment.author?.name}: ${comment.body}`);
567
+ * ```
568
+ */
281
569
  async getComment(commentId) {
282
570
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
283
571
  throw new CommentIdRequiredError();
@@ -303,6 +591,25 @@ export class TaskStorageService {
303
591
  }
304
592
  throw await toServiceException('GET', url, res.response);
305
593
  }
594
+ /**
595
+ * Update a comment's body text.
596
+ *
597
+ * @param commentId - The unique comment identifier
598
+ * @param body - The new comment text (must be non-empty)
599
+ * @returns The updated comment
600
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
601
+ * @throws {@link CommentBodyRequiredError} if the body is empty or not a string
602
+ * @throws {@link ServiceException} if the API request fails
603
+ *
604
+ * @example
605
+ * ```typescript
606
+ * const updated = await tasks.updateComment(
607
+ * 'comment_xyz789',
608
+ * 'Updated: This is now ready for final review.',
609
+ * );
610
+ * console.log('Updated at:', updated.updated_at);
611
+ * ```
612
+ */
306
613
  async updateComment(commentId, body) {
307
614
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
308
615
  throw new CommentIdRequiredError();
@@ -333,6 +640,19 @@ export class TaskStorageService {
333
640
  }
334
641
  throw await toServiceException('PATCH', url, res.response);
335
642
  }
643
+ /**
644
+ * Delete a comment permanently.
645
+ *
646
+ * @param commentId - The unique comment identifier
647
+ * @throws {@link CommentIdRequiredError} if the comment ID is empty or not a string
648
+ * @throws {@link ServiceException} if the API request fails
649
+ *
650
+ * @example
651
+ * ```typescript
652
+ * await tasks.deleteComment('comment_xyz789');
653
+ * console.log('Comment deleted');
654
+ * ```
655
+ */
336
656
  async deleteComment(commentId) {
337
657
  if (!commentId || typeof commentId !== 'string' || commentId.trim().length === 0) {
338
658
  throw new CommentIdRequiredError();
@@ -358,6 +678,26 @@ export class TaskStorageService {
358
678
  }
359
679
  throw await toServiceException('DELETE', url, res.response);
360
680
  }
681
+ /**
682
+ * List comments on a task with optional pagination.
683
+ *
684
+ * @param taskId - The ID of the task whose comments to list
685
+ * @param params - Optional pagination parameters
686
+ * @returns Paginated list of comments
687
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
688
+ * @throws {@link ServiceException} if the API request fails
689
+ *
690
+ * @example
691
+ * ```typescript
692
+ * const { comments, total } = await tasks.listComments('task_abc123', {
693
+ * limit: 25,
694
+ * offset: 0,
695
+ * });
696
+ * for (const c of comments) {
697
+ * console.log(`${c.author?.name}: ${c.body}`);
698
+ * }
699
+ * ```
700
+ */
361
701
  async listComments(taskId, params) {
362
702
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
363
703
  throw new TaskIdRequiredError();
@@ -389,6 +729,21 @@ export class TaskStorageService {
389
729
  }
390
730
  throw await toServiceException('GET', url, res.response);
391
731
  }
732
+ /**
733
+ * Create a new tag for categorizing tasks.
734
+ *
735
+ * @param name - The tag display name (must be non-empty)
736
+ * @param color - Optional hex color code (e.g., `'#ff0000'`)
737
+ * @returns The newly created tag
738
+ * @throws {@link TagNameRequiredError} if the name is empty or not a string
739
+ * @throws {@link ServiceException} if the API request fails
740
+ *
741
+ * @example
742
+ * ```typescript
743
+ * const tag = await tasks.createTag('urgent', '#ff0000');
744
+ * console.log('Created tag:', tag.id, tag.name);
745
+ * ```
746
+ */
392
747
  async createTag(name, color) {
393
748
  if (!name || typeof name !== 'string' || name.trim().length === 0) {
394
749
  throw new TagNameRequiredError();
@@ -419,6 +774,20 @@ export class TaskStorageService {
419
774
  }
420
775
  throw await toServiceException('POST', url, res.response);
421
776
  }
777
+ /**
778
+ * Get a tag by its ID.
779
+ *
780
+ * @param tagId - The unique tag identifier
781
+ * @returns The tag
782
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
783
+ * @throws {@link ServiceException} if the API request fails
784
+ *
785
+ * @example
786
+ * ```typescript
787
+ * const tag = await tasks.getTag('tag_def456');
788
+ * console.log(`${tag.name} (${tag.color})`);
789
+ * ```
790
+ */
422
791
  async getTag(tagId) {
423
792
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
424
793
  throw new TagIdRequiredError();
@@ -444,6 +813,23 @@ export class TaskStorageService {
444
813
  }
445
814
  throw await toServiceException('GET', url, res.response);
446
815
  }
816
+ /**
817
+ * Update a tag's name and optionally its color.
818
+ *
819
+ * @param tagId - The unique tag identifier
820
+ * @param name - The new tag name (must be non-empty)
821
+ * @param color - Optional new hex color code
822
+ * @returns The updated tag
823
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
824
+ * @throws {@link TagNameRequiredError} if the name is empty or not a string
825
+ * @throws {@link ServiceException} if the API request fails
826
+ *
827
+ * @example
828
+ * ```typescript
829
+ * const updated = await tasks.updateTag('tag_def456', 'critical', '#cc0000');
830
+ * console.log('Updated:', updated.name);
831
+ * ```
832
+ */
447
833
  async updateTag(tagId, name, color) {
448
834
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
449
835
  throw new TagIdRequiredError();
@@ -477,6 +863,19 @@ export class TaskStorageService {
477
863
  }
478
864
  throw await toServiceException('PATCH', url, res.response);
479
865
  }
866
+ /**
867
+ * Delete a tag permanently.
868
+ *
869
+ * @param tagId - The unique tag identifier
870
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
871
+ * @throws {@link ServiceException} if the API request fails
872
+ *
873
+ * @example
874
+ * ```typescript
875
+ * await tasks.deleteTag('tag_def456');
876
+ * console.log('Tag deleted');
877
+ * ```
878
+ */
480
879
  async deleteTag(tagId) {
481
880
  if (!tagId || typeof tagId !== 'string' || tagId.trim().length === 0) {
482
881
  throw new TagIdRequiredError();
@@ -502,6 +901,20 @@ export class TaskStorageService {
502
901
  }
503
902
  throw await toServiceException('DELETE', url, res.response);
504
903
  }
904
+ /**
905
+ * List all tags in the organization.
906
+ *
907
+ * @returns List of all tags
908
+ * @throws {@link ServiceException} if the API request fails
909
+ *
910
+ * @example
911
+ * ```typescript
912
+ * const { tags } = await tasks.listTags();
913
+ * for (const tag of tags) {
914
+ * console.log(`${tag.name} (${tag.color ?? 'no color'})`);
915
+ * }
916
+ * ```
917
+ */
505
918
  async listTags() {
506
919
  const url = buildUrl(this.#baseUrl, `/task/tags/list/${TASK_API_VERSION}`);
507
920
  const signal = AbortSignal.timeout(30_000);
@@ -524,6 +937,21 @@ export class TaskStorageService {
524
937
  }
525
938
  throw await toServiceException('GET', url, res.response);
526
939
  }
940
+ /**
941
+ * Associate a tag with a task.
942
+ *
943
+ * @param taskId - The ID of the task
944
+ * @param tagId - The ID of the tag to add
945
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
946
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
947
+ * @throws {@link ServiceException} if the API request fails
948
+ *
949
+ * @example
950
+ * ```typescript
951
+ * await tasks.addTagToTask('task_abc123', 'tag_def456');
952
+ * console.log('Tag added to task');
953
+ * ```
954
+ */
527
955
  async addTagToTask(taskId, tagId) {
528
956
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
529
957
  throw new TaskIdRequiredError();
@@ -552,6 +980,21 @@ export class TaskStorageService {
552
980
  }
553
981
  throw await toServiceException('POST', url, res.response);
554
982
  }
983
+ /**
984
+ * Remove a tag association from a task.
985
+ *
986
+ * @param taskId - The ID of the task
987
+ * @param tagId - The ID of the tag to remove
988
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
989
+ * @throws {@link TagIdRequiredError} if the tag ID is empty or not a string
990
+ * @throws {@link ServiceException} if the API request fails
991
+ *
992
+ * @example
993
+ * ```typescript
994
+ * await tasks.removeTagFromTask('task_abc123', 'tag_def456');
995
+ * console.log('Tag removed from task');
996
+ * ```
997
+ */
555
998
  async removeTagFromTask(taskId, tagId) {
556
999
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
557
1000
  throw new TaskIdRequiredError();
@@ -580,6 +1023,20 @@ export class TaskStorageService {
580
1023
  }
581
1024
  throw await toServiceException('DELETE', url, res.response);
582
1025
  }
1026
+ /**
1027
+ * List all tags associated with a specific task.
1028
+ *
1029
+ * @param taskId - The ID of the task
1030
+ * @returns Array of tags on the task
1031
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1032
+ * @throws {@link ServiceException} if the API request fails
1033
+ *
1034
+ * @example
1035
+ * ```typescript
1036
+ * const tags = await tasks.listTagsForTask('task_abc123');
1037
+ * console.log('Tags:', tags.map((t) => t.name).join(', '));
1038
+ * ```
1039
+ */
583
1040
  async listTagsForTask(taskId) {
584
1041
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
585
1042
  throw new TaskIdRequiredError();
@@ -605,7 +1062,33 @@ export class TaskStorageService {
605
1062
  }
606
1063
  throw await toServiceException('GET', url, res.response);
607
1064
  }
608
- // Attachment methods
1065
+ /**
1066
+ * Initiate a file upload to a task. Returns a presigned S3 URL for direct upload.
1067
+ *
1068
+ * @remarks
1069
+ * After receiving the presigned URL, upload the file content via HTTP PUT to that URL.
1070
+ * Then call {@link TaskStorageService.confirmAttachment | confirmAttachment} to finalize.
1071
+ *
1072
+ * @param taskId - The ID of the task to attach the file to
1073
+ * @param params - Attachment metadata including filename, content type, and size
1074
+ * @returns The created attachment record and a presigned upload URL
1075
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1076
+ * @throws {@link ServiceException} if the API request fails
1077
+ *
1078
+ * @example
1079
+ * ```typescript
1080
+ * const { attachment, presigned_url } = await tasks.uploadAttachment(
1081
+ * 'task_abc123',
1082
+ * { filename: 'report.pdf', content_type: 'application/pdf', size: 102400 },
1083
+ * );
1084
+ *
1085
+ * // Upload the file to S3
1086
+ * await fetch(presigned_url, { method: 'PUT', body: fileContent });
1087
+ *
1088
+ * // Confirm the upload
1089
+ * await tasks.confirmAttachment(attachment.id);
1090
+ * ```
1091
+ */
609
1092
  async uploadAttachment(taskId, params) {
610
1093
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
611
1094
  throw new TaskIdRequiredError();
@@ -633,6 +1116,24 @@ export class TaskStorageService {
633
1116
  }
634
1117
  throw await toServiceException('POST', url, res.response);
635
1118
  }
1119
+ /**
1120
+ * Confirm that a file upload has completed successfully.
1121
+ *
1122
+ * @remarks
1123
+ * Call this after successfully uploading the file to the presigned URL
1124
+ * returned by {@link TaskStorageService.uploadAttachment | uploadAttachment}.
1125
+ *
1126
+ * @param attachmentId - The unique attachment identifier
1127
+ * @returns The confirmed attachment record
1128
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
1129
+ * @throws {@link ServiceException} if the API request fails
1130
+ *
1131
+ * @example
1132
+ * ```typescript
1133
+ * const confirmed = await tasks.confirmAttachment('att_ghi789');
1134
+ * console.log('Confirmed:', confirmed.filename);
1135
+ * ```
1136
+ */
636
1137
  async confirmAttachment(attachmentId) {
637
1138
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
638
1139
  throw new AttachmentIdRequiredError();
@@ -658,6 +1159,20 @@ export class TaskStorageService {
658
1159
  }
659
1160
  throw await toServiceException('POST', url, res.response);
660
1161
  }
1162
+ /**
1163
+ * Get a presigned S3 URL for downloading an attachment.
1164
+ *
1165
+ * @param attachmentId - The unique attachment identifier
1166
+ * @returns A presigned download URL with expiry information
1167
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
1168
+ * @throws {@link ServiceException} if the API request fails
1169
+ *
1170
+ * @example
1171
+ * ```typescript
1172
+ * const { presigned_url, expiry_seconds } = await tasks.downloadAttachment('att_ghi789');
1173
+ * console.log(`Download URL (expires in ${expiry_seconds}s):`, presigned_url);
1174
+ * ```
1175
+ */
661
1176
  async downloadAttachment(attachmentId) {
662
1177
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
663
1178
  throw new AttachmentIdRequiredError();
@@ -683,6 +1198,22 @@ export class TaskStorageService {
683
1198
  }
684
1199
  throw await toServiceException('POST', url, res.response);
685
1200
  }
1201
+ /**
1202
+ * List all attachments on a task.
1203
+ *
1204
+ * @param taskId - The ID of the task
1205
+ * @returns List of attachments with total count
1206
+ * @throws {@link TaskIdRequiredError} if the task ID is empty or not a string
1207
+ * @throws {@link ServiceException} if the API request fails
1208
+ *
1209
+ * @example
1210
+ * ```typescript
1211
+ * const { attachments, total } = await tasks.listAttachments('task_abc123');
1212
+ * for (const att of attachments) {
1213
+ * console.log(`${att.filename} (${att.content_type}, ${att.size} bytes)`);
1214
+ * }
1215
+ * ```
1216
+ */
686
1217
  async listAttachments(taskId) {
687
1218
  if (!taskId || typeof taskId !== 'string' || taskId.trim().length === 0) {
688
1219
  throw new TaskIdRequiredError();
@@ -708,6 +1239,19 @@ export class TaskStorageService {
708
1239
  }
709
1240
  throw await toServiceException('GET', url, res.response);
710
1241
  }
1242
+ /**
1243
+ * Delete an attachment permanently.
1244
+ *
1245
+ * @param attachmentId - The unique attachment identifier
1246
+ * @throws {@link AttachmentIdRequiredError} if the attachment ID is empty or not a string
1247
+ * @throws {@link ServiceException} if the API request fails
1248
+ *
1249
+ * @example
1250
+ * ```typescript
1251
+ * await tasks.deleteAttachment('att_ghi789');
1252
+ * console.log('Attachment deleted');
1253
+ * ```
1254
+ */
711
1255
  async deleteAttachment(attachmentId) {
712
1256
  if (!attachmentId || typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
713
1257
  throw new AttachmentIdRequiredError();
@@ -733,6 +1277,20 @@ export class TaskStorageService {
733
1277
  }
734
1278
  throw await toServiceException('DELETE', url, res.response);
735
1279
  }
1280
+ /**
1281
+ * List all users who have been referenced in tasks (as creators, assignees, or closers).
1282
+ *
1283
+ * @returns List of user entity references
1284
+ * @throws {@link ServiceException} if the API request fails
1285
+ *
1286
+ * @example
1287
+ * ```typescript
1288
+ * const { users } = await tasks.listUsers();
1289
+ * for (const user of users) {
1290
+ * console.log(`${user.name} (${user.id})`);
1291
+ * }
1292
+ * ```
1293
+ */
736
1294
  async listUsers() {
737
1295
  const url = buildUrl(this.#baseUrl, `/task/users/${TASK_API_VERSION}`);
738
1296
  const signal = AbortSignal.timeout(30_000);
@@ -755,6 +1313,20 @@ export class TaskStorageService {
755
1313
  }
756
1314
  throw await toServiceException('GET', url, res.response);
757
1315
  }
1316
+ /**
1317
+ * List all projects that have been referenced in tasks.
1318
+ *
1319
+ * @returns List of project entity references
1320
+ * @throws {@link ServiceException} if the API request fails
1321
+ *
1322
+ * @example
1323
+ * ```typescript
1324
+ * const { projects } = await tasks.listProjects();
1325
+ * for (const project of projects) {
1326
+ * console.log(`${project.name} (${project.id})`);
1327
+ * }
1328
+ * ```
1329
+ */
758
1330
  async listProjects() {
759
1331
  const url = buildUrl(this.#baseUrl, `/task/projects/${TASK_API_VERSION}`);
760
1332
  const signal = AbortSignal.timeout(30_000);
@@ -777,6 +1349,22 @@ export class TaskStorageService {
777
1349
  }
778
1350
  throw await toServiceException('GET', url, res.response);
779
1351
  }
1352
+ /**
1353
+ * Get task activity time-series data showing daily task counts by status.
1354
+ *
1355
+ * @param params - Optional parameters controlling the number of days to retrieve
1356
+ * @returns Time-series activity data with daily snapshots
1357
+ * @throws {@link ServiceException} if the API request fails
1358
+ *
1359
+ * @example
1360
+ * ```typescript
1361
+ * const { activity, days } = await tasks.getActivity({ days: 30 });
1362
+ * console.log(`Activity over ${days} days:`);
1363
+ * for (const point of activity) {
1364
+ * console.log(`${point.date}: ${point.open} open, ${point.inProgress} in progress`);
1365
+ * }
1366
+ * ```
1367
+ */
780
1368
  async getActivity(params) {
781
1369
  const queryParams = new URLSearchParams();
782
1370
  if (params?.days !== undefined)