@parall/sdk 1.54.0 → 1.55.1

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/constants.ts CHANGED
@@ -371,6 +371,11 @@ export const ENDPOINTS = {
371
371
  ORG_MEMBERS: (orgId: string) => `${API_BASE}/orgs/${orgId}/members`,
372
372
  ORG_MEMBERS_FORMER: (orgId: string) => `${API_BASE}/orgs/${orgId}/members/former`,
373
373
  TEAMS: (orgId: string) => `${API_BASE}/orgs/${orgId}/teams`,
374
+ TEAM: (orgId: string, teamId: string) => `${API_BASE}/orgs/${orgId}/teams/${teamId}`,
375
+ TEAM_MEMBERS: (orgId: string, teamId: string) =>
376
+ `${API_BASE}/orgs/${orgId}/teams/${teamId}/members`,
377
+ TEAM_MEMBER: (orgId: string, teamId: string, userId: string) =>
378
+ `${API_BASE}/orgs/${orgId}/teams/${teamId}/members/${userId}`,
374
379
  ORG_MEMBERS_ONLINE: (orgId: string) => `${API_BASE}/orgs/${orgId}/members/online`,
375
380
  ORG_MEMBER: (orgId: string, userId: string) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
376
381
  ORG_MEMBER_CHATS: (orgId: string, memberId: string) =>
@@ -562,6 +567,7 @@ export const ENDPOINTS = {
562
567
 
563
568
  // Tasks (org-scoped)
564
569
  TASKS: (orgId: string) => `${API_BASE}/orgs/${orgId}/tasks`,
570
+ TASK_SUBTASK_SUMMARY: (orgId: string) => `${API_BASE}/orgs/${orgId}/tasks/subtask-summary`,
565
571
  TASK: (orgId: string, taskId: string) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}`,
566
572
  TASK_ARCHIVE: (orgId: string, taskId: string) =>
567
573
  `${API_BASE}/orgs/${orgId}/tasks/${taskId}/archive`,
@@ -585,10 +591,31 @@ export const ENDPOINTS = {
585
591
  `${API_BASE}/orgs/${orgId}/tasks/${taskId}/comments/${commentId}`,
586
592
  TASK_ACTIVITIES: (orgId: string, taskId: string) =>
587
593
  `${API_BASE}/orgs/${orgId}/tasks/${taskId}/activities`,
594
+ TASK_LABELS: (orgId: string, taskId: string) =>
595
+ `${API_BASE}/orgs/${orgId}/tasks/${taskId}/labels`,
596
+ TASK_LABEL: (orgId: string, taskId: string, labelId: string) =>
597
+ `${API_BASE}/orgs/${orgId}/tasks/${taskId}/labels/${labelId}`,
598
+
599
+ // Labels (org-scoped vocabulary; writes are admin-gated)
600
+ LABELS: (orgId: string) => `${API_BASE}/orgs/${orgId}/labels`,
601
+ LABEL: (orgId: string, labelId: string) => `${API_BASE}/orgs/${orgId}/labels/${labelId}`,
588
602
 
589
603
  // Projects (org-scoped)
590
604
  PROJECTS: (orgId: string) => `${API_BASE}/orgs/${orgId}/projects`,
605
+ PROJECT_TASK_SUMMARY: (orgId: string) => `${API_BASE}/orgs/${orgId}/projects/task-summary`,
591
606
  PROJECT: (orgId: string, projectId: string) => `${API_BASE}/orgs/${orgId}/projects/${projectId}`,
607
+ PROJECT_MEMBERS: (orgId: string, projectId: string) =>
608
+ `${API_BASE}/orgs/${orgId}/projects/${projectId}/members`,
609
+ PROJECT_READERS: (orgId: string, projectId: string) =>
610
+ `${API_BASE}/orgs/${orgId}/projects/${projectId}/readers`,
611
+ PROJECT_LIBRARY: (orgId: string) => `${API_BASE}/orgs/${orgId}/projects/library`,
612
+ PROJECT_JOIN: (orgId: string, projectId: string) =>
613
+ `${API_BASE}/orgs/${orgId}/projects/${projectId}/join`,
614
+ PROJECT_JOIN_REQUESTS: (orgId: string, projectId: string) =>
615
+ `${API_BASE}/orgs/${orgId}/projects/${projectId}/join-requests`,
616
+ // subject is a bare user ID or a bare team ID; both are path-segment safe.
617
+ PROJECT_MEMBER: (orgId: string, projectId: string, subject: string) =>
618
+ `${API_BASE}/orgs/${orgId}/projects/${projectId}/members/${subject}`,
592
619
 
593
620
  // Schedules (org-scoped, platform time trigger primitive)
594
621
  SCHEDULES: (orgId: string) => `${API_BASE}/orgs/${orgId}/schedules`,
@@ -653,8 +680,10 @@ export const ENDPOINTS = {
653
680
  SLACK_HISTORY: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/slack/history`,
654
681
  SLACK_MEMBERS: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/slack/members`,
655
682
  SLACK_STATUS: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/slack/status`,
656
- // WeChat tier-B read verb (agent-only; internal research preview).
683
+ // WeChat tier-B read verbs (agent-only; internal research preview).
657
684
  WECHAT_CONTACTS: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/wechat/contacts`,
685
+ WECHAT_PROFILE: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/wechat/profile`,
686
+ WECHAT_STATUS: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/wechat/status`,
658
687
 
659
688
  // Invitations (org-scoped, admin)
660
689
  ORG_INVITATIONS: (orgId: string) => `${API_BASE}/orgs/${orgId}/invitations`,
@@ -822,8 +851,11 @@ export const ENDPOINTS = {
822
851
  // Unified search (messages + tasks + wiki, org-scoped)
823
852
  SEARCH: (orgId: string) => `${API_BASE}/orgs/${orgId}/search`,
824
853
 
825
- // Feature flags (org-scoped, server-evaluated)
826
- FEATURE_FLAGS: (orgId: string) => `${API_BASE}/orgs/${orgId}/feature-flags`,
854
+ // Feature flags (server-evaluated). Org-scoped by default; omit the org for
855
+ // the pre-org form, which resolves "cap:" capabilities only (PostHog flags
856
+ // are org-scoped and absent there) for callers that have no org yet.
857
+ FEATURE_FLAGS: (orgId?: string) =>
858
+ orgId ? `${API_BASE}/orgs/${orgId}/feature-flags` : `${API_BASE}/feature-flags`,
827
859
 
828
860
  // Team templates (org-scoped, owner/admin only)
829
861
  TEMPLATES: (orgId: string) => `${API_BASE}/orgs/${orgId}/templates`,
@@ -905,6 +937,10 @@ export const ENDPOINTS = {
905
937
  // Per-profile egress proxy (hosted Cloud Profiles; manager-only, human-only).
906
938
  ORG_EDGE_PROFILE_PROXY: (orgId: string, edgeId: string, profileName: string) =>
907
939
  `/api/v1/orgs/${orgId}/edge/${edgeId}/profiles/${encodeURIComponent(profileName)}/proxy`,
940
+ // Per-profile one-shot cookie seed (hosted Cloud Profiles; manager-only,
941
+ // human-only, idle-only). Injected at the next cold start, then consumed.
942
+ ORG_EDGE_PROFILE_COOKIES: (orgId: string, edgeId: string, profileName: string) =>
943
+ `/api/v1/orgs/${orgId}/edge/${edgeId}/profiles/${encodeURIComponent(profileName)}/cookies`,
908
944
  ORG_EDGE_EXEC: (orgId: string) => `/api/v1/orgs/${orgId}/edge/exec`,
909
945
  // Cloud Edge live viewer command (V1b) — api-server, gated on cap:edge-viewer.
910
946
  // Same request/reply shape as the v2 browser-profile viewer, on the v3 edge
@@ -925,11 +961,13 @@ export const ENDPOINTS = {
925
961
  ORG_CLIPS_INSTALLED: (orgId: string) => `/api/v1/orgs/${orgId}/clips/installed`,
926
962
  ORG_CLIP_UNINSTALL: (orgId: string, clipId: string) =>
927
963
  `/api/v1/orgs/${orgId}/clips/${clipId}/uninstall`,
964
+ ORG_CLIP_MCP_REMOVE: (orgId: string, clipId: string) =>
965
+ `/api/v1/orgs/${orgId}/clips/${clipId}/mcp-remove`,
928
966
  // Per-org agent exec access for one clip (default all agents; PUT human-only)
929
967
  ORG_CLIP_EXEC_ACCESS: (orgId: string, clipId: string) =>
930
968
  `/api/v1/orgs/${orgId}/clips/${clipId}/exec-access`,
931
- // MCP clip server config (cap:clip-mcp; publisher-org only cross-org gets
932
- // 403 MCP_CROSS_ORG_DISABLED on the whole family, reads included). The
969
+ // MCP clip server config (cap:clip-mcp; cross-org is limited to installed,
970
+ // reviewed Official OAuth Clips). The
933
971
  // singular family addresses the org's DEFAULT connection (pre-165
934
972
  // back-compat shim); the plural family is the multi-connection surface —
935
973
  // one config row (credential slot) per connection, addressed by config id.
@@ -998,6 +1036,7 @@ export const WS_EVENTS = {
998
1036
  PONG: 'pong',
999
1037
  WATCHING: 'watching',
1000
1038
  MESSAGE_NEW: 'message.new',
1039
+ /** @deprecated `message.patch` is retired and never published; kept for compile compatibility. */
1001
1040
  MESSAGE_PATCH: 'message.patch',
1002
1041
  MESSAGE_EDIT: 'message.edit',
1003
1042
  MESSAGE_DELETE: 'message.delete',
@@ -1018,6 +1057,9 @@ export const WS_EVENTS = {
1018
1057
  PROJECT_CREATED: 'project.created',
1019
1058
  PROJECT_UPDATED: 'project.updated',
1020
1059
  PROJECT_DELETED: 'project.deleted',
1060
+ LABEL_CREATED: 'label.created',
1061
+ LABEL_UPDATED: 'label.updated',
1062
+ LABEL_DELETED: 'label.deleted',
1021
1063
  AGENT_SESSION_NEW: 'agent_session.new',
1022
1064
  AGENT_SESSION_UPDATE: 'agent_session.update',
1023
1065
  AGENT_STEP_NEW: 'agent_step.new',
@@ -1028,6 +1070,7 @@ export const WS_EVENTS = {
1028
1070
  ORG_JOIN_REQUEST_NEW: 'org.join_request.new',
1029
1071
  ORG_INVITE_LINK_JOINED: 'org.invite_link.joined',
1030
1072
  ORG_MEMBER_REMOVED: 'org.member.removed',
1073
+ ORG_MEMBER_PROFILE_UPDATED: 'org.member.profile.updated',
1031
1074
  AGENT_CONFIG_UPDATE: 'agent_config.update',
1032
1075
  PRESENCE_UPDATE: 'presence.update',
1033
1076
  WIKI_CHANGESET_CREATED: 'wiki.changeset.created',
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ export * from './browser-viewer.js';
2
2
  export type { ParallClientOptions } from './client.js';
3
3
  export { ApiError, ParallClient } from './client.js';
4
4
  export * from './constants.js';
5
+ export * from './subject.js';
6
+ export * from './task-label-types.js';
5
7
  export * from './types.js';
6
8
  export type {
7
9
  ParallWsOptions,
@@ -0,0 +1,342 @@
1
+ import { ENDPOINTS } from './constants.js';
2
+ import type {
3
+ AddProjectMemberRequest,
4
+ Approval,
5
+ CreateProjectRequest,
6
+ CreateTaskCommentRequest,
7
+ CreateTaskRelationRequest,
8
+ CreateTaskRequest,
9
+ PaginatedResponse,
10
+ Project,
11
+ ProjectJoinRequest,
12
+ ProjectLibraryEntry,
13
+ ProjectMember,
14
+ ProjectTaskSummaryResponse,
15
+ Task,
16
+ TaskActivity,
17
+ TaskComment,
18
+ TaskRelation,
19
+ TaskRelationTargetType,
20
+ TaskSubtaskSummaryResponse,
21
+ TaskWatcher,
22
+ ThreadWatcher,
23
+ UpdateProjectMemberRequest,
24
+ UpdateProjectRequest,
25
+ UpdateTaskCommentRequest,
26
+ UpdateTaskRequest,
27
+ } from './types.js';
28
+
29
+ /** Project and task REST methods shared by every ParallClient instance. */
30
+ export abstract class ProjectTaskClient {
31
+ protected abstract request<T>(
32
+ method: string,
33
+ path: string,
34
+ body?: unknown,
35
+ query?: Record<string, string | number | boolean | undefined>,
36
+ ): Promise<T>;
37
+
38
+ async createTask(orgId: string, data: CreateTaskRequest): Promise<Task> {
39
+ return this.request('POST', ENDPOINTS.TASKS(orgId), data);
40
+ }
41
+
42
+ async getTasks(
43
+ orgId: string,
44
+ params?: {
45
+ q?: string;
46
+ status?: string;
47
+ priority?: string;
48
+ assignee_id?: string;
49
+ parent_id?: string;
50
+ project_id?: string;
51
+ /** Comma-separated label IDs; matching uses any-of semantics. */
52
+ label_ids?: string;
53
+ limit?: number;
54
+ cursor?: string;
55
+ sort?: string;
56
+ order?: string;
57
+ scope?: 'active' | 'archived' | 'all';
58
+ },
59
+ ): Promise<PaginatedResponse<Task>> {
60
+ return this.request('GET', ENDPOINTS.TASKS(orgId), undefined, params);
61
+ }
62
+
63
+ async getTaskSubtaskSummary(
64
+ orgId: string,
65
+ params?: { assignee_id?: string; project_id?: string },
66
+ ): Promise<TaskSubtaskSummaryResponse> {
67
+ return this.request('GET', ENDPOINTS.TASK_SUBTASK_SUMMARY(orgId), undefined, params);
68
+ }
69
+
70
+ async getTask(orgId: string, taskId: string): Promise<Task> {
71
+ return this.request('GET', ENDPOINTS.TASK(orgId, taskId));
72
+ }
73
+
74
+ async updateTask(orgId: string, taskId: string, data: UpdateTaskRequest): Promise<Task> {
75
+ return this.request('PATCH', ENDPOINTS.TASK(orgId, taskId), data);
76
+ }
77
+
78
+ async deleteTask(orgId: string, taskId: string, opts?: { force?: boolean }): Promise<void> {
79
+ return this.request(
80
+ 'DELETE',
81
+ ENDPOINTS.TASK(orgId, taskId),
82
+ undefined,
83
+ opts?.force ? { force: 'true' } : undefined,
84
+ );
85
+ }
86
+
87
+ async archiveTask(orgId: string, taskId: string): Promise<void> {
88
+ return this.request('POST', ENDPOINTS.TASK_ARCHIVE(orgId, taskId));
89
+ }
90
+
91
+ async restoreTask(orgId: string, taskId: string): Promise<void> {
92
+ return this.request('POST', ENDPOINTS.TASK_RESTORE(orgId, taskId));
93
+ }
94
+
95
+ async watchTask(orgId: string, taskId: string): Promise<void> {
96
+ return this.request('POST', ENDPOINTS.TASK_WATCH(orgId, taskId));
97
+ }
98
+
99
+ async unwatchTask(orgId: string, taskId: string): Promise<void> {
100
+ return this.request('DELETE', ENDPOINTS.TASK_WATCH(orgId, taskId));
101
+ }
102
+
103
+ async getTaskWatchers(orgId: string, taskId: string): Promise<TaskWatcher[]> {
104
+ const res = await this.request<{ data: TaskWatcher[] }>(
105
+ 'GET',
106
+ ENDPOINTS.TASK_WATCHERS(orgId, taskId),
107
+ );
108
+ return res.data;
109
+ }
110
+
111
+ async subscribeTaskMember(orgId: string, taskId: string, userId: string): Promise<void> {
112
+ return this.request('PUT', ENDPOINTS.TASK_SUBSCRIBER(orgId, taskId, userId));
113
+ }
114
+
115
+ async unsubscribeTaskMember(orgId: string, taskId: string, userId: string): Promise<void> {
116
+ return this.request('DELETE', ENDPOINTS.TASK_SUBSCRIBER(orgId, taskId, userId));
117
+ }
118
+
119
+ async watchThread(threadRootId: string): Promise<void> {
120
+ return this.request('POST', ENDPOINTS.MESSAGE_WATCH(threadRootId));
121
+ }
122
+
123
+ async unwatchThread(threadRootId: string): Promise<void> {
124
+ return this.request('DELETE', ENDPOINTS.MESSAGE_WATCH(threadRootId));
125
+ }
126
+
127
+ async getThreadWatchers(threadRootId: string): Promise<ThreadWatcher[]> {
128
+ const res = await this.request<{ data: ThreadWatcher[] }>(
129
+ 'GET',
130
+ ENDPOINTS.MESSAGE_WATCHERS(threadRootId),
131
+ );
132
+ return res.data;
133
+ }
134
+
135
+ async isWatchingThread(threadRootId: string): Promise<boolean> {
136
+ const res = await this.request<{ watching: boolean }>(
137
+ 'GET',
138
+ ENDPOINTS.MESSAGE_WATCHING(threadRootId),
139
+ );
140
+ return res.watching;
141
+ }
142
+
143
+ async getSubtasks(orgId: string, taskId: string): Promise<Task[]> {
144
+ const res = await this.request<PaginatedResponse<Task>>(
145
+ 'GET',
146
+ ENDPOINTS.TASK_SUBTASKS(orgId, taskId),
147
+ );
148
+ return res.data;
149
+ }
150
+
151
+ async createTaskRelation(
152
+ orgId: string,
153
+ taskId: string,
154
+ data: CreateTaskRelationRequest,
155
+ ): Promise<TaskRelation> {
156
+ return this.request('POST', ENDPOINTS.TASK_RELATIONS(orgId, taskId), data);
157
+ }
158
+
159
+ async getTaskRelations(orgId: string, taskId: string): Promise<TaskRelation[]> {
160
+ const res = await this.request<{ data: TaskRelation[] }>(
161
+ 'GET',
162
+ ENDPOINTS.TASK_RELATIONS(orgId, taskId),
163
+ );
164
+ return res.data;
165
+ }
166
+
167
+ async listTaskRelationsByTarget(
168
+ orgId: string,
169
+ params: { target_type: TaskRelationTargetType; target_id: string },
170
+ ): Promise<TaskRelation[]> {
171
+ const query = new URLSearchParams({
172
+ target_type: params.target_type,
173
+ target_id: params.target_id,
174
+ });
175
+ const res = await this.request<{ data: TaskRelation[] }>(
176
+ 'GET',
177
+ `${ENDPOINTS.TASK_RELATIONS_BY_TARGET(orgId)}?${query.toString()}`,
178
+ );
179
+ return res.data;
180
+ }
181
+
182
+ async deleteTaskRelation(orgId: string, taskId: string, relationId: string): Promise<void> {
183
+ return this.request('DELETE', ENDPOINTS.TASK_RELATION(orgId, taskId, relationId));
184
+ }
185
+
186
+ async getTaskComments(
187
+ orgId: string,
188
+ taskId: string,
189
+ params?: { limit?: number; cursor?: string; order?: string },
190
+ ): Promise<PaginatedResponse<TaskComment>> {
191
+ return this.request('GET', ENDPOINTS.TASK_COMMENTS(orgId, taskId), undefined, params);
192
+ }
193
+
194
+ async getTaskComment(orgId: string, taskId: string, commentId: string): Promise<TaskComment> {
195
+ return this.request('GET', ENDPOINTS.TASK_COMMENT(orgId, taskId, commentId));
196
+ }
197
+
198
+ async createTaskComment(
199
+ orgId: string,
200
+ taskId: string,
201
+ data: CreateTaskCommentRequest,
202
+ ): Promise<TaskComment> {
203
+ return this.request('POST', ENDPOINTS.TASK_COMMENTS(orgId, taskId), data);
204
+ }
205
+
206
+ async updateTaskComment(
207
+ orgId: string,
208
+ taskId: string,
209
+ commentId: string,
210
+ data: UpdateTaskCommentRequest,
211
+ ): Promise<TaskComment> {
212
+ return this.request('PATCH', ENDPOINTS.TASK_COMMENT(orgId, taskId, commentId), data);
213
+ }
214
+
215
+ async deleteTaskComment(orgId: string, taskId: string, commentId: string): Promise<void> {
216
+ return this.request('DELETE', ENDPOINTS.TASK_COMMENT(orgId, taskId, commentId));
217
+ }
218
+
219
+ async getTaskActivities(
220
+ orgId: string,
221
+ taskId: string,
222
+ params?: { limit?: number; cursor?: string },
223
+ ): Promise<PaginatedResponse<TaskActivity>> {
224
+ return this.request('GET', ENDPOINTS.TASK_ACTIVITIES(orgId, taskId), undefined, params);
225
+ }
226
+
227
+ async createProject(orgId: string, data: CreateProjectRequest): Promise<Project> {
228
+ return this.request('POST', ENDPOINTS.PROJECTS(orgId), data);
229
+ }
230
+
231
+ async getProjects(orgId: string): Promise<Project[]> {
232
+ const res = await this.request<{ data: Project[] }>('GET', ENDPOINTS.PROJECTS(orgId));
233
+ return res.data;
234
+ }
235
+
236
+ async getProjectTaskSummary(orgId: string): Promise<ProjectTaskSummaryResponse> {
237
+ return this.request('GET', ENDPOINTS.PROJECT_TASK_SUMMARY(orgId));
238
+ }
239
+
240
+ async getProject(orgId: string, projectId: string): Promise<Project> {
241
+ return this.request('GET', ENDPOINTS.PROJECT(orgId, projectId));
242
+ }
243
+
244
+ async updateProject(
245
+ orgId: string,
246
+ projectId: string,
247
+ data: UpdateProjectRequest,
248
+ ): Promise<Project> {
249
+ return this.request('PATCH', ENDPOINTS.PROJECT(orgId, projectId), data);
250
+ }
251
+
252
+ async deleteProject(orgId: string, projectId: string): Promise<void> {
253
+ return this.request('DELETE', ENDPOINTS.PROJECT(orgId, projectId));
254
+ }
255
+
256
+ /** Roster with display info; readable by anyone who can read the project. */
257
+ async getProjectMembers(orgId: string, projectId: string): Promise<ProjectMember[]> {
258
+ const res = await this.request<{ data: ProjectMember[] }>(
259
+ 'GET',
260
+ ENDPOINTS.PROJECT_MEMBERS(orgId, projectId),
261
+ );
262
+ return res.data;
263
+ }
264
+
265
+ /**
266
+ * User IDs who may read the project — the assignee-eligibility set. Every
267
+ * tier answers the roster expansion (direct, team-reached, org
268
+ * owners/admins); visibility only shapes admission, never reach.
269
+ */
270
+ async getProjectReaders(orgId: string, projectId: string): Promise<string[]> {
271
+ const res = await this.request<{ data: string[] }>(
272
+ 'GET',
273
+ ENDPOINTS.PROJECT_READERS(orgId, projectId),
274
+ );
275
+ return res.data;
276
+ }
277
+
278
+ /** The join library: every discoverable project (public + restricted, plus
279
+ * private for org owners/admins) with the caller's admission state. */
280
+ async getProjectLibrary(orgId: string): Promise<ProjectLibraryEntry[]> {
281
+ const res = await this.request<{ data: ProjectLibraryEntry[] }>(
282
+ 'GET',
283
+ ENDPOINTS.PROJECT_LIBRARY(orgId),
284
+ );
285
+ return res.data;
286
+ }
287
+
288
+ /** Public-tier self-admission: writes the caller's own member row.
289
+ * Other tiers answer `404 PROJECT_NOT_FOUND`; a duplicate answers
290
+ * `409 MEMBER_EXISTS`. */
291
+ async joinProject(orgId: string, projectId: string): Promise<ProjectMember> {
292
+ return this.request('POST', ENDPOINTS.PROJECT_JOIN(orgId, projectId));
293
+ }
294
+
295
+ /** Restricted-tier admission petition. A pending duplicate answers
296
+ * `409 REQUEST_EXISTS`; membership answers `409 MEMBER_EXISTS`. */
297
+ async createProjectJoinRequest(orgId: string, projectId: string): Promise<Approval> {
298
+ return this.request('POST', ENDPOINTS.PROJECT_JOIN_REQUESTS(orgId, projectId));
299
+ }
300
+
301
+ /** Pending petitions for one project — manager standing required. */
302
+ async listProjectJoinRequests(orgId: string, projectId: string): Promise<ProjectJoinRequest[]> {
303
+ const res = await this.request<{ data: ProjectJoinRequest[] }>(
304
+ 'GET',
305
+ ENDPOINTS.PROJECT_JOIN_REQUESTS(orgId, projectId),
306
+ );
307
+ return res.data;
308
+ }
309
+
310
+ /** Withdraws the caller's own pending petition. */
311
+ async cancelProjectJoinRequest(orgId: string, projectId: string): Promise<void> {
312
+ return this.request('DELETE', ENDPOINTS.PROJECT_JOIN_REQUESTS(orgId, projectId));
313
+ }
314
+
315
+ /**
316
+ * Adds one subject (user ID or team ID) to the roster. Manager-only,
317
+ * create-only: an existing row answers `409 MEMBER_EXISTS` — change roles
318
+ * through updateProjectMember instead.
319
+ */
320
+ async addProjectMember(
321
+ orgId: string,
322
+ projectId: string,
323
+ req: AddProjectMemberRequest,
324
+ ): Promise<ProjectMember> {
325
+ return this.request('POST', ENDPOINTS.PROJECT_MEMBERS(orgId, projectId), req);
326
+ }
327
+
328
+ /** Sets one roster entry's role. Demoting the last manager answers `400 LAST_MANAGER`. */
329
+ async updateProjectMember(
330
+ orgId: string,
331
+ projectId: string,
332
+ subject: string,
333
+ req: UpdateProjectMemberRequest,
334
+ ): Promise<ProjectMember> {
335
+ return this.request('PATCH', ENDPOINTS.PROJECT_MEMBER(orgId, projectId, subject), req);
336
+ }
337
+
338
+ /** Removes one roster entry. Removing the last manager answers `400 LAST_MANAGER`. */
339
+ async removeProjectMember(orgId: string, projectId: string, subject: string): Promise<void> {
340
+ return this.request('DELETE', ENDPOINTS.PROJECT_MEMBER(orgId, projectId, subject));
341
+ }
342
+ }
package/src/subject.ts ADDED
@@ -0,0 +1,57 @@
1
+ /** Shared ACL subject token helpers for clients that edit permission rosters. */
2
+
3
+ export type SubjectKind = 'wildcard' | 'user' | 'team';
4
+
5
+ export interface ParsedSubject {
6
+ kind: SubjectKind;
7
+ /** Empty for wildcard, user ID for user, and the full team ID for team. */
8
+ value: string;
9
+ }
10
+
11
+ /**
12
+ * Team subjects are stored as bare team IDs (`team_...`), mirroring the
13
+ * server's `pkg/subject`: the ID prefix is what makes a token a team
14
+ * reference, so no marker syntax exists and a rename never invalidates a
15
+ * stored reference.
16
+ */
17
+ const TEAM_ID_PREFIX = 'team_';
18
+
19
+ export function isTeamSubject(token: string): boolean {
20
+ return token.startsWith(TEAM_ID_PREFIX);
21
+ }
22
+
23
+ export function parseSubjectToken(token: string): ParsedSubject {
24
+ if (token === '*') {
25
+ return { kind: 'wildcard', value: '' };
26
+ }
27
+ if (!token) {
28
+ throw new Error('Subject token is empty');
29
+ }
30
+ if (isTeamSubject(token)) {
31
+ return { kind: 'team', value: token };
32
+ }
33
+ return { kind: 'user', value: token };
34
+ }
35
+
36
+ /**
37
+ * Maximum distinct team references one ACL row may carry. Mirrors
38
+ * `subject.MaxTeamSubjectsPerWrite` on the server, which enforces it.
39
+ */
40
+ export const MAX_TEAM_SUBJECTS_PER_WRITE = 200;
41
+
42
+ /**
43
+ * Returns unique team IDs in first-seen order for batched write validation.
44
+ * An ID that names no team comes back from the server's existence check
45
+ * reported missing, so the write fails with a legible error instead of
46
+ * storing a token that matches nobody.
47
+ */
48
+ export function teamIdsFromSubjects(subjects: readonly string[]): string[] {
49
+ const seen = new Set<string>();
50
+ const ids: string[] = [];
51
+ for (const token of subjects) {
52
+ if (!isTeamSubject(token) || seen.has(token)) continue;
53
+ seen.add(token);
54
+ ids.push(token);
55
+ }
56
+ return ids;
57
+ }
@@ -0,0 +1,37 @@
1
+ import { ENDPOINTS } from './constants.js';
2
+ import { ProjectTaskClient } from './project-task-client.js';
3
+ import type {
4
+ AddTaskLabelsRequest,
5
+ CreateLabelRequest,
6
+ Label,
7
+ UpdateLabelRequest,
8
+ } from './task-label-types.js';
9
+ import type { Task } from './types.js';
10
+
11
+ /** Organization label vocabulary and incremental Task attachment methods. */
12
+ export abstract class TaskLabelClient extends ProjectTaskClient {
13
+ async getLabels(orgId: string): Promise<Label[]> {
14
+ const response = await this.request<{ data: Label[] }>('GET', ENDPOINTS.LABELS(orgId));
15
+ return response.data;
16
+ }
17
+
18
+ async createLabel(orgId: string, data: CreateLabelRequest): Promise<Label> {
19
+ return this.request('POST', ENDPOINTS.LABELS(orgId), data);
20
+ }
21
+
22
+ async updateLabel(orgId: string, labelId: string, data: UpdateLabelRequest): Promise<Label> {
23
+ return this.request('PATCH', ENDPOINTS.LABEL(orgId, labelId), data);
24
+ }
25
+
26
+ async deleteLabel(orgId: string, labelId: string): Promise<void> {
27
+ return this.request('DELETE', ENDPOINTS.LABEL(orgId, labelId));
28
+ }
29
+
30
+ async addTaskLabels(orgId: string, taskId: string, data: AddTaskLabelsRequest): Promise<Task> {
31
+ return this.request('POST', ENDPOINTS.TASK_LABELS(orgId, taskId), data);
32
+ }
33
+
34
+ async removeTaskLabel(orgId: string, taskId: string, labelId: string): Promise<Task> {
35
+ return this.request('DELETE', ENDPOINTS.TASK_LABEL(orgId, taskId, labelId));
36
+ }
37
+ }
@@ -0,0 +1,51 @@
1
+ export type LabelColor =
2
+ | 'default'
3
+ | 'red'
4
+ | 'green'
5
+ | 'yellow'
6
+ | 'blue'
7
+ | 'lilac'
8
+ | 'orange'
9
+ | 'magenta'
10
+ | 'sand'
11
+ | 'fuchsia'
12
+ | 'cerulean'
13
+ | 'teal'
14
+ | 'lime';
15
+
16
+ export interface Label {
17
+ id: string;
18
+ org_id: string;
19
+ name: string;
20
+ color: LabelColor;
21
+ /** Optional agent-readable guidance for when this label applies. */
22
+ description?: string | null;
23
+ created_by: string;
24
+ created_at: string;
25
+ updated_at: string;
26
+ }
27
+
28
+ export interface CreateLabelRequest {
29
+ name: string;
30
+ color?: LabelColor;
31
+ description?: string;
32
+ }
33
+
34
+ export interface UpdateLabelRequest {
35
+ name?: string;
36
+ color?: LabelColor;
37
+ /** Explicit null clears the description. */
38
+ description?: string | null;
39
+ }
40
+
41
+ export interface AddTaskLabelsRequest {
42
+ label_ids: string[];
43
+ }
44
+
45
+ export type LabelCreatedData = Label;
46
+ export type LabelUpdatedData = Label;
47
+
48
+ export interface LabelDeletedData {
49
+ label_id: string;
50
+ org_id: string;
51
+ }