@doist/comms-sdk 0.7.1 → 0.9.0

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.
@@ -3,38 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.addCommentRequest = addCommentRequest;
4
4
  const endpoints_1 = require("../consts/endpoints");
5
5
  const http_client_1 = require("../transport/http-client");
6
- const enums_1 = require("../types/enums");
7
6
  const uuidv7_1 = require("../utils/uuidv7");
8
- const SENTINEL_GROUP_IDS = new Set(Object.values(enums_1.NOTIFY_AUDIENCE_GROUP_IDS));
9
- function isNotifyAudience(value) {
10
- return typeof value === 'string' && enums_1.NOTIFY_AUDIENCES.includes(value);
11
- }
12
- function collectMarkerOffenses(field, values) {
13
- if (!values)
14
- return null;
15
- const offending = values.filter((id) => SENTINEL_GROUP_IDS.has(id));
16
- return offending.length > 0 ? { field, offending } : null;
17
- }
18
- function applyNotifyAudience(params) {
19
- const offenses = [
20
- collectMarkerOffenses('groups', params.groups ?? undefined),
21
- collectMarkerOffenses('directGroupMentions', params.directGroupMentions ?? undefined),
22
- ].filter((o) => o !== null);
23
- if (offenses.length > 0) {
24
- const details = offenses
25
- .map(({ field, offending }) => `\`${field}\` contains ${offending.join(', ')}`)
26
- .join('; ');
27
- throw new Error(`Reserved broadcast marker IDs found: ${details}. Pass these via \`notifyAudience\` on createComment / closeThread / reopenThread (e.g. \`notifyAudience: 'channel'\` for EVERYONE) instead of populating \`groups\` / \`directGroupMentions\` directly.`);
28
- }
29
- if (params.notifyAudience == null)
30
- return params;
31
- if (!isNotifyAudience(params.notifyAudience)) {
32
- throw new Error(`Invalid \`notifyAudience\` value "${String(params.notifyAudience)}". Expected one of: ${enums_1.NOTIFY_AUDIENCES.join(', ')}.`);
33
- }
34
- const sentinel = enums_1.NOTIFY_AUDIENCE_GROUP_IDS[params.notifyAudience];
35
- const { notifyAudience: _stripped, groups, ...rest } = params;
36
- return { ...rest, groups: [...(groups ?? []), sentinel] };
37
- }
7
+ const notify_audience_1 = require("./notify-audience");
38
8
  /**
39
9
  * Internal helper that powers `comments.createComment`,
40
10
  * `threads.closeThread`, and `threads.reopenThread`.
@@ -53,7 +23,7 @@ function applyNotifyAudience(params) {
53
23
  * @returns The parsed {@link Comment} returned by the API.
54
24
  */
55
25
  function addCommentRequest(context, params, options) {
56
- const normalized = applyNotifyAudience(params);
26
+ const normalized = (0, notify_audience_1.applyNotifyAudience)(params);
57
27
  const withId = { ...normalized, id: (0, uuidv7_1.resolveCreateId)(normalized.id) };
58
28
  const payload = options?.threadAction
59
29
  ? { ...withId, threadAction: options.threadAction }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyNotifyAudience = applyNotifyAudience;
4
+ const enums_1 = require("../types/enums");
5
+ const SENTINEL_GROUP_IDS = new Set(Object.values(enums_1.NOTIFY_AUDIENCE_GROUP_IDS));
6
+ function isNotifyAudience(value) {
7
+ return typeof value === 'string' && enums_1.NOTIFY_AUDIENCES.includes(value);
8
+ }
9
+ function collectMarkerOffenses(field, values) {
10
+ if (!values)
11
+ return null;
12
+ const offending = values.filter((id) => SENTINEL_GROUP_IDS.has(id));
13
+ return offending.length > 0 ? { field, offending } : null;
14
+ }
15
+ /**
16
+ * Normalizes a `notifyAudience` flag into a sentinel `groups` entry
17
+ * (`EVERYONE` / `EVERYONE_IN_THREAD`) and rejects callers who put those
18
+ * reserved markers directly into `groups` / `directGroupMentions`.
19
+ *
20
+ * When `notifyAudience` is set it is stripped and its sentinel is appended to
21
+ * `groups`; the backend only understands the marker in `groups`. When it is
22
+ * unset the params are returned unchanged, preserving any explicit
23
+ * `groups` / `directGroupMentions` (including a `null`) on the wire.
24
+ */
25
+ function applyNotifyAudience(params) {
26
+ const offenses = [
27
+ collectMarkerOffenses('groups', params.groups ?? undefined),
28
+ collectMarkerOffenses('directGroupMentions', params.directGroupMentions ?? undefined),
29
+ ].filter((o) => o !== null);
30
+ if (offenses.length > 0) {
31
+ const details = offenses
32
+ .map(({ field, offending }) => `\`${field}\` contains ${offending.join(', ')}`)
33
+ .join('; ');
34
+ throw new Error(`Reserved broadcast marker IDs found: ${details}. Pass these via \`notifyAudience\` (e.g. \`notifyAudience: 'channel'\` for EVERYONE) instead of populating \`groups\` / \`directGroupMentions\` directly.`);
35
+ }
36
+ if (params.notifyAudience == null) {
37
+ return params;
38
+ }
39
+ if (!isNotifyAudience(params.notifyAudience)) {
40
+ throw new Error(`Invalid \`notifyAudience\` value "${String(params.notifyAudience)}". Expected one of: ${enums_1.NOTIFY_AUDIENCES.join(', ')}.`);
41
+ }
42
+ const sentinel = enums_1.NOTIFY_AUDIENCE_GROUP_IDS[params.notifyAudience];
43
+ const { notifyAudience: _stripped, groups, ...rest } = params;
44
+ return { ...rest, groups: [...(groups ?? []), sentinel] };
45
+ }
@@ -8,6 +8,7 @@ const entities_1 = require("../types/entities");
8
8
  const uuidv7_1 = require("../utils/uuidv7");
9
9
  const add_comment_helper_1 = require("./add-comment-helper");
10
10
  const base_client_1 = require("./base-client");
11
+ const notify_audience_1 = require("./notify-audience");
11
12
  exports.ThreadListSchema = zod_1.z.array(entities_1.ThreadSchema);
12
13
  const GetUnreadResponseSchema = zod_1.z.object({
13
14
  data: zod_1.z.array(entities_1.UnreadThreadSchema),
@@ -95,6 +96,11 @@ class ThreadsClient extends base_client_1.BaseClient {
95
96
  * @param args.content - The thread content.
96
97
  * @param args.recipients - Optional array of user IDs to notify.
97
98
  * @param args.groups - Optional array of custom group IDs to notify.
99
+ * @param args.notifyAudience - Optional broader audience to notify in addition to
100
+ * `recipients` and `groups`. `'channel'` notifies everyone in the channel;
101
+ * `'thread'` notifies everyone who has interacted with the thread — but a
102
+ * brand-new thread has no interactions yet, so the backend discards
103
+ * `'thread'` at create time. It is only meaningful on replies (`createComment`).
98
104
  * @param args.attachments - Optional array of {@link Attachment}s (from `attachments.upload`).
99
105
  * @returns The created thread object.
100
106
  *
@@ -104,11 +110,13 @@ class ThreadsClient extends base_client_1.BaseClient {
104
110
  * channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44',
105
111
  * title: 'New Feature Discussion',
106
112
  * content: 'Let\'s discuss the new feature...',
113
+ * notifyAudience: 'channel', // tag "Everyone in channel"
107
114
  * })
108
115
  * ```
109
116
  */
110
117
  createThread(args) {
111
- return this.simple('POST', 'add', { ...args, id: (0, uuidv7_1.resolveCreateId)(args.id) }, this.threadSchema);
118
+ const normalized = (0, notify_audience_1.applyNotifyAudience)(args);
119
+ return this.simple('POST', 'add', { ...normalized, id: (0, uuidv7_1.resolveCreateId)(args.id) }, this.threadSchema);
112
120
  }
113
121
  /**
114
122
  * Partial update of an existing thread.
@@ -420,16 +420,29 @@ exports.UnreadConversationSchema = zod_1.z.object({
420
420
  directMention: zod_1.z.boolean(),
421
421
  });
422
422
  // SearchResult entity from API.
423
+ // The API currently only emits 'thread' and 'conversation' results: a match inside a
424
+ // comment is returned as a 'thread' result with `commentId` set, and a match inside a
425
+ // message as a 'conversation' result with `messageId` set.
423
426
  exports.SEARCH_RESULT_TYPES = ['thread', 'comment', 'message', 'conversation'];
424
427
  exports.SearchResultSchema = zod_1.z.object({
428
+ /**
429
+ * Opaque search-result key in the form `thread_<threadId>` or
430
+ * `conversation_<conversationId>`. Unique per result; not a Comms object id — use
431
+ * `threadId`, `conversationId`, `commentId`, or `messageId` to reference objects.
432
+ */
425
433
  id: zod_1.z.string(),
426
434
  type: zod_1.z.enum(exports.SEARCH_RESULT_TYPES),
427
435
  snippet: zod_1.z.string(),
428
436
  snippetCreatorId: zod_1.z.number(),
429
437
  snippetLastUpdated: zod_1.z.date(),
438
+ /** The owning thread id. Set by the API on 'thread' results. */
430
439
  threadId: zod_1.z.string().nullable().optional(),
440
+ /** The owning conversation id. Set by the API on 'conversation' results. */
431
441
  conversationId: zod_1.z.string().nullable().optional(),
442
+ /** The matched comment id, set on 'thread' results when the match is a comment. */
432
443
  commentId: zod_1.z.string().nullable().optional(),
444
+ /** The matched message id, set by the API on 'conversation' results. */
445
+ messageId: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).transform(String).nullable().optional(),
433
446
  channelId: zod_1.z.string().nullable().optional(),
434
447
  channelName: zod_1.z.string().nullable().optional(),
435
448
  channelColor: zod_1.z.number().nullable().optional(),
@@ -42,6 +42,7 @@ exports.CreateThreadArgsSchema = zod_1.z.object({
42
42
  id: zod_1.z.string().optional(),
43
43
  recipients: zod_1.z.array(zod_1.z.number()).nullable().optional(),
44
44
  groups: zod_1.z.array(zod_1.z.string()).nullable().optional(),
45
+ notifyAudience: zod_1.z.enum(enums_1.NOTIFY_AUDIENCES).nullable().optional(),
45
46
  attachments: zod_1.z.array(entities_1.RequestAttachmentSchema).nullable().optional(),
46
47
  });
47
48
  exports.UpdateThreadArgsSchema = zod_1.z.object({
@@ -1,37 +1,7 @@
1
1
  import { ENDPOINT_COMMENTS } from '../consts/endpoints.js';
2
2
  import { request } from '../transport/http-client.js';
3
- import { NOTIFY_AUDIENCE_GROUP_IDS, NOTIFY_AUDIENCES } from '../types/enums.js';
4
3
  import { resolveCreateId } from '../utils/uuidv7.js';
5
- const SENTINEL_GROUP_IDS = new Set(Object.values(NOTIFY_AUDIENCE_GROUP_IDS));
6
- function isNotifyAudience(value) {
7
- return typeof value === 'string' && NOTIFY_AUDIENCES.includes(value);
8
- }
9
- function collectMarkerOffenses(field, values) {
10
- if (!values)
11
- return null;
12
- const offending = values.filter((id) => SENTINEL_GROUP_IDS.has(id));
13
- return offending.length > 0 ? { field, offending } : null;
14
- }
15
- function applyNotifyAudience(params) {
16
- const offenses = [
17
- collectMarkerOffenses('groups', params.groups ?? undefined),
18
- collectMarkerOffenses('directGroupMentions', params.directGroupMentions ?? undefined),
19
- ].filter((o) => o !== null);
20
- if (offenses.length > 0) {
21
- const details = offenses
22
- .map(({ field, offending }) => `\`${field}\` contains ${offending.join(', ')}`)
23
- .join('; ');
24
- throw new Error(`Reserved broadcast marker IDs found: ${details}. Pass these via \`notifyAudience\` on createComment / closeThread / reopenThread (e.g. \`notifyAudience: 'channel'\` for EVERYONE) instead of populating \`groups\` / \`directGroupMentions\` directly.`);
25
- }
26
- if (params.notifyAudience == null)
27
- return params;
28
- if (!isNotifyAudience(params.notifyAudience)) {
29
- throw new Error(`Invalid \`notifyAudience\` value "${String(params.notifyAudience)}". Expected one of: ${NOTIFY_AUDIENCES.join(', ')}.`);
30
- }
31
- const sentinel = NOTIFY_AUDIENCE_GROUP_IDS[params.notifyAudience];
32
- const { notifyAudience: _stripped, groups, ...rest } = params;
33
- return { ...rest, groups: [...(groups ?? []), sentinel] };
34
- }
4
+ import { applyNotifyAudience } from './notify-audience.js';
35
5
  /**
36
6
  * Internal helper that powers `comments.createComment`,
37
7
  * `threads.closeThread`, and `threads.reopenThread`.
@@ -0,0 +1,42 @@
1
+ import { NOTIFY_AUDIENCE_GROUP_IDS, NOTIFY_AUDIENCES } from '../types/enums.js';
2
+ const SENTINEL_GROUP_IDS = new Set(Object.values(NOTIFY_AUDIENCE_GROUP_IDS));
3
+ function isNotifyAudience(value) {
4
+ return typeof value === 'string' && NOTIFY_AUDIENCES.includes(value);
5
+ }
6
+ function collectMarkerOffenses(field, values) {
7
+ if (!values)
8
+ return null;
9
+ const offending = values.filter((id) => SENTINEL_GROUP_IDS.has(id));
10
+ return offending.length > 0 ? { field, offending } : null;
11
+ }
12
+ /**
13
+ * Normalizes a `notifyAudience` flag into a sentinel `groups` entry
14
+ * (`EVERYONE` / `EVERYONE_IN_THREAD`) and rejects callers who put those
15
+ * reserved markers directly into `groups` / `directGroupMentions`.
16
+ *
17
+ * When `notifyAudience` is set it is stripped and its sentinel is appended to
18
+ * `groups`; the backend only understands the marker in `groups`. When it is
19
+ * unset the params are returned unchanged, preserving any explicit
20
+ * `groups` / `directGroupMentions` (including a `null`) on the wire.
21
+ */
22
+ export function applyNotifyAudience(params) {
23
+ const offenses = [
24
+ collectMarkerOffenses('groups', params.groups ?? undefined),
25
+ collectMarkerOffenses('directGroupMentions', params.directGroupMentions ?? undefined),
26
+ ].filter((o) => o !== null);
27
+ if (offenses.length > 0) {
28
+ const details = offenses
29
+ .map(({ field, offending }) => `\`${field}\` contains ${offending.join(', ')}`)
30
+ .join('; ');
31
+ throw new Error(`Reserved broadcast marker IDs found: ${details}. Pass these via \`notifyAudience\` (e.g. \`notifyAudience: 'channel'\` for EVERYONE) instead of populating \`groups\` / \`directGroupMentions\` directly.`);
32
+ }
33
+ if (params.notifyAudience == null) {
34
+ return params;
35
+ }
36
+ if (!isNotifyAudience(params.notifyAudience)) {
37
+ throw new Error(`Invalid \`notifyAudience\` value "${String(params.notifyAudience)}". Expected one of: ${NOTIFY_AUDIENCES.join(', ')}.`);
38
+ }
39
+ const sentinel = NOTIFY_AUDIENCE_GROUP_IDS[params.notifyAudience];
40
+ const { notifyAudience: _stripped, groups, ...rest } = params;
41
+ return { ...rest, groups: [...(groups ?? []), sentinel] };
42
+ }
@@ -5,6 +5,7 @@ import { CommentSchema, createCommentSchema, createThreadSchema, StatusOkSchema,
5
5
  import { resolveCreateId } from '../utils/uuidv7.js';
6
6
  import { addCommentRequest } from './add-comment-helper.js';
7
7
  import { BaseClient } from './base-client.js';
8
+ import { applyNotifyAudience } from './notify-audience.js';
8
9
  export const ThreadListSchema = z.array(ThreadSchema);
9
10
  const GetUnreadResponseSchema = z.object({
10
11
  data: z.array(UnreadThreadSchema),
@@ -92,6 +93,11 @@ export class ThreadsClient extends BaseClient {
92
93
  * @param args.content - The thread content.
93
94
  * @param args.recipients - Optional array of user IDs to notify.
94
95
  * @param args.groups - Optional array of custom group IDs to notify.
96
+ * @param args.notifyAudience - Optional broader audience to notify in addition to
97
+ * `recipients` and `groups`. `'channel'` notifies everyone in the channel;
98
+ * `'thread'` notifies everyone who has interacted with the thread — but a
99
+ * brand-new thread has no interactions yet, so the backend discards
100
+ * `'thread'` at create time. It is only meaningful on replies (`createComment`).
95
101
  * @param args.attachments - Optional array of {@link Attachment}s (from `attachments.upload`).
96
102
  * @returns The created thread object.
97
103
  *
@@ -101,11 +107,13 @@ export class ThreadsClient extends BaseClient {
101
107
  * channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44',
102
108
  * title: 'New Feature Discussion',
103
109
  * content: 'Let\'s discuss the new feature...',
110
+ * notifyAudience: 'channel', // tag "Everyone in channel"
104
111
  * })
105
112
  * ```
106
113
  */
107
114
  createThread(args) {
108
- return this.simple('POST', 'add', { ...args, id: resolveCreateId(args.id) }, this.threadSchema);
115
+ const normalized = applyNotifyAudience(args);
116
+ return this.simple('POST', 'add', { ...normalized, id: resolveCreateId(args.id) }, this.threadSchema);
109
117
  }
110
118
  /**
111
119
  * Partial update of an existing thread.
@@ -410,16 +410,29 @@ export const UnreadConversationSchema = z.object({
410
410
  directMention: z.boolean(),
411
411
  });
412
412
  // SearchResult entity from API.
413
+ // The API currently only emits 'thread' and 'conversation' results: a match inside a
414
+ // comment is returned as a 'thread' result with `commentId` set, and a match inside a
415
+ // message as a 'conversation' result with `messageId` set.
413
416
  export const SEARCH_RESULT_TYPES = ['thread', 'comment', 'message', 'conversation'];
414
417
  export const SearchResultSchema = z.object({
418
+ /**
419
+ * Opaque search-result key in the form `thread_<threadId>` or
420
+ * `conversation_<conversationId>`. Unique per result; not a Comms object id — use
421
+ * `threadId`, `conversationId`, `commentId`, or `messageId` to reference objects.
422
+ */
415
423
  id: z.string(),
416
424
  type: z.enum(SEARCH_RESULT_TYPES),
417
425
  snippet: z.string(),
418
426
  snippetCreatorId: z.number(),
419
427
  snippetLastUpdated: z.date(),
428
+ /** The owning thread id. Set by the API on 'thread' results. */
420
429
  threadId: z.string().nullable().optional(),
430
+ /** The owning conversation id. Set by the API on 'conversation' results. */
421
431
  conversationId: z.string().nullable().optional(),
432
+ /** The matched comment id, set on 'thread' results when the match is a comment. */
422
433
  commentId: z.string().nullable().optional(),
434
+ /** The matched message id, set by the API on 'conversation' results. */
435
+ messageId: z.union([z.string(), z.number()]).transform(String).nullable().optional(),
423
436
  channelId: z.string().nullable().optional(),
424
437
  channelName: z.string().nullable().optional(),
425
438
  channelColor: z.number().nullable().optional(),
@@ -39,6 +39,7 @@ export const CreateThreadArgsSchema = z.object({
39
39
  id: z.string().optional(),
40
40
  recipients: z.array(z.number()).nullable().optional(),
41
41
  groups: z.array(z.string()).nullable().optional(),
42
+ notifyAudience: z.enum(NOTIFY_AUDIENCES).nullable().optional(),
42
43
  attachments: z.array(RequestAttachmentSchema).nullable().optional(),
43
44
  });
44
45
  export const UpdateThreadArgsSchema = z.object({
@@ -0,0 +1,22 @@
1
+ import { type NotifyAudience } from '../types/enums.js';
2
+ /**
3
+ * Params bearing the group-targeting fields that can carry a `notifyAudience`.
4
+ * Shared by the comment-creating endpoints and thread creation.
5
+ */
6
+ type NotifyAudienceParams = {
7
+ groups?: string[] | null;
8
+ directGroupMentions?: string[] | null;
9
+ notifyAudience?: NotifyAudience | null;
10
+ };
11
+ /**
12
+ * Normalizes a `notifyAudience` flag into a sentinel `groups` entry
13
+ * (`EVERYONE` / `EVERYONE_IN_THREAD`) and rejects callers who put those
14
+ * reserved markers directly into `groups` / `directGroupMentions`.
15
+ *
16
+ * When `notifyAudience` is set it is stripped and its sentinel is appended to
17
+ * `groups`; the backend only understands the marker in `groups`. When it is
18
+ * unset the params are returned unchanged, preserving any explicit
19
+ * `groups` / `directGroupMentions` (including a `null`) on the wire.
20
+ */
21
+ export declare function applyNotifyAudience<T extends NotifyAudienceParams>(params: T): Omit<T, 'notifyAudience'>;
22
+ export {};
@@ -336,6 +336,11 @@ export declare class ThreadsClient extends BaseClient {
336
336
  * @param args.content - The thread content.
337
337
  * @param args.recipients - Optional array of user IDs to notify.
338
338
  * @param args.groups - Optional array of custom group IDs to notify.
339
+ * @param args.notifyAudience - Optional broader audience to notify in addition to
340
+ * `recipients` and `groups`. `'channel'` notifies everyone in the channel;
341
+ * `'thread'` notifies everyone who has interacted with the thread — but a
342
+ * brand-new thread has no interactions yet, so the backend discards
343
+ * `'thread'` at create time. It is only meaningful on replies (`createComment`).
339
344
  * @param args.attachments - Optional array of {@link Attachment}s (from `attachments.upload`).
340
345
  * @returns The created thread object.
341
346
  *
@@ -345,6 +350,7 @@ export declare class ThreadsClient extends BaseClient {
345
350
  * channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44',
346
351
  * title: 'New Feature Discussion',
347
352
  * content: 'Let\'s discuss the new feature...',
353
+ * notifyAudience: 'channel', // tag "Everyone in channel"
348
354
  * })
349
355
  * ```
350
356
  */
@@ -2818,6 +2818,7 @@ export declare const SearchResultSchema: z.ZodObject<{
2818
2818
  threadId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2819
2819
  conversationId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2820
2820
  commentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2821
+ messageId: z.ZodOptional<z.ZodNullable<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>>>;
2821
2822
  channelId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2822
2823
  channelName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2823
2824
  channelColor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
@@ -40,6 +40,10 @@ export declare const CreateThreadArgsSchema: z.ZodObject<{
40
40
  id: z.ZodOptional<z.ZodString>;
41
41
  recipients: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodNumber>>>;
42
42
  groups: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
43
+ notifyAudience: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
44
+ channel: "channel";
45
+ thread: "thread";
46
+ }>>>;
43
47
  attachments: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
44
48
  attachmentId: z.ZodString;
45
49
  urlType: z.ZodString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/comms-sdk",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "A TypeScript wrapper for the Comms REST API.",
5
5
  "author": "Doist developers",
6
6
  "homepage": "https://doist.github.io/comms-sdk-typescript/",