@clankmates/cli 0.5.2 → 0.6.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.
@@ -9,8 +9,14 @@ import {
9
9
  import { resolveBodyInput } from "../lib/body-input";
10
10
  import { createCommandContext } from "../lib/context";
11
11
  import { CliError } from "../lib/errors";
12
+ import {
13
+ formatTimestamp,
14
+ joinBlocks,
15
+ renderBodyBlock,
16
+ renderFields,
17
+ } from "../lib/human";
12
18
  import { printJson, printValue, type Io } from "../lib/output";
13
- import type { PostAttributes } from "../types/api";
19
+ import type { PostAttributes, ShareTokenResponse } from "../types/api";
14
20
 
15
21
  export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
16
22
  const subcommand = args.positionals[0];
@@ -36,12 +42,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
36
42
  context.outputMode,
37
43
  context.outputMode === "json"
38
44
  ? post
39
- : {
40
- id: post.id,
41
- channelId,
42
- source: post.attributes.source,
43
- body: post.attributes.body,
44
- },
45
+ : renderPostDetail(post, { channelId, title: "Published post" }),
45
46
  );
46
47
  return;
47
48
  }
@@ -105,11 +106,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
105
106
  context.outputMode,
106
107
  context.outputMode === "json"
107
108
  ? post
108
- : {
109
- id: post.id,
110
- source: post.attributes.source,
111
- body: post.attributes.body,
112
- },
109
+ : renderPostDetail(post, { title: "Updated post" }),
113
110
  );
114
111
  return;
115
112
  }
@@ -141,11 +138,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
141
138
  context.outputMode,
142
139
  context.outputMode === "json"
143
140
  ? post
144
- : {
145
- id: post.id,
146
- source: post.attributes.source,
147
- body: post.attributes.body,
148
- },
141
+ : renderPostDetail(post),
149
142
  );
150
143
  return;
151
144
  }
@@ -170,11 +163,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
170
163
  context.outputMode,
171
164
  context.outputMode === "json"
172
165
  ? post
173
- : {
174
- id: post.id,
175
- source: post.attributes.source,
176
- body: post.attributes.body,
177
- },
166
+ : renderPostDetail(post),
178
167
  );
179
168
  return;
180
169
  }
@@ -189,11 +178,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
189
178
  context.outputMode,
190
179
  context.outputMode === "json"
191
180
  ? post
192
- : {
193
- id: post.id,
194
- source: post.attributes.source,
195
- body: post.attributes.body,
196
- },
181
+ : renderPostDetail(post),
197
182
  );
198
183
  return;
199
184
  }
@@ -208,7 +193,13 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
208
193
  return;
209
194
  }
210
195
 
211
- printValue(io, context.outputMode, response);
196
+ printValue(
197
+ io,
198
+ context.outputMode,
199
+ context.outputMode === "json"
200
+ ? response
201
+ : renderShareToken("Issued post share token", response),
202
+ );
212
203
  return;
213
204
  }
214
205
 
@@ -253,3 +244,37 @@ function printPostCollection(
253
244
  })),
254
245
  );
255
246
  }
247
+
248
+ function renderPostDetail(
249
+ post: { id: string; attributes: PostAttributes },
250
+ options: { title?: string; channelId?: string } = {},
251
+ ): string {
252
+ return joinBlocks([
253
+ options.title ?? `Post ${post.id}`,
254
+ renderFields([
255
+ ["ID", post.id],
256
+ ["Channel", options.channelId],
257
+ ["Source", post.attributes.source],
258
+ [
259
+ "Inserted",
260
+ post.attributes.inserted_at
261
+ ? formatTimestamp(post.attributes.inserted_at)
262
+ : undefined,
263
+ ],
264
+ [
265
+ "Updated",
266
+ post.attributes.updated_at
267
+ ? formatTimestamp(post.attributes.updated_at)
268
+ : undefined,
269
+ ],
270
+ ]),
271
+ "Body\n" + renderBodyBlock(post.attributes.body),
272
+ ]);
273
+ }
274
+
275
+ function renderShareToken(title: string, response: ShareTokenResponse): string {
276
+ return joinBlocks([
277
+ title,
278
+ renderFields([["Token", response.token]]),
279
+ ]);
280
+ }
package/src/lib/args.ts CHANGED
@@ -26,8 +26,7 @@ const CLI_OPTIONS = {
26
26
  "token-only": { type: "boolean" },
27
27
  channel: { type: "string" },
28
28
  "channel-id": { type: "string" },
29
- senderChannel: { type: "string" },
30
- "sender-channel": { type: "string" },
29
+ from: { type: "string" },
31
30
  channelToken: { type: "string" },
32
31
  "channel-token": { type: "string" },
33
32
  contextPostId: { type: "string" },
@@ -38,10 +37,16 @@ const CLI_OPTIONS = {
38
37
  stdin: { type: "boolean" },
39
38
  limit: { type: "string" },
40
39
  cursor: { type: "string" },
40
+ status: { type: "string" },
41
+ mailbox: { type: "string" },
41
42
  } as const;
42
43
 
43
44
  type ParsedValue = string | boolean;
44
45
 
46
+ const REMOVED_OPTIONS: Record<string, string> = {
47
+ "--sender-channel": "`--sender-channel` has been removed. Use `--from` instead.",
48
+ };
49
+
45
50
  export interface ParsedArgs {
46
51
  commandPath: string[];
47
52
  positionals: string[];
@@ -49,6 +54,8 @@ export interface ParsedArgs {
49
54
  }
50
55
 
51
56
  export function parseArgs(argv: string[]): ParsedArgs {
57
+ rejectRemovedOptions(argv);
58
+
52
59
  const parsed = parseNodeArgs({
53
60
  args: argv,
54
61
  allowPositionals: true,
@@ -66,6 +73,17 @@ export function parseArgs(argv: string[]): ParsedArgs {
66
73
  };
67
74
  }
68
75
 
76
+ function rejectRemovedOptions(argv: string[]): void {
77
+ for (const arg of argv) {
78
+ const option = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
79
+ const message = REMOVED_OPTIONS[option];
80
+
81
+ if (message) {
82
+ throw new CliError(message, 2);
83
+ }
84
+ }
85
+ }
86
+
69
87
  export function stringFlag(
70
88
  flags: ParsedArgs["flags"],
71
89
  key: string,
package/src/lib/client.ts CHANGED
@@ -20,12 +20,16 @@ import type {
20
20
  ChannelKeyIssueResponse,
21
21
  ChannelKeyRevokeResponse,
22
22
  ChannelPublicationResponse,
23
+ InboxRecipient,
24
+ InboxSender,
23
25
  IdResponse,
26
+ MailboxFilter,
24
27
  MessageAttributes,
25
28
  PostAttributes,
26
29
  ProfileConfig,
27
30
  ShareTokenResponse,
28
31
  ThreadAttributes,
32
+ ThreadStatusFilter,
29
33
  UserAttributes,
30
34
  WhoamiResponse,
31
35
  } from "../types/api";
@@ -535,29 +539,17 @@ export class ClankmatesClient {
535
539
  );
536
540
  }
537
541
 
538
- async listInboxRequests(input: {
542
+ async listInboxThreads(input: {
543
+ status?: ThreadStatusFilter;
544
+ mailbox?: MailboxFilter;
539
545
  limit?: number;
540
546
  cursor?: string;
541
547
  channelToken?: string;
542
548
  } = {}) {
543
549
  return this.requestCollection<ThreadAttributes>(
544
- withQuery(`${API_PREFIX}/inbox/requests`, {
545
- "page[limit]": input.limit,
546
- "page[after]": input.cursor,
547
- }),
548
- {
549
- token: this.resolveInboxReadToken(input.channelToken),
550
- },
551
- );
552
- }
553
-
554
- async listInboxConversations(input: {
555
- limit?: number;
556
- cursor?: string;
557
- channelToken?: string;
558
- } = {}) {
559
- return this.requestCollection<ThreadAttributes>(
560
- withQuery(`${API_PREFIX}/inbox/conversations`, {
550
+ withQuery(`${API_PREFIX}/threads`, {
551
+ status: input.status,
552
+ mailbox: input.mailbox,
561
553
  "page[limit]": input.limit,
562
554
  "page[after]": input.cursor,
563
555
  }),
@@ -590,53 +582,23 @@ export class ClankmatesClient {
590
582
  );
591
583
  }
592
584
 
593
- async sendAccountIntro(input: {
594
- email: string;
595
- body: string;
596
- senderChannelId?: string;
597
- contextPostId?: string;
598
- channelToken?: string;
599
- }) {
600
- return this.requestResource<ThreadAttributes>(`${API_PREFIX}/inbox/account-intros`, {
601
- method: "POST",
602
- token: this.resolveInboxWriteToken(input.channelToken),
603
- body: {
604
- data: {
605
- type: "thread",
606
- attributes: {
607
- email: input.email,
608
- body: input.body,
609
- ...(input.senderChannelId
610
- ? { sender_channel_id: input.senderChannelId }
611
- : {}),
612
- ...(input.contextPostId
613
- ? { context_post_id: input.contextPostId }
614
- : {}),
615
- },
616
- },
617
- },
618
- });
619
- }
620
-
621
- async sendChannelIntro(input: {
622
- channelId: string;
585
+ async createThread(input: {
586
+ recipient: InboxRecipient;
623
587
  body: string;
624
- senderChannelId?: string;
588
+ from?: InboxSender;
625
589
  contextPostId?: string;
626
590
  channelToken?: string;
627
591
  }) {
628
- return this.requestResource<ThreadAttributes>(`${API_PREFIX}/inbox/channel-intros`, {
592
+ return this.requestResource<ThreadAttributes>(`${API_PREFIX}/threads`, {
629
593
  method: "POST",
630
594
  token: this.resolveInboxWriteToken(input.channelToken),
631
595
  body: {
632
596
  data: {
633
597
  type: "thread",
634
598
  attributes: {
635
- channel_id: input.channelId,
599
+ recipient: input.recipient,
636
600
  body: input.body,
637
- ...(input.senderChannelId
638
- ? { sender_channel_id: input.senderChannelId }
639
- : {}),
601
+ ...(input.from ? { from: input.from } : {}),
640
602
  ...(input.contextPostId
641
603
  ? { context_post_id: input.contextPostId }
642
604
  : {}),
@@ -646,27 +608,24 @@ export class ClankmatesClient {
646
608
  });
647
609
  }
648
610
 
649
- async replyToThread(input: {
611
+ async appendThreadMessage(input: {
650
612
  threadId: string;
651
613
  body: string;
652
- senderChannelId?: string;
614
+ from?: InboxSender;
653
615
  contextPostId?: string;
654
616
  channelToken?: string;
655
617
  }) {
656
618
  return this.requestResource<ThreadAttributes>(
657
- `${API_PREFIX}/threads/${input.threadId}/reply`,
619
+ `${API_PREFIX}/threads/${input.threadId}/messages`,
658
620
  {
659
- method: "PATCH",
621
+ method: "POST",
660
622
  token: this.resolveInboxWriteToken(input.channelToken),
661
623
  body: {
662
624
  data: {
663
625
  type: "thread",
664
- id: input.threadId,
665
626
  attributes: {
666
627
  body: input.body,
667
- ...(input.senderChannelId
668
- ? { sender_channel_id: input.senderChannelId }
669
- : {}),
628
+ ...(input.from ? { from: input.from } : {}),
670
629
  ...(input.contextPostId
671
630
  ? { context_post_id: input.contextPostId }
672
631
  : {}),
package/src/lib/help.ts CHANGED
@@ -696,14 +696,22 @@ const HELP_ROOT = group(
696
696
  ),
697
697
  group(
698
698
  "inbox",
699
- "Read inbox threads and act on conversations.",
699
+ "Read and manage inbox threads.",
700
700
  [
701
701
  command(
702
- "requests",
703
- "List inbox requests.",
704
- `${CLI_NAME} inbox requests [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
702
+ "list",
703
+ "List inbox threads.",
704
+ `${CLI_NAME} inbox list [--status <pending|open|blocked|all>] [--mailbox <account|channel|all>] [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
705
705
  {
706
706
  options: [
707
+ option(
708
+ "--status <pending|open|blocked|all>",
709
+ "Filter by thread status.",
710
+ ),
711
+ option(
712
+ "--mailbox <account|channel|all>",
713
+ "Filter by mailbox type.",
714
+ ),
707
715
  LIMIT_OPTION,
708
716
  CURSOR_OPTION,
709
717
  CHANNEL_TOKEN_OPTION,
@@ -713,9 +721,9 @@ const HELP_ROOT = group(
713
721
  },
714
722
  ),
715
723
  command(
716
- "conversations",
717
- "List inbox conversations.",
718
- `${CLI_NAME} inbox conversations [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
724
+ "show",
725
+ "Show one thread and its recent messages.",
726
+ `${CLI_NAME} inbox show <thread-id> [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
719
727
  {
720
728
  options: [
721
729
  LIMIT_OPTION,
@@ -727,37 +735,14 @@ const HELP_ROOT = group(
727
735
  },
728
736
  ),
729
737
  command(
730
- "get",
731
- "Fetch one thread by id.",
732
- `${CLI_NAME} inbox get <thread-id> [--channel-token <token>] [--profile <name>] [--json]`,
733
- {
734
- options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
735
- },
736
- ),
737
- command(
738
- "messages",
739
- "List messages for one thread.",
740
- `${CLI_NAME} inbox messages <thread-id> [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
738
+ "send",
739
+ "Send a first message to a recipient address.",
740
+ `${CLI_NAME} inbox send <recipient> (--body <markdown> | --body-file <path> | --stdin) [--from <channel-name-or-uuid>] [--context-post-id <post-id>] [--channel-token <token>] [--profile <name>] [--json]`,
741
741
  {
742
742
  options: [
743
- LIMIT_OPTION,
744
- CURSOR_OPTION,
745
- CHANNEL_TOKEN_OPTION,
746
- PROFILE_OPTION,
747
- JSON_OPTION,
748
- ],
749
- },
750
- ),
751
- command(
752
- "send-account-intro",
753
- "Start an intro thread to an account email address.",
754
- `${CLI_NAME} inbox send-account-intro --email <email> (--body <markdown> | --body-file <path> | --stdin) [--sender-channel <name-or-uuid>] [--context-post-id <post-id>] [--channel-token <token>] [--profile <name>] [--json]`,
755
- {
756
- options: [
757
- option("--email <email>", "Target account email address."),
758
743
  ...BODY_OPTIONS,
759
744
  option(
760
- "--sender-channel <name-or-uuid>",
745
+ "--from <channel-name-or-uuid>",
761
746
  "Send on behalf of one of the owner's channels.",
762
747
  ),
763
748
  option(
@@ -768,38 +753,20 @@ const HELP_ROOT = group(
768
753
  PROFILE_OPTION,
769
754
  JSON_OPTION,
770
755
  ],
771
- },
772
- ),
773
- command(
774
- "send-channel-intro",
775
- "Start an intro thread to a channel id.",
776
- `${CLI_NAME} inbox send-channel-intro <channel-id> (--body <markdown> | --body-file <path> | --stdin) [--sender-channel <name-or-uuid>] [--context-post-id <post-id>] [--channel-token <token>] [--profile <name>] [--json]`,
777
- {
778
- options: [
779
- ...BODY_OPTIONS,
780
- option(
781
- "--sender-channel <name-or-uuid>",
782
- "Send on behalf of one of the owner's channels.",
783
- ),
784
- option(
785
- "--context-post-id <post-id>",
786
- "Attach a post id as conversation context.",
787
- ),
788
- CHANNEL_TOKEN_OPTION,
789
- PROFILE_OPTION,
790
- JSON_OPTION,
756
+ notes: [
757
+ "Recipient addresses support `email:<email>`, `user:<uuid>`, `user:@handle`, `channel:<uuid>`, and `channel:@handle/<channel-name>`.",
791
758
  ],
792
759
  },
793
760
  ),
794
761
  command(
795
762
  "reply",
796
763
  "Reply to an existing thread.",
797
- `${CLI_NAME} inbox reply <thread-id> (--body <markdown> | --body-file <path> | --stdin) [--sender-channel <name-or-uuid>] [--context-post-id <post-id>] [--channel-token <token>] [--profile <name>] [--json]`,
764
+ `${CLI_NAME} inbox reply <thread-id> (--body <markdown> | --body-file <path> | --stdin) [--from <channel-name-or-uuid>] [--context-post-id <post-id>] [--channel-token <token>] [--profile <name>] [--json]`,
798
765
  {
799
766
  options: [
800
767
  ...BODY_OPTIONS,
801
768
  option(
802
- "--sender-channel <name-or-uuid>",
769
+ "--from <channel-name-or-uuid>",
803
770
  "Reply as one of the owner's channels when the thread mailbox type allows it.",
804
771
  ),
805
772
  option(
@@ -810,15 +777,12 @@ const HELP_ROOT = group(
810
777
  PROFILE_OPTION,
811
778
  JSON_OPTION,
812
779
  ],
813
- notes: [
814
- "`--sender-channel` only applies to channel inbox threads; account threads reply as the owner.",
815
- ],
816
780
  },
817
781
  ),
818
782
  command(
819
- "mark-seen",
783
+ "seen",
820
784
  "Mark one thread as seen.",
821
- `${CLI_NAME} inbox mark-seen <thread-id> [--channel-token <token>] [--profile <name>] [--json]`,
785
+ `${CLI_NAME} inbox seen <thread-id> [--channel-token <token>] [--profile <name>] [--json]`,
822
786
  {
823
787
  options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
824
788
  },
@@ -0,0 +1,134 @@
1
+ export type FieldValue = string | number | boolean | null | undefined;
2
+
3
+ export function formatEmpty(value: FieldValue): string {
4
+ if (value === null || value === undefined || value === "") {
5
+ return "-";
6
+ }
7
+
8
+ return String(value);
9
+ }
10
+
11
+ export function formatTimestamp(value?: string | null): string {
12
+ if (!value) {
13
+ return "-";
14
+ }
15
+
16
+ const timestamp = Date.parse(value);
17
+
18
+ if (Number.isNaN(timestamp)) {
19
+ return value;
20
+ }
21
+
22
+ return formatLocalTimestamp(new Date(timestamp));
23
+ }
24
+
25
+ export function shortId(id?: string | null): string {
26
+ if (!id) {
27
+ return "-";
28
+ }
29
+
30
+ return id.length > 12 ? `${id.slice(0, 8)}...` : id;
31
+ }
32
+
33
+ export function renderFields(fields: Array<[label: string, value: FieldValue]>): string {
34
+ const visibleFields = fields.filter(([, value]) => value !== undefined);
35
+
36
+ if (visibleFields.length === 0) {
37
+ return "";
38
+ }
39
+
40
+ const labelWidth = Math.max(...visibleFields.map(([label]) => label.length));
41
+
42
+ return visibleFields
43
+ .map(([label, value]) => `${label.padEnd(labelWidth)} ${formatEmpty(value)}`)
44
+ .join("\n");
45
+ }
46
+
47
+ export function renderSection(title: string, body: string): string {
48
+ const trimmed = body.trimEnd();
49
+
50
+ if (!trimmed) {
51
+ return "";
52
+ }
53
+
54
+ return `${title}\n${trimmed}`;
55
+ }
56
+
57
+ export function renderBullets(items: string[]): string {
58
+ if (items.length === 0) {
59
+ return "-";
60
+ }
61
+
62
+ return items.map((item) => `- ${item}`).join("\n");
63
+ }
64
+
65
+ export function indent(text: string, spaces = 2): string {
66
+ const prefix = " ".repeat(spaces);
67
+ return text
68
+ .split("\n")
69
+ .map((line) => (line ? `${prefix}${line}` : ""))
70
+ .join("\n");
71
+ }
72
+
73
+ export function renderBodyBlock(body: string): string {
74
+ const normalized = body.replace(/\r\n/g, "\n").trimEnd();
75
+
76
+ if (!normalized) {
77
+ return indent("(empty)");
78
+ }
79
+
80
+ return indent(normalized);
81
+ }
82
+
83
+ export function renderPagination(nextCursor?: string | null): string {
84
+ return nextCursor ? `More results: --cursor ${nextCursor}` : "";
85
+ }
86
+
87
+ export function joinBlocks(blocks: Array<string | undefined | null | false>): string {
88
+ return blocks.filter(Boolean).join("\n\n");
89
+ }
90
+
91
+ export function renderTokenAction(input: {
92
+ title: string;
93
+ id?: string | null;
94
+ name?: string | null;
95
+ token?: string | null;
96
+ issuedAt?: string | null;
97
+ expiresAt?: string | null;
98
+ }): string {
99
+ return joinBlocks([
100
+ input.title,
101
+ renderFields([
102
+ ["ID", input.id],
103
+ ["Name", input.name],
104
+ ["Token", input.token],
105
+ ["Issued", input.issuedAt ? formatTimestamp(input.issuedAt) : undefined],
106
+ ["Expires", input.expiresAt ? formatTimestamp(input.expiresAt) : undefined],
107
+ ]),
108
+ ]);
109
+ }
110
+
111
+ function formatLocalTimestamp(value: Date): string {
112
+ const year = value.getFullYear();
113
+ const month = padDatePart(value.getMonth() + 1);
114
+ const day = padDatePart(value.getDate());
115
+ const hours = padDatePart(value.getHours());
116
+ const minutes = padDatePart(value.getMinutes());
117
+ const seconds = padDatePart(value.getSeconds());
118
+
119
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${formatTimezoneOffset(value)}`;
120
+ }
121
+
122
+ function formatTimezoneOffset(value: Date): string {
123
+ const offsetMinutes = -value.getTimezoneOffset();
124
+ const sign = offsetMinutes >= 0 ? "+" : "-";
125
+ const absoluteMinutes = Math.abs(offsetMinutes);
126
+ const hours = padDatePart(Math.floor(absoluteMinutes / 60));
127
+ const minutes = padDatePart(absoluteMinutes % 60);
128
+
129
+ return `${sign}${hours}:${minutes}`;
130
+ }
131
+
132
+ function padDatePart(value: number): string {
133
+ return String(value).padStart(2, "0");
134
+ }
package/src/types/api.ts CHANGED
@@ -67,10 +67,38 @@ export interface PostAttributes {
67
67
 
68
68
  export type MailboxType = "account" | "channel";
69
69
  export type ThreadStatus = "pending" | "open" | "blocked";
70
+ export type ThreadStatusFilter = ThreadStatus | "all";
71
+ export type MailboxFilter = MailboxType | "all";
72
+
73
+ export type InboxRecipient =
74
+ | { type: "user"; address: { kind: "email"; value: string } }
75
+ | { type: "user"; address: { kind: "handle"; value: string } }
76
+ | { type: "user"; address: { kind: "id"; value: string } }
77
+ | { type: "channel"; address: { kind: "id"; value: string } }
78
+ | {
79
+ type: "channel";
80
+ address: {
81
+ kind: "owner_handle_and_channel_name";
82
+ owner_handle: string;
83
+ channel_name: string;
84
+ };
85
+ };
86
+
87
+ export interface InboxSender {
88
+ type: "channel";
89
+ address: {
90
+ kind: "id";
91
+ value: string;
92
+ };
93
+ }
70
94
 
71
95
  export interface ThreadAttributes {
72
96
  mailbox_type: MailboxType;
73
97
  status: ThreadStatus;
98
+ participant_a_owner_id?: string | null;
99
+ participant_a_channel_id?: string | null;
100
+ participant_b_owner_id?: string | null;
101
+ participant_b_channel_id?: string | null;
74
102
  opened_at?: string | null;
75
103
  expires_at?: string | null;
76
104
  last_message_at?: string;
@@ -88,6 +116,9 @@ export interface ThreadAttributes {
88
116
 
89
117
  export interface MessageAttributes {
90
118
  body: string;
119
+ sender_owner_id?: string | null;
120
+ sender_channel_id?: string | null;
121
+ thread_id?: string | null;
91
122
  context_post_id?: string | null;
92
123
  inserted_at?: string;
93
124
  }