@critical-path/client 0.20.4 → 0.20.6

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/src/index.ts DELETED
@@ -1,888 +0,0 @@
1
- import type {
2
- CreateWorkflowBody,
3
- UpdateWorkflowBody,
4
- CreateProjectBody,
5
- UpdateProjectBody,
6
- CreateTaskBody,
7
- UpdateTaskBody,
8
- CreateDependencyBody,
9
- CreateTeamBody,
10
- UpdateTeamBody,
11
- CreateContainerBody,
12
- UpdateContainerBody,
13
- CreateDeliverableBody,
14
- UpdateDeliverableBody,
15
- CreateIterationBody,
16
- UpdateIterationBody,
17
- CreateCommentBody,
18
- UpdateCommentBody,
19
- CommentReactionBody,
20
- CreateAttachmentBody,
21
- UploadAttachmentBody,
22
- PresignAttachmentBody,
23
- LogTimeBody,
24
- CreateWebhookBody,
25
- UpdateWebhookBody
26
- } from '@critical-path/core/schemas';
27
- import type {
28
- ActivityQuery,
29
- Page,
30
- TaskQuery,
31
- PublicWebhook,
32
- TaskDependency,
33
- Project,
34
- Task,
35
- TaskTodoItem,
36
- TaskStatus,
37
- Priority,
38
- TaskDependencyGraph,
39
- Team,
40
- TaskContainer,
41
- Iteration,
42
- Activity,
43
- Comment,
44
- CommentReaction,
45
- Attachment,
46
- PresignedUrlOptions,
47
- PresignedUploadResult,
48
- TimeEntry,
49
- Workflow,
50
- Deliverable,
51
- DeliverableSummary,
52
- CreateDeliverableInput,
53
- SemanticStatus,
54
- StatusDefinition,
55
- TaskDerivedStatus,
56
- TaskLifecycleState,
57
- CriticalPathAnalysis,
58
- TimelineLadder,
59
- TimelineLadderOptions,
60
- TaskLadderView,
61
- MacroTimelineSummary,
62
- MacroPhaseRollup,
63
- StandardTaskTimelineItem,
64
- ConcreteTaskEvidence,
65
- RealityDelta,
66
- TaskMetrics,
67
- TaskProgressHistory,
68
- TaskProgressInference,
69
- TaskEVM,
70
- TaskInferredActuals,
71
- ProgressInferenceSource,
72
- ProgressCurveProfile,
73
- WorkloadDistribution,
74
- WorkloadBucket,
75
- WorkloadDistributionOptions,
76
- WorkloadInterval,
77
- WorkloadGroupBy,
78
- WorkloadMetric
79
- } from '@critical-path/core';
80
-
81
- export type {
82
- TaskTodoItem,
83
- TaskStatus,
84
- Priority,
85
- SemanticStatus,
86
- StatusDefinition,
87
- TaskDerivedStatus,
88
- TaskLifecycleState,
89
- CriticalPathAnalysis,
90
- TimelineLadder,
91
- TimelineLadderOptions,
92
- TaskLadderView,
93
- MacroTimelineSummary,
94
- MacroPhaseRollup,
95
- StandardTaskTimelineItem,
96
- ConcreteTaskEvidence,
97
- RealityDelta,
98
- TaskMetrics,
99
- TaskProgressHistory,
100
- TaskProgressInference,
101
- TaskEVM,
102
- TaskInferredActuals,
103
- ProgressInferenceSource,
104
- ProgressCurveProfile,
105
- WorkloadDistribution,
106
- WorkloadBucket,
107
- WorkloadDistributionOptions,
108
- WorkloadInterval,
109
- WorkloadGroupBy,
110
- WorkloadMetric
111
- };
112
-
113
- export interface UpdateStatusOptions {
114
- taskId?: string;
115
- projectId?: string;
116
- details?: string;
117
- isEngaged?: boolean;
118
- }
119
-
120
- export interface UpdateStatusResult {
121
- success: boolean;
122
- status: string;
123
- timestamp: number;
124
- }
125
-
126
- type HeaderMap = Record<string, string>;
127
-
128
- export interface ClientOptions {
129
- /** API base URL, e.g. `http://localhost:3000/api/critical-path`. */
130
- baseUrl: string;
131
- /**
132
- * Headers sent with every request. Pass a function to compute them per request, e.g. to attach
133
- * a freshly refreshed access token: `headers: async () => ({ Authorization: `Bearer ${await getToken()}` })`.
134
- */
135
- headers?: HeaderMap | (() => HeaderMap | Promise<HeaderMap>);
136
- fetch?: typeof fetch;
137
- /** Abort requests that take longer than this. Default: no timeout. */
138
- timeoutMs?: number;
139
- /**
140
- * Retry GET requests that fail with a network error, 429, 502, 503 or 504. Default: no retries.
141
- * Delays double from `baseDelayMs` (default 250 ms) and honour `Retry-After` on 429/503.
142
- */
143
- retry?: { retries: number; baseDelayMs?: number };
144
- }
145
-
146
- /** Per-call options for a scoped client (see `CriticalPathClient.with`). */
147
- export interface RequestOptions {
148
- signal?: AbortSignal;
149
- headers?: HeaderMap;
150
- timeoutMs?: number;
151
- }
152
-
153
- /**
154
- * Error thrown for non-2xx responses. Carries the HTTP status and the server's error details
155
- * (`issues` for validation errors, `cyclePath` for dependency cycles, and so on).
156
- */
157
- export class CriticalPathError extends Error {
158
- /** HTTP status code. */
159
- readonly status: number;
160
- /** Field-level validation problems for 400 responses. */
161
- readonly issues?: Array<{ path: string; message: string }>;
162
- /** The parsed JSON error body, if any (e.g. `fromStatus`, `toStatus`, `cyclePath`, `fieldKey`). */
163
- readonly body?: Record<string, unknown>;
164
-
165
- constructor(message: string, status: number, body?: Record<string, unknown>) {
166
- super(message);
167
- this.name = 'CriticalPathError';
168
- this.status = status;
169
- this.body = body;
170
- this.issues = Array.isArray(body?.issues) ? (body!.issues as CriticalPathError['issues']) : undefined;
171
- }
172
-
173
- get isNotFound(): boolean {
174
- return this.status === 404;
175
- }
176
- }
177
-
178
- const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
179
- const sleep = (ms: number, signal?: AbortSignal | null) =>
180
- new Promise<void>((resolve, reject) => {
181
- const timer = setTimeout(resolve, ms);
182
- signal?.addEventListener('abort', () => {
183
- clearTimeout(timer);
184
- reject(signal.reason);
185
- }, { once: true });
186
- });
187
-
188
- function toBase64(data: Uint8Array | ArrayBuffer): string {
189
- const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
190
- let binary = '';
191
- for (let i = 0; i < bytes.length; i += 0x8000) {
192
- binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
193
- }
194
- return btoa(binary);
195
- }
196
-
197
- export class CriticalPathClient {
198
- private readonly baseUrl: string;
199
- private readonly options: ClientOptions;
200
- private readonly customFetch: typeof fetch;
201
- /** Set on scoped clients created by `with()`. */
202
- private readonly requestOptions?: RequestOptions;
203
-
204
- constructor(options: ClientOptions) {
205
- this.options = options;
206
- this.baseUrl = options.baseUrl.replace(/\/+$/, '');
207
- this.customFetch = options.fetch || ((...args) => globalThis.fetch(...args));
208
- }
209
-
210
- /**
211
- * Returns a client that applies `options` to every call, e.g. to cancel requests when a
212
- * component unmounts: `client.with({ signal: controller.signal }).getTasks(projectId)`.
213
- */
214
- with(options: RequestOptions): CriticalPathClient {
215
- const scoped = Object.create(this) as CriticalPathClient;
216
- const merged: RequestOptions = {
217
- ...this.requestOptions,
218
- ...options,
219
- headers: { ...this.requestOptions?.headers, ...options.headers }
220
- };
221
- Object.defineProperty(scoped, 'requestOptions', { value: merged });
222
- return scoped;
223
- }
224
-
225
- /**
226
- * Sends a DELETE and resolves to `false` when the resource does not exist (HTTP 404),
227
- * so delete methods keep their boolean contract instead of throwing.
228
- */
229
- private async deleteRequest(endpoint: string): Promise<boolean> {
230
- try {
231
- const res = await this.request<{ success: boolean }>(endpoint, { method: 'DELETE' });
232
- return res.success;
233
- } catch (err) {
234
- if (err instanceof CriticalPathError && err.isNotFound) return false;
235
- throw err;
236
- }
237
- }
238
-
239
- private async request<T>(endpoint: string, init: RequestInit = {}): Promise<T> {
240
- const url = `${this.baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
241
- const method = (init.method ?? 'GET').toUpperCase();
242
- const configured = typeof this.options.headers === 'function' ? await this.options.headers() : this.options.headers;
243
- const headers: HeaderMap = {
244
- ...(init.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
245
- ...configured,
246
- ...this.requestOptions?.headers
247
- };
248
- const timeoutMs = this.requestOptions?.timeoutMs ?? this.options.timeoutMs;
249
- const signals = [this.requestOptions?.signal, timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined].filter(
250
- (s): s is AbortSignal => !!s
251
- );
252
- const signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
253
- const retries = method === 'GET' ? this.options.retry?.retries ?? 0 : 0;
254
- const baseDelay = this.options.retry?.baseDelayMs ?? 250;
255
-
256
- for (let attempt = 0; ; attempt++) {
257
- let response: Response;
258
- try {
259
- response = await this.customFetch(url, { ...init, method, headers, signal });
260
- } catch (err) {
261
- // Network failure; never retry after an abort or timeout.
262
- if (attempt < retries && !signal?.aborted) {
263
- await sleep(baseDelay * 2 ** attempt, signal);
264
- continue;
265
- }
266
- throw err;
267
- }
268
-
269
- if (response.ok) return response.json() as Promise<T>;
270
-
271
- if (attempt < retries && RETRYABLE_STATUSES.has(response.status)) {
272
- const retryAfter = Number(response.headers.get('Retry-After'));
273
- await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : baseDelay * 2 ** attempt, signal);
274
- continue;
275
- }
276
-
277
- let body: Record<string, unknown> | undefined;
278
- try {
279
- body = await response.json();
280
- } catch {
281
- // Non-JSON error body
282
- }
283
- const message = typeof body?.error === 'string' ? body.error : `HTTP Error ${response.status}: ${response.statusText}`;
284
- throw new CriticalPathError(message, response.status, body);
285
- }
286
- }
287
-
288
-
289
- // Workflows
290
- async getWorkflows(): Promise<Workflow[]> {
291
- const res = await this.request<{ workflows: Workflow[] }>('/workflows');
292
- return res.workflows;
293
- }
294
-
295
- async getWorkflow(id: string): Promise<Workflow> {
296
- const res = await this.request<{ workflow: Workflow }>(`/workflows/${encodeURIComponent(id)}`);
297
- return res.workflow;
298
- }
299
-
300
- async createWorkflow(data: CreateWorkflowBody): Promise<Workflow> {
301
- const res = await this.request<{ workflow: Workflow }>('/workflows', {
302
- method: 'POST',
303
- body: JSON.stringify(data)
304
- });
305
- return res.workflow;
306
- }
307
-
308
- async updateWorkflow(id: string, updates: UpdateWorkflowBody): Promise<Workflow> {
309
- const res = await this.request<{ workflow: Workflow }>(`/workflows/${encodeURIComponent(id)}`, {
310
- method: 'PATCH',
311
- body: JSON.stringify(updates)
312
- });
313
- return res.workflow;
314
- }
315
-
316
- async deleteWorkflow(id: string): Promise<boolean> {
317
- return this.deleteRequest(`/workflows/${encodeURIComponent(id)}`);
318
- }
319
-
320
- // Projects
321
- async getProjects(): Promise<Project[]> {
322
- const res = await this.request<{ projects: Project[] }>('/projects');
323
- return res.projects;
324
- }
325
-
326
- async getProject(id: string): Promise<Project> {
327
- const res = await this.request<{ project: Project }>(`/projects/${encodeURIComponent(id)}`);
328
- return res.project;
329
- }
330
-
331
- async createProject(data: CreateProjectBody): Promise<Project> {
332
- const res = await this.request<{ project: Project }>('/projects', {
333
- method: 'POST',
334
- body: JSON.stringify(data)
335
- });
336
- return res.project;
337
- }
338
-
339
- async updateProject(id: string, updates: UpdateProjectBody): Promise<Project> {
340
- const res = await this.request<{ project: Project }>(`/projects/${encodeURIComponent(id)}`, {
341
- method: 'PATCH',
342
- body: JSON.stringify(updates)
343
- });
344
- return res.project;
345
- }
346
-
347
- /** Deletes a project and its tasks. Resolves to `false` if it does not exist. */
348
- async deleteProject(id: string): Promise<boolean> {
349
- return this.deleteRequest(`/projects/${encodeURIComponent(id)}`);
350
- }
351
-
352
- async calculateCriticalPath(projectId: string): Promise<CriticalPathAnalysis> {
353
- const res = await this.request<{ analysis: CriticalPathAnalysis }>(
354
- `/projects/${encodeURIComponent(projectId)}/critical-path`
355
- );
356
- return res.analysis;
357
- }
358
-
359
- async getTimelineLadder(
360
- projectId: string,
361
- options: TimelineLadderOptions = {}
362
- ): Promise<TimelineLadder> {
363
- const params = new URLSearchParams();
364
- if (options.level) params.set('level', options.level);
365
- if (options.containerId) params.set('containerId', options.containerId);
366
- if (options.iterationId) params.set('iterationId', options.iterationId);
367
- const qs = params.toString();
368
- const query = qs ? `?${qs}` : '';
369
- const res = await this.request<{ ladder: TimelineLadder }>(
370
- `/projects/${encodeURIComponent(projectId)}/ladder${query}`
371
- );
372
- return res.ladder;
373
- }
374
-
375
- async getWorkloadDistribution(
376
- projectId?: string,
377
- options: WorkloadDistributionOptions = {}
378
- ): Promise<WorkloadDistribution> {
379
- const params = new URLSearchParams();
380
- if (options.startDate) params.set('startDate', options.startDate);
381
- if (options.endDate) params.set('endDate', options.endDate);
382
- if (options.interval) params.set('interval', options.interval);
383
- if (options.groupBy) params.set('groupBy', options.groupBy);
384
- if (options.metric) params.set('metric', options.metric);
385
- if (typeof options.defaultWeeklyCapacityHours === 'number') {
386
- params.set('defaultWeeklyCapacityHours', options.defaultWeeklyCapacityHours.toString());
387
- }
388
-
389
- const path = projectId
390
- ? `/projects/${encodeURIComponent(projectId)}/workload`
391
- : '/workload';
392
- const qs = params.toString();
393
- const query = qs ? `?${qs}` : '';
394
-
395
- const res = await this.request<{ workload: WorkloadDistribution }>(`${path}${query}`);
396
- return res.workload;
397
- }
398
-
399
- // Tasks
400
- /** All tasks (optionally in one project), following pagination. Prefer `queryTasks` for large lists. */
401
- async getTasks(projectId?: string): Promise<Task[]> {
402
- return this.collectPages((cursor) => this.queryTasks({ projectId, cursor, limit: 500 }));
403
- }
404
-
405
- /**
406
- * One page of tasks, oldest first. Filters: `status` and `priority` (arrays), `assigneeId`,
407
- * `iterationId`, `deliverableId`, `containerId`, `parentId` (`null` for top-level tasks).
408
- */
409
- async queryTasks(query: TaskQuery = {}): Promise<Page<Task>> {
410
- const params = new URLSearchParams();
411
- for (const [key, value] of Object.entries(query)) {
412
- if (value === undefined || key === 'projectIds') continue;
413
- if (key === 'parentId' && value === null) params.set('parentId', 'none');
414
- else params.set(key, Array.isArray(value) ? value.join(',') : String(value));
415
- }
416
- const qs = params.size ? `?${params}` : '';
417
- const res = await this.request<{ tasks: Task[]; nextCursor?: string }>(`/tasks${qs}`);
418
- return { items: res.tasks, nextCursor: res.nextCursor };
419
- }
420
-
421
- private async collectPages<T>(fetchPage: (cursor?: string) => Promise<Page<T>>): Promise<T[]> {
422
- const items: T[] = [];
423
- let cursor: string | undefined;
424
- do {
425
- const page = await fetchPage(cursor);
426
- items.push(...page.items);
427
- cursor = page.nextCursor;
428
- } while (cursor);
429
- return items;
430
- }
431
-
432
- async getTask(id: string): Promise<Task> {
433
- const res = await this.request<{ task: Task }>(`/tasks/${encodeURIComponent(id)}`);
434
- return res.task;
435
- }
436
-
437
- async getTaskLadder(taskId: string): Promise<TaskLadderView> {
438
- const res = await this.request<{ taskLadder: TaskLadderView }>(
439
- `/tasks/${encodeURIComponent(taskId)}/ladder`
440
- );
441
- return res.taskLadder;
442
- }
443
-
444
- async getTaskMetrics(taskId: string): Promise<TaskMetrics> {
445
- const res = await this.request<{ metrics: TaskMetrics }>(
446
- `/tasks/${encodeURIComponent(taskId)}/metrics`
447
- );
448
- return res.metrics;
449
- }
450
-
451
- async getTaskProgressHistory(taskId: string): Promise<TaskProgressHistory> {
452
- const res = await this.request<{ progressHistory: TaskProgressHistory }>(
453
- `/tasks/${encodeURIComponent(taskId)}/progress-history`
454
- );
455
- return res.progressHistory;
456
- }
457
-
458
- async createTask(data: CreateTaskBody): Promise<Task> {
459
- const res = await this.request<{ task: Task }>('/tasks', {
460
- method: 'POST',
461
- body: JSON.stringify(data)
462
- });
463
- return res.task;
464
- }
465
-
466
- async updateTask(id: string, updates: UpdateTaskBody): Promise<Task> {
467
- const res = await this.request<{ task: Task }>(`/tasks/${encodeURIComponent(id)}`, {
468
- method: 'PATCH',
469
- body: JSON.stringify(updates)
470
- });
471
- return res.task;
472
- }
473
-
474
- async createSubtask(
475
- parentTaskId: string,
476
- data: {
477
- title: string;
478
- description?: string;
479
- status?: TaskStatus;
480
- priority?: Priority;
481
- projectId?: string;
482
- assigneeId?: string;
483
- customFields?: Record<string, unknown>;
484
- }
485
- ): Promise<Task> {
486
- let projectId = data.projectId;
487
- if (!projectId) {
488
- const parentTask = await this.getTask(parentTaskId);
489
- projectId = parentTask.projectId;
490
- }
491
- return this.createTask({
492
- projectId,
493
- title: data.title,
494
- description: data.description,
495
- parentId: parentTaskId,
496
- status: data.status || 'todo',
497
- priority: data.priority || 'medium',
498
- assigneeId: data.assigneeId,
499
- customFields: data.customFields
500
- });
501
- }
502
-
503
- async addTodo(
504
- taskId: string,
505
- title: string
506
- ): Promise<TaskTodoItem> {
507
- const task = await this.getTask(taskId);
508
- const item: TaskTodoItem = {
509
- id: `todo_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
510
- title,
511
- completed: false,
512
- createdAt: new Date().toISOString()
513
- };
514
- const todos = [...(task.todos || []), item];
515
- await this.updateTask(taskId, { todos });
516
- return item;
517
- }
518
-
519
- async toggleTodo(
520
- taskId: string,
521
- todoIdOrTitle: string,
522
- completed?: boolean
523
- ): Promise<Task> {
524
- const task = await this.getTask(taskId);
525
- if (!task.todos || task.todos.length === 0) {
526
- throw new Error(`Task ${taskId} has no checklist items.`);
527
- }
528
- const item = task.todos.find(
529
- (t) => t.id === todoIdOrTitle || t.title.toLowerCase() === todoIdOrTitle.toLowerCase()
530
- );
531
- if (!item) {
532
- throw new Error(`Checklist item "${todoIdOrTitle}" not found on task ${taskId}.`);
533
- }
534
- item.completed = completed !== undefined ? completed : !item.completed;
535
- item.completedAt = item.completed ? new Date().toISOString() : undefined;
536
- return this.updateTask(taskId, { todos: task.todos });
537
- }
538
-
539
- // Webhooks
540
- async getWebhooks(): Promise<PublicWebhook[]> {
541
- return (await this.request<{ webhooks: PublicWebhook[] }>('/webhooks')).webhooks;
542
- }
543
-
544
- async getWebhook(id: string): Promise<PublicWebhook> {
545
- return (await this.request<{ webhook: PublicWebhook }>(`/webhooks/${encodeURIComponent(id)}`)).webhook;
546
- }
547
-
548
- /** Registers a webhook. The returned `secret` is shown only once; store it to verify deliveries. */
549
- async createWebhook(data: CreateWebhookBody): Promise<{ webhook: PublicWebhook; secret: string }> {
550
- return this.request<{ webhook: PublicWebhook; secret: string }>('/webhooks', {
551
- method: 'POST',
552
- body: JSON.stringify(data)
553
- });
554
- }
555
-
556
- async updateWebhook(id: string, updates: UpdateWebhookBody): Promise<PublicWebhook> {
557
- const res = await this.request<{ webhook: PublicWebhook }>(`/webhooks/${encodeURIComponent(id)}`, {
558
- method: 'PATCH',
559
- body: JSON.stringify(updates)
560
- });
561
- return res.webhook;
562
- }
563
-
564
- async deleteWebhook(id: string): Promise<boolean> {
565
- return this.deleteRequest(`/webhooks/${encodeURIComponent(id)}`);
566
- }
567
-
568
- /** Declares that `taskId` depends on another task. Rejects with a 409 error on cycles. */
569
- async addDependency(taskId: string, data: CreateDependencyBody): Promise<TaskDependency> {
570
- const res = await this.request<{ dependency: TaskDependency }>(`/tasks/${encodeURIComponent(taskId)}/dependencies`, {
571
- method: 'POST',
572
- body: JSON.stringify(data)
573
- });
574
- return res.dependency;
575
- }
576
-
577
- /**
578
- * Deletes a task with its subtasks, dependencies, comments, attachments and time entries.
579
- * Pass `{ subtasks: 'detach' }` to keep subtasks (their parent link is cleared).
580
- */
581
- async deleteTask(id: string, options: { subtasks?: 'delete' | 'detach' } = {}): Promise<boolean> {
582
- const query = options.subtasks ? `?subtasks=${options.subtasks}` : '';
583
- return this.deleteRequest(`/tasks/${encodeURIComponent(id)}${query}`);
584
- }
585
-
586
- async removeDependency(taskId: string, dependencyId: string): Promise<boolean> {
587
- return this.deleteRequest(`/tasks/${encodeURIComponent(taskId)}/dependencies/${encodeURIComponent(dependencyId)}`);
588
- }
589
-
590
- async getTaskDependencies(taskId: string): Promise<TaskDependencyGraph> {
591
- const res = await this.request<{ graph: TaskDependencyGraph }>(`/tasks/${encodeURIComponent(taskId)}/dependencies`);
592
- return res.graph;
593
- }
594
-
595
- async getTaskLifecycleState(taskId: string): Promise<TaskDerivedStatus> {
596
- const res = await this.request<{ state: TaskDerivedStatus }>(`/tasks/${encodeURIComponent(taskId)}/lifecycle`);
597
- return res.state;
598
- }
599
-
600
- async getAllowedTaskTransitions(taskId: string): Promise<string[]> {
601
- const res = await this.request<{ allowedNextStatuses: string[]; allowedPreviousStatuses?: string[] }>(`/tasks/${encodeURIComponent(taskId)}/transitions`);
602
- return res.allowedNextStatuses;
603
- }
604
-
605
- async getAllowedPreviousTaskTransitions(taskId: string): Promise<string[]> {
606
- const res = await this.request<{ allowedNextStatuses: string[]; allowedPreviousStatuses?: string[] }>(`/tasks/${encodeURIComponent(taskId)}/transitions`);
607
- return res.allowedPreviousStatuses || [];
608
- }
609
-
610
- // Teams
611
- async getTeams(): Promise<Team[]> {
612
- const res = await this.request<{ teams: Team[] }>('/teams');
613
- return res.teams;
614
- }
615
-
616
- async getTeam(id: string): Promise<Team> {
617
- const res = await this.request<{ team: Team }>(`/teams/${encodeURIComponent(id)}`);
618
- return res.team;
619
- }
620
-
621
- async createTeam(data: CreateTeamBody): Promise<Team> {
622
- const res = await this.request<{ team: Team }>('/teams', {
623
- method: 'POST',
624
- body: JSON.stringify(data)
625
- });
626
- return res.team;
627
- }
628
-
629
- async updateTeam(id: string, updates: UpdateTeamBody): Promise<Team> {
630
- const res = await this.request<{ team: Team }>(`/teams/${encodeURIComponent(id)}`, {
631
- method: 'PATCH',
632
- body: JSON.stringify(updates)
633
- });
634
- return res.team;
635
- }
636
-
637
- async deleteTeam(id: string): Promise<boolean> {
638
- return this.deleteRequest(`/teams/${encodeURIComponent(id)}`);
639
- }
640
-
641
- // Containers
642
- async getContainers(projectId: string): Promise<TaskContainer[]> {
643
- const res = await this.request<{ containers: TaskContainer[] }>(`/containers?projectId=${encodeURIComponent(projectId)}`);
644
- return res.containers;
645
- }
646
-
647
- async getContainer(id: string): Promise<TaskContainer> {
648
- return (await this.request<{ container: TaskContainer }>(`/containers/${encodeURIComponent(id)}`)).container;
649
- }
650
-
651
- async createContainer(data: CreateContainerBody): Promise<TaskContainer> {
652
- const res = await this.request<{ container: TaskContainer }>('/containers', {
653
- method: 'POST',
654
- body: JSON.stringify(data)
655
- });
656
- return res.container;
657
- }
658
-
659
- async updateContainer(id: string, updates: UpdateContainerBody): Promise<TaskContainer> {
660
- const res = await this.request<{ container: TaskContainer }>(`/containers/${encodeURIComponent(id)}`, {
661
- method: 'PATCH',
662
- body: JSON.stringify(updates)
663
- });
664
- return res.container;
665
- }
666
-
667
- async deleteContainer(id: string): Promise<boolean> {
668
- return this.deleteRequest(`/containers/${encodeURIComponent(id)}`);
669
- }
670
-
671
- // Deliverables
672
- async getDeliverables(projectId: string): Promise<Deliverable[]> {
673
- const res = await this.request<{ deliverables: Deliverable[] }>(`/deliverables?projectId=${encodeURIComponent(projectId)}`);
674
- return res.deliverables;
675
- }
676
-
677
- async getDeliverable(id: string): Promise<Deliverable> {
678
- const res = await this.request<{ deliverable: Deliverable }>(`/deliverables/${encodeURIComponent(id)}`);
679
- return res.deliverable;
680
- }
681
-
682
- async getDeliverableSummary(id: string): Promise<DeliverableSummary> {
683
- const res = await this.request<{ summary: DeliverableSummary }>(`/deliverables/${encodeURIComponent(id)}/summary`);
684
- return res.summary;
685
- }
686
-
687
- async createDeliverable(data: CreateDeliverableBody): Promise<Deliverable> {
688
- const res = await this.request<{ deliverable: Deliverable }>('/deliverables', {
689
- method: 'POST',
690
- body: JSON.stringify(data)
691
- });
692
- return res.deliverable;
693
- }
694
-
695
- async updateDeliverable(id: string, updates: UpdateDeliverableBody): Promise<Deliverable> {
696
- const res = await this.request<{ deliverable: Deliverable }>(`/deliverables/${encodeURIComponent(id)}`, {
697
- method: 'PATCH',
698
- body: JSON.stringify(updates)
699
- });
700
- return res.deliverable;
701
- }
702
-
703
- async deleteDeliverable(id: string): Promise<boolean> {
704
- return this.deleteRequest(`/deliverables/${encodeURIComponent(id)}`);
705
- }
706
-
707
- // Iterations
708
- async getIterations(projectId: string): Promise<Iteration[]> {
709
- const res = await this.request<{ iterations: Iteration[] }>(`/iterations?projectId=${encodeURIComponent(projectId)}`);
710
- return res.iterations;
711
- }
712
-
713
- async getIteration(id: string): Promise<Iteration> {
714
- return (await this.request<{ iteration: Iteration }>(`/iterations/${encodeURIComponent(id)}`)).iteration;
715
- }
716
-
717
- async createIteration(data: CreateIterationBody): Promise<Iteration> {
718
- const res = await this.request<{ iteration: Iteration }>('/iterations', {
719
- method: 'POST',
720
- body: JSON.stringify(data)
721
- });
722
- return res.iteration;
723
- }
724
-
725
- async updateIteration(id: string, updates: UpdateIterationBody): Promise<Iteration> {
726
- const res = await this.request<{ iteration: Iteration }>(`/iterations/${encodeURIComponent(id)}`, {
727
- method: 'PATCH',
728
- body: JSON.stringify(updates)
729
- });
730
- return res.iteration;
731
- }
732
-
733
- async deleteIteration(id: string): Promise<boolean> {
734
- return this.deleteRequest(`/iterations/${encodeURIComponent(id)}`);
735
- }
736
-
737
- // Activity Stream
738
- /** The whole activity feed (newest first), following pagination. Prefer `queryActivities` for feeds. */
739
- async getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]> {
740
- return this.collectPages((cursor) => this.queryActivities({ ...filter, cursor, limit: 500 }));
741
- }
742
-
743
- /** One page of the activity feed, newest first. */
744
- async queryActivities(query: ActivityQuery = {}): Promise<Page<Activity>> {
745
- const params = new URLSearchParams();
746
- for (const [key, value] of Object.entries(query)) {
747
- if (value !== undefined && key !== 'projectIds') params.set(key, String(value));
748
- }
749
- const qs = params.size ? `?${params}` : '';
750
- const res = await this.request<{ activities: Activity[]; nextCursor?: string }>(`/activities${qs}`);
751
- return { items: res.activities, nextCursor: res.nextCursor };
752
- }
753
-
754
- // Comments
755
- async getComments(taskId: string): Promise<Comment[]> {
756
- const res = await this.request<{ comments: Comment[] }>(`/comments?taskId=${encodeURIComponent(taskId)}`);
757
- return res.comments;
758
- }
759
-
760
- async getComment(id: string): Promise<Comment> {
761
- const res = await this.request<{ comment: Comment }>(`/comments/${encodeURIComponent(id)}`);
762
- return res.comment;
763
- }
764
-
765
- async addComment(data: CreateCommentBody): Promise<Comment> {
766
- const res = await this.request<{ comment: Comment }>('/comments', {
767
- method: 'POST',
768
- body: JSON.stringify(data)
769
- });
770
- return res.comment;
771
- }
772
-
773
- async updateComment(id: string, updates: UpdateCommentBody): Promise<Comment> {
774
- const res = await this.request<{ comment: Comment }>(`/comments/${encodeURIComponent(id)}`, {
775
- method: 'PATCH',
776
- body: JSON.stringify(updates)
777
- });
778
- return res.comment;
779
- }
780
-
781
- async deleteComment(id: string): Promise<boolean> {
782
- return this.deleteRequest(`/comments/${encodeURIComponent(id)}`);
783
- }
784
-
785
- async addCommentReaction(commentId: string, reaction: CommentReactionBody): Promise<Comment> {
786
- const res = await this.request<{ comment: Comment }>(`/comments/${encodeURIComponent(commentId)}/reactions`, {
787
- method: 'POST',
788
- body: JSON.stringify(reaction)
789
- });
790
- return res.comment;
791
- }
792
-
793
- async removeCommentReaction(commentId: string, reaction: CommentReactionBody): Promise<Comment> {
794
- const query = new URLSearchParams({ emoji: reaction.emoji });
795
- const res = await this.request<{ comment: Comment }>(
796
- `/comments/${encodeURIComponent(commentId)}/reactions?${query}`,
797
- { method: 'DELETE' }
798
- );
799
- return res.comment;
800
- }
801
-
802
- // Attachments
803
- async getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }): Promise<Attachment[]> {
804
- const params = new URLSearchParams();
805
- if (filter?.taskId) params.set('taskId', filter.taskId);
806
- if (filter?.projectId) params.set('projectId', filter.projectId);
807
- if (filter?.commentId) params.set('commentId', filter.commentId);
808
- const query = params.toString() ? `?${params.toString()}` : '';
809
- const res = await this.request<{ attachments: Attachment[] }>(`/attachments${query}`);
810
- return res.attachments;
811
- }
812
-
813
- async getAttachment(id: string): Promise<Attachment> {
814
- const res = await this.request<{ attachment: Attachment }>(`/attachments/${encodeURIComponent(id)}`);
815
- return res.attachment;
816
- }
817
-
818
- async createAttachment(data: CreateAttachmentBody): Promise<Attachment> {
819
- const res = await this.request<{ attachment: Attachment }>('/attachments', {
820
- method: 'POST',
821
- body: JSON.stringify(data)
822
- });
823
- return res.attachment;
824
- }
825
-
826
- /**
827
- * Uploads a file through the server's FileStorageAdapter. Binary data is sent base64-encoded.
828
- */
829
- async uploadAttachmentFile(
830
- data: Omit<UploadAttachmentBody, 'data'> & { data: string | Uint8Array | ArrayBuffer }
831
- ): Promise<Attachment> {
832
- const body: UploadAttachmentBody =
833
- typeof data.data === 'string'
834
- ? { ...data, data: data.data }
835
- : { ...data, data: toBase64(data.data), encoding: 'base64' };
836
- const res = await this.request<{ attachment: Attachment }>('/attachments/upload', {
837
- method: 'POST',
838
- body: JSON.stringify(body)
839
- });
840
- return res.attachment;
841
- }
842
-
843
- async deleteAttachment(id: string): Promise<boolean> {
844
- return this.deleteRequest(`/attachments/${encodeURIComponent(id)}`);
845
- }
846
-
847
- async getPresignedAttachmentUploadUrl(options: PresignAttachmentBody): Promise<PresignedUploadResult> {
848
- const res = await this.request<{ presigned: PresignedUploadResult }>('/attachments/presign', {
849
- method: 'POST',
850
- body: JSON.stringify(options)
851
- });
852
- return res.presigned;
853
- }
854
-
855
- // Time Entries
856
- async getTimeEntries(taskId: string): Promise<TimeEntry[]> {
857
- const res = await this.request<{ timeEntries: TimeEntry[] }>(`/time-entries?taskId=${encodeURIComponent(taskId)}`);
858
- return res.timeEntries;
859
- }
860
-
861
- async logTime(data: LogTimeBody): Promise<TimeEntry> {
862
- const res = await this.request<{ timeEntry: TimeEntry }>('/time-entries', {
863
- method: 'POST',
864
- body: JSON.stringify(data)
865
- });
866
- return res.timeEntry;
867
- }
868
-
869
- // Agent Status / Telemetry
870
- async updateStatus(
871
- status: string,
872
- options?: UpdateStatusOptions
873
- ): Promise<UpdateStatusResult> {
874
- const res = await this.request<UpdateStatusResult>('/status', {
875
- method: 'POST',
876
- body: JSON.stringify({
877
- status,
878
- taskId: options?.taskId,
879
- projectId: options?.projectId,
880
- details: options?.details,
881
- isEngaged: options?.isEngaged ?? true
882
- })
883
- });
884
- return res;
885
- }
886
- }
887
-
888
- export type { CommentReaction };