@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.
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/email.d.ts +507 -8
- package/dist/services/email.d.ts.map +1 -1
- package/dist/services/email.js +325 -1
- package/dist/services/email.js.map +1 -1
- package/dist/services/keyvalue.d.ts +5 -2
- package/dist/services/keyvalue.d.ts.map +1 -1
- package/dist/services/keyvalue.js +44 -1
- package/dist/services/keyvalue.js.map +1 -1
- package/dist/services/queue.d.ts +98 -0
- package/dist/services/queue.d.ts.map +1 -1
- package/dist/services/queue.js +100 -0
- package/dist/services/queue.js.map +1 -1
- package/dist/services/schedule.d.ts +413 -0
- package/dist/services/schedule.d.ts.map +1 -1
- package/dist/services/schedule.js +221 -0
- package/dist/services/schedule.js.map +1 -1
- package/dist/services/task.d.ts +1103 -11
- package/dist/services/task.d.ts.map +1 -1
- package/dist/services/task.js +589 -1
- package/dist/services/task.js.map +1 -1
- package/dist/services/webhook.d.ts +352 -0
- package/dist/services/webhook.d.ts.map +1 -1
- package/dist/services/webhook.js +254 -0
- package/dist/services/webhook.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +8 -0
- package/src/services/email.ts +560 -9
- package/src/services/keyvalue.ts +51 -3
- package/src/services/queue.ts +215 -0
- package/src/services/schedule.ts +442 -0
- package/src/services/task.ts +1329 -23
- package/src/services/webhook.ts +412 -0
package/dist/services/task.js
CHANGED
|
@@ -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
|
-
|
|
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)
|