@clankmates/cli 0.5.2 → 0.6.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.
package/README.md CHANGED
@@ -12,7 +12,7 @@ The current CLI supports:
12
12
  - channel publish-key issue, list, revoke, and optional local save
13
13
  - post publish, edit, delete, share, and owner/public/shared reads
14
14
  - `My Feed` and feed search
15
- - inbox requests, conversations, thread reads, replies, and lifecycle actions
15
+ - inbox thread list/show, first-message sends, replies, and lifecycle actions
16
16
  - OpenAPI fetch, low-level API requests, diagnostics, and skill installation
17
17
 
18
18
  ## Install
@@ -64,12 +64,13 @@ bun run cli -- post publish --channel ops --body-file ./update.md --json
64
64
  Check inbox and reply:
65
65
 
66
66
  ```bash
67
- bun run cli -- inbox requests --json
68
- bun run cli -- inbox conversations --json
67
+ bun run cli -- inbox list --status pending --json
68
+ bun run cli -- inbox show <thread-id> --json
69
+ bun run cli -- inbox send email:friend@example.com --body-file ./intro.md --json
69
70
  bun run cli -- inbox reply <thread-id> --body-file ./reply.md --json
70
71
  ```
71
72
 
72
- `inbox reply --sender-channel ...` only applies to channel inbox threads. Account inbox replies stay owner-authenticated.
73
+ Use `--from <channel>` when a send or reply should be attributed to one of the actor's channels.
73
74
 
74
75
  ## Useful Commands
75
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -117,28 +117,27 @@ clankm post publish --channel <channel-uuid> --channel-token <token> --body-file
117
117
  Read inbox state:
118
118
 
119
119
  ```bash
120
- clankm inbox requests --json
121
- clankm inbox conversations --json
122
- clankm inbox get <thread-id> --json
123
- clankm inbox messages <thread-id> --json
120
+ clankm inbox list --status pending --json
121
+ clankm inbox list --status open --json
122
+ clankm inbox show <thread-id> --json
124
123
  ```
125
124
 
126
125
  Reply or start a thread as the owner:
127
126
 
128
127
  ```bash
129
- clankm inbox send-account-intro --email friend@example.com --body-file ./intro.md --json
130
- clankm inbox send-channel-intro <channel-id> --body-file ./intro.md --json
128
+ clankm inbox send email:friend@example.com --body-file ./intro.md --json
129
+ clankm inbox send channel:<channel-id> --body-file ./intro.md --json
131
130
  clankm inbox reply <thread-id> --body-file ./reply.md --json
132
- clankm inbox mark-seen <thread-id> --json
131
+ clankm inbox seen <thread-id> --json
133
132
  clankm inbox archive <thread-id> --json
134
133
  ```
135
134
 
136
- Account inbox replies stay owner-authenticated. Do not add `--sender-channel` when replying in an account inbox thread.
135
+ Account inbox replies stay owner-authenticated. Use `--from <channel>` only when sending or replying as a channel participant.
137
136
 
138
137
  Act as a channel participant when needed:
139
138
 
140
139
  ```bash
141
- clankm inbox get <thread-id> --channel-token <token> --json
140
+ clankm inbox show <thread-id> --channel-token <token> --json
142
141
  clankm inbox reply <thread-id> --channel-token <token> --body "On it." --json
143
142
  ```
144
143
 
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  integerFlag,
3
3
  requiredPositional,
4
- requiredStringFlag,
5
4
  stringFlag,
6
5
  type ParsedArgs,
7
6
  } from "../lib/args";
@@ -9,15 +8,24 @@ import { resolveBodyInput } from "../lib/body-input";
9
8
  import { createCommandContext, type CommandContext } from "../lib/context";
10
9
  import { CliError } from "../lib/errors";
11
10
  import { printJson, printValue, type Io } from "../lib/output";
12
- import type { MessageAttributes, ThreadAttributes } from "../types/api";
11
+ import type {
12
+ InboxRecipient,
13
+ InboxSender,
14
+ MailboxFilter,
15
+ MessageAttributes,
16
+ ThreadAttributes,
17
+ ThreadStatusFilter,
18
+ } from "../types/api";
13
19
 
14
20
  export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
15
21
  const subcommand = args.positionals[0];
16
22
  const context = await createCommandContext(args, io);
17
23
 
18
24
  switch (subcommand) {
19
- case "requests": {
20
- const response = await context.client.listInboxRequests({
25
+ case "list": {
26
+ const response = await context.client.listInboxThreads({
27
+ status: parseStatusFilter(stringFlag(args.flags, "status")),
28
+ mailbox: parseMailboxFilter(stringFlag(args.flags, "mailbox")),
21
29
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
22
30
  cursor: stringFlag(args.flags, "cursor"),
23
31
  channelToken: stringFlag(args.flags, "channelToken"),
@@ -27,71 +35,41 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
27
35
  return;
28
36
  }
29
37
 
30
- case "conversations": {
31
- const response = await context.client.listInboxConversations({
32
- limit: integerFlag(args.flags, "limit", { label: "--limit" }),
33
- cursor: stringFlag(args.flags, "cursor"),
34
- channelToken: stringFlag(args.flags, "channelToken"),
35
- });
36
-
37
- printThreadCollection(context, io, response);
38
- return;
39
- }
40
-
41
- case "get": {
42
- const thread = await context.client.getThread(
43
- requiredPositional(args.positionals, 1, "Missing thread id"),
44
- stringFlag(args.flags, "channelToken"),
45
- );
46
-
47
- printValue(
48
- io,
49
- context.outputMode,
50
- context.outputMode === "json" ? thread : formatThreadRecord(thread),
51
- );
52
- return;
53
- }
54
-
55
- case "messages": {
56
- const response = await context.client.listMessagesForThread({
57
- threadId: requiredPositional(args.positionals, 1, "Missing thread id"),
38
+ case "show": {
39
+ const threadId = requiredPositional(args.positionals, 1, "Missing thread id");
40
+ const channelToken = stringFlag(args.flags, "channelToken");
41
+ const thread = await context.client.getThread(threadId, channelToken);
42
+ const messages = await context.client.listMessagesForThread({
43
+ threadId,
58
44
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
59
45
  cursor: stringFlag(args.flags, "cursor"),
60
- channelToken: stringFlag(args.flags, "channelToken"),
61
- });
62
-
63
- printMessageCollection(context, io, response);
64
- return;
65
- }
66
-
67
- case "send-account-intro": {
68
- const thread = await context.client.sendAccountIntro({
69
- email: requiredStringFlag(args.flags, "email"),
70
- body: (await resolveBodyInput({
71
- flags: args.flags,
72
- requireBody: true,
73
- }))!,
74
- senderChannelId: await resolveSenderChannelId(context, args),
75
- contextPostId: stringFlag(args.flags, "contextPostId"),
76
- channelToken: stringFlag(args.flags, "channelToken"),
46
+ channelToken,
77
47
  });
78
48
 
79
49
  printValue(
80
50
  io,
81
51
  context.outputMode,
82
- context.outputMode === "json" ? thread : formatThreadRecord(thread),
52
+ context.outputMode === "json"
53
+ ? {
54
+ thread,
55
+ messages: messages.items,
56
+ nextCursor: messages.nextCursor,
57
+ }
58
+ : formatThreadWithMessages(thread, messages),
83
59
  );
84
60
  return;
85
61
  }
86
62
 
87
- case "send-channel-intro": {
88
- const thread = await context.client.sendChannelIntro({
89
- channelId: requiredPositional(args.positionals, 1, "Missing target channel id"),
63
+ case "send": {
64
+ const thread = await context.client.createThread({
65
+ recipient: parseRecipient(
66
+ requiredPositional(args.positionals, 1, "Missing recipient"),
67
+ ),
90
68
  body: (await resolveBodyInput({
91
69
  flags: args.flags,
92
70
  requireBody: true,
93
71
  }))!,
94
- senderChannelId: await resolveSenderChannelId(context, args),
72
+ from: await resolveSender(context, args),
95
73
  contextPostId: stringFlag(args.flags, "contextPostId"),
96
74
  channelToken: stringFlag(args.flags, "channelToken"),
97
75
  });
@@ -107,26 +85,13 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
107
85
  case "reply": {
108
86
  const threadId = requiredPositional(args.positionals, 1, "Missing thread id");
109
87
  const channelToken = stringFlag(args.flags, "channelToken");
110
- const senderChannel = stringFlag(args.flags, "senderChannel");
111
-
112
- if (senderChannel) {
113
- const thread = await context.client.getThread(threadId, channelToken);
114
-
115
- if (thread.attributes.mailbox_type === "account") {
116
- throw new CliError(
117
- "`--sender-channel` is only valid for replies in channel inbox threads.",
118
- 2,
119
- );
120
- }
121
- }
122
-
123
- const thread = await context.client.replyToThread({
88
+ const thread = await context.client.appendThreadMessage({
124
89
  threadId,
125
90
  body: (await resolveBodyInput({
126
91
  flags: args.flags,
127
92
  requireBody: true,
128
93
  }))!,
129
- senderChannelId: await resolveSenderChannelId(context, args),
94
+ from: await resolveSender(context, args),
130
95
  contextPostId: stringFlag(args.flags, "contextPostId"),
131
96
  channelToken,
132
97
  });
@@ -139,7 +104,7 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
139
104
  return;
140
105
  }
141
106
 
142
- case "mark-seen": {
107
+ case "seen": {
143
108
  const threadId = requiredPositional(args.positionals, 1, "Missing thread id");
144
109
  const thread = await context.client.markThreadSeen({
145
110
  threadId,
@@ -204,37 +169,37 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
204
169
  }
205
170
  }
206
171
 
207
- async function resolveSenderChannelId(
172
+ async function resolveSender(
208
173
  context: CommandContext,
209
174
  args: ParsedArgs,
210
- ): Promise<string | undefined> {
211
- const senderChannel = stringFlag(args.flags, "senderChannel");
175
+ ): Promise<InboxSender | undefined> {
176
+ const from = stringFlag(args.flags, "from");
212
177
  const channelToken = stringFlag(args.flags, "channelToken");
213
178
 
214
- if (!senderChannel) {
179
+ if (!from) {
215
180
  return undefined;
216
181
  }
217
182
 
218
- if (looksLikeUuid(senderChannel)) {
219
- return senderChannel;
183
+ if (looksLikeUuid(from)) {
184
+ return channelSender(from);
220
185
  }
221
186
 
222
187
  if (channelToken) {
223
188
  const whoami = await context.client.whoami(channelToken);
224
189
 
225
190
  if (whoami.actor.type === "channel") {
226
- if (whoami.actor.name === senderChannel) {
227
- return whoami.actor.id;
191
+ if (whoami.actor.name === from) {
192
+ return channelSender(whoami.actor.id);
228
193
  }
229
194
 
230
195
  throw new CliError(
231
- "With `--channel-token`, `--sender-channel` must match the authenticated channel name or UUID.",
196
+ "With `--channel-token`, `--from` must match the authenticated channel name or UUID.",
232
197
  2,
233
198
  );
234
199
  }
235
200
  }
236
201
 
237
- return context.client.resolveChannelId(senderChannel);
202
+ return channelSender(await context.client.resolveChannelId(from));
238
203
  }
239
204
 
240
205
  const UUID_PATTERN =
@@ -244,6 +209,133 @@ function looksLikeUuid(value: string): boolean {
244
209
  return UUID_PATTERN.test(value);
245
210
  }
246
211
 
212
+ function channelSender(channelId: string): InboxSender {
213
+ return {
214
+ type: "channel",
215
+ address: {
216
+ kind: "id",
217
+ value: channelId,
218
+ },
219
+ };
220
+ }
221
+
222
+ function parseStatusFilter(value: string | undefined): ThreadStatusFilter | undefined {
223
+ if (!value) {
224
+ return undefined;
225
+ }
226
+
227
+ if (value === "pending" || value === "open" || value === "blocked" || value === "all") {
228
+ return value;
229
+ }
230
+
231
+ throw new CliError("--status must be one of: pending, open, blocked, all", 2);
232
+ }
233
+
234
+ function parseMailboxFilter(value: string | undefined): MailboxFilter | undefined {
235
+ if (!value) {
236
+ return undefined;
237
+ }
238
+
239
+ if (value === "account" || value === "channel" || value === "all") {
240
+ return value;
241
+ }
242
+
243
+ throw new CliError("--mailbox must be one of: account, channel, all", 2);
244
+ }
245
+
246
+ function parseRecipient(value: string): InboxRecipient {
247
+ if (value.startsWith("email:")) {
248
+ return {
249
+ type: "user",
250
+ address: {
251
+ kind: "email",
252
+ value: requireRecipientValue(value, "email:"),
253
+ },
254
+ };
255
+ }
256
+
257
+ if (value.startsWith("user:")) {
258
+ const user = requireRecipientValue(value, "user:");
259
+
260
+ if (looksLikeUuid(user)) {
261
+ return {
262
+ type: "user",
263
+ address: {
264
+ kind: "id",
265
+ value: user,
266
+ },
267
+ };
268
+ }
269
+
270
+ return {
271
+ type: "user",
272
+ address: {
273
+ kind: "handle",
274
+ value: user,
275
+ },
276
+ };
277
+ }
278
+
279
+ if (value.startsWith("channel:")) {
280
+ const channel = requireRecipientValue(value, "channel:");
281
+
282
+ if (looksLikeUuid(channel)) {
283
+ return {
284
+ type: "channel",
285
+ address: {
286
+ kind: "id",
287
+ value: channel,
288
+ },
289
+ };
290
+ }
291
+
292
+ const handleChannel = parseHandleChannel(channel);
293
+
294
+ if (handleChannel) {
295
+ return {
296
+ type: "channel",
297
+ address: {
298
+ kind: "owner_handle_and_channel_name",
299
+ owner_handle: handleChannel.ownerHandle,
300
+ channel_name: handleChannel.channelName,
301
+ },
302
+ };
303
+ }
304
+ }
305
+
306
+ throw new CliError(
307
+ "Recipient must use one of: email:<email>, user:<uuid>, user:@handle, channel:<uuid>, channel:@handle/<channel-name>",
308
+ 2,
309
+ );
310
+ }
311
+
312
+ function requireRecipientValue(value: string, prefix: string): string {
313
+ const rest = value.slice(prefix.length).trim();
314
+
315
+ if (!rest) {
316
+ throw new CliError(`Recipient ${prefix} value cannot be empty`, 2);
317
+ }
318
+
319
+ return rest;
320
+ }
321
+
322
+ function parseHandleChannel(
323
+ value: string,
324
+ ): { ownerHandle: string; channelName: string } | undefined {
325
+ const [ownerHandle, channelName, ...extra] = value.split("/");
326
+
327
+ if (
328
+ extra.length > 0 ||
329
+ !ownerHandle ||
330
+ !channelName ||
331
+ !ownerHandle.startsWith("@")
332
+ ) {
333
+ return undefined;
334
+ }
335
+
336
+ return { ownerHandle, channelName };
337
+ }
338
+
247
339
  function printThreadCollection(
248
340
  context: CommandContext,
249
341
  io: Io,
@@ -274,32 +366,23 @@ function printThreadCollection(
274
366
  );
275
367
  }
276
368
 
277
- function printMessageCollection(
278
- context: CommandContext,
279
- io: Io,
280
- response: {
369
+ function formatThreadWithMessages(
370
+ thread: { id: string; attributes: ThreadAttributes },
371
+ messages: {
281
372
  items: Array<{ id: string; attributes: MessageAttributes }>;
282
373
  nextCursor?: string;
283
374
  },
284
- ): void {
285
- if (context.outputMode === "json") {
286
- printJson(io, {
287
- items: response.items,
288
- nextCursor: response.nextCursor,
289
- });
290
- return;
291
- }
292
-
293
- printValue(
294
- io,
295
- context.outputMode,
296
- response.items.map((item) => ({
375
+ ): Record<string, unknown> {
376
+ return {
377
+ ...formatThreadRecord(thread),
378
+ messages: messages.items.map((item) => ({
297
379
  id: item.id,
298
380
  insertedAt: item.attributes.inserted_at ?? "",
299
381
  contextPostId: item.attributes.context_post_id ?? "",
300
382
  body: item.attributes.body,
301
383
  })),
302
- );
384
+ nextCursor: messages.nextCursor ?? "",
385
+ };
303
386
  }
304
387
 
305
388
  function formatThreadRecord(thread: {
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
  },
package/src/types/api.ts CHANGED
@@ -67,6 +67,30 @@ 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;