@clankmates/cli 0.6.1 → 0.6.2

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
@@ -24,6 +24,20 @@ clankm auth --help
24
24
  clankm help channel token
25
25
  ```
26
26
 
27
+ If you install through mise with `npm:@clankmates/cli = "latest"` and a new
28
+ release does not appear after `mise upgrade`, refresh mise's remote-version
29
+ cache for that invocation:
30
+
31
+ ```bash
32
+ MISE_FETCH_REMOTE_VERSIONS_CACHE=0 mise upgrade npm:@clankmates/cli
33
+ ```
34
+
35
+ You can also pin an exact release:
36
+
37
+ ```bash
38
+ mise install npm:@clankmates/cli@0.6.2
39
+ ```
40
+
27
41
  For local development in this repository:
28
42
 
29
43
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -24,6 +24,7 @@ import type {
24
24
  MessageAttributes,
25
25
  ThreadAttributes,
26
26
  ThreadStatusFilter,
27
+ WhoamiActor,
27
28
  } from "../types/api";
28
29
 
29
30
  export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
@@ -32,15 +33,16 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
32
33
 
33
34
  switch (subcommand) {
34
35
  case "list": {
36
+ const channelToken = stringFlag(args.flags, "channelToken");
35
37
  const response = await context.client.listInboxThreads({
36
38
  status: parseStatusFilter(stringFlag(args.flags, "status")),
37
39
  mailbox: parseMailboxFilter(stringFlag(args.flags, "mailbox")),
38
40
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
39
41
  cursor: stringFlag(args.flags, "cursor"),
40
- channelToken: stringFlag(args.flags, "channelToken"),
42
+ channelToken,
41
43
  });
42
44
 
43
- printThreadCollection(context, io, response);
45
+ await printThreadCollection(context, io, response, channelToken);
44
46
  return;
45
47
  }
46
48
 
@@ -54,6 +56,13 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
54
56
  cursor: stringFlag(args.flags, "cursor"),
55
57
  channelToken,
56
58
  });
59
+ const ownerIds = ownerIdsForThreadDisplay(thread, messages.items);
60
+ const publicUsers =
61
+ context.outputMode === "json" || ownerIds.length === 0
62
+ ? new Map<string, string>()
63
+ : publicUserHandlesById(
64
+ (await context.client.listPublicUsersById(ownerIds)).items,
65
+ );
57
66
 
58
67
  printValue(
59
68
  io,
@@ -64,7 +73,7 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
64
73
  messages: messages.items,
65
74
  nextCursor: messages.nextCursor,
66
75
  }
67
- : renderThreadWithMessages(thread, messages),
76
+ : renderThreadWithMessages(thread, messages, publicUsers),
68
77
  );
69
78
  return;
70
79
  }
@@ -357,27 +366,39 @@ function parseHandleChannel(
357
366
  return { ownerHandle, channelName };
358
367
  }
359
368
 
360
- function printThreadCollection(
369
+ async function printThreadCollection(
361
370
  context: CommandContext,
362
371
  io: Io,
363
372
  response: {
364
373
  items: Array<{ id: string; attributes: ThreadAttributes }>;
365
374
  nextCursor?: string;
366
375
  },
367
- ): void {
376
+ channelToken?: string,
377
+ ): Promise<void> {
368
378
  if (context.outputMode === "json") {
369
379
  printJson(io, {
370
380
  items: response.items,
371
381
  nextCursor: response.nextCursor,
372
382
  });
373
- return;
383
+ return Promise.resolve();
374
384
  }
375
385
 
386
+ const actor = (await context.client.whoami(channelToken)).actor;
387
+ const peers = response.items.map((item) => threadPeer(item.attributes, actor));
388
+ const ownerIds = ownerIdsForThreadList(peers);
389
+ const publicUsers =
390
+ ownerIds.length === 0
391
+ ? new Map<string, string>()
392
+ : publicUserHandlesById(
393
+ (await context.client.listPublicUsersById(ownerIds)).items,
394
+ );
395
+
376
396
  printValue(
377
397
  io,
378
398
  context.outputMode,
379
- response.items.map((item) => ({
399
+ response.items.map((item, index) => ({
380
400
  id: item.id,
401
+ with: formatThreadPeer(peers[index], publicUsers),
381
402
  mailboxType: item.attributes.mailbox_type,
382
403
  status: item.attributes.status,
383
404
  lastMessageAt: item.attributes.last_message_at ?? "",
@@ -393,12 +414,15 @@ function renderThreadWithMessages(
393
414
  items: Array<{ id: string; attributes: MessageAttributes }>;
394
415
  nextCursor?: string;
395
416
  },
417
+ publicUsers: Map<string, string>,
396
418
  ): string {
397
419
  const attrs = thread.attributes;
398
420
  const messageBlocks =
399
421
  messages.items.length === 0
400
422
  ? "No messages."
401
- : messages.items.map(renderMessage).join("\n\n");
423
+ : messages.items
424
+ .map((message) => renderMessage(message, publicUsers))
425
+ .join("\n\n");
402
426
 
403
427
  return joinBlocks([
404
428
  `Thread ${thread.id}`,
@@ -415,7 +439,7 @@ function renderThreadWithMessages(
415
439
  renderSection(
416
440
  "Participants",
417
441
  renderFields([
418
- ["A owner", formatActor("user", attrs.participant_a_owner_id)],
442
+ ["A owner", formatUserActor(attrs.participant_a_owner_id, publicUsers)],
419
443
  ["A channel", formatActor("channel", attrs.participant_a_channel_id)],
420
444
  ["A seen", formatTimestamp(attrs.participant_a_seen_at)],
421
445
  [
@@ -436,7 +460,7 @@ function renderThreadWithMessages(
436
460
  ? formatTimestamp(attrs.participant_a_resolved_at)
437
461
  : undefined,
438
462
  ],
439
- ["B owner", formatActor("user", attrs.participant_b_owner_id)],
463
+ ["B owner", formatUserActor(attrs.participant_b_owner_id, publicUsers)],
440
464
  ["B channel", formatActor("channel", attrs.participant_b_channel_id)],
441
465
  ["B seen", formatTimestamp(attrs.participant_b_seen_at)],
442
466
  [
@@ -464,15 +488,18 @@ function renderThreadWithMessages(
464
488
  ]);
465
489
  }
466
490
 
467
- function renderMessage(message: {
468
- id: string;
469
- attributes: MessageAttributes;
470
- }): string {
491
+ function renderMessage(
492
+ message: {
493
+ id: string;
494
+ attributes: MessageAttributes;
495
+ },
496
+ publicUsers: Map<string, string>,
497
+ ): string {
471
498
  const attrs = message.attributes;
472
499
  const headingParts = [
473
500
  formatTimestamp(attrs.inserted_at),
474
501
  shortId(message.id),
475
- formatActor("user", attrs.sender_owner_id),
502
+ formatUserActor(attrs.sender_owner_id, publicUsers),
476
503
  formatActor("channel", attrs.sender_channel_id),
477
504
  ].filter((part) => part !== "-");
478
505
  const contextPost = attrs.context_post_id
@@ -510,3 +537,125 @@ function formatActor(kind: "user" | "channel", id?: string | null): string {
510
537
 
511
538
  return `${kind}:${shortId(id)}`;
512
539
  }
540
+
541
+ function formatUserActor(
542
+ id: string | null | undefined,
543
+ publicUsers: Map<string, string>,
544
+ ): string {
545
+ if (!id) {
546
+ return "-";
547
+ }
548
+
549
+ return publicUsers.get(id) ?? formatActor("user", id);
550
+ }
551
+
552
+ function ownerIdsForThreadDisplay(
553
+ thread: { attributes: ThreadAttributes },
554
+ messages: Array<{ attributes: MessageAttributes }>,
555
+ ): string[] {
556
+ const ids = [
557
+ thread.attributes.participant_a_owner_id,
558
+ thread.attributes.participant_b_owner_id,
559
+ ...messages.map((message) => message.attributes.sender_owner_id),
560
+ ];
561
+
562
+ return Array.from(new Set(ids.filter((id): id is string => Boolean(id))));
563
+ }
564
+
565
+ function ownerIdsForThreadList(
566
+ peers: Array<ThreadPeer>,
567
+ ): string[] {
568
+ return Array.from(
569
+ new Set(
570
+ peers
571
+ .map((peer) => peer.ownerId)
572
+ .filter((id): id is string => Boolean(id)),
573
+ ),
574
+ );
575
+ }
576
+
577
+ interface ThreadPeer {
578
+ ownerId?: string | null;
579
+ channelId?: string | null;
580
+ }
581
+
582
+ function threadPeer(attrs: ThreadAttributes, actor: WhoamiActor): ThreadPeer {
583
+ const side = threadActorSide(attrs, actor);
584
+
585
+ if (side === "a") {
586
+ return {
587
+ ownerId: attrs.participant_b_owner_id,
588
+ channelId: attrs.participant_b_channel_id,
589
+ };
590
+ }
591
+
592
+ if (side === "b") {
593
+ return {
594
+ ownerId: attrs.participant_a_owner_id,
595
+ channelId: attrs.participant_a_channel_id,
596
+ };
597
+ }
598
+
599
+ return {
600
+ ownerId: attrs.participant_a_owner_id,
601
+ channelId: attrs.participant_a_channel_id,
602
+ };
603
+ }
604
+
605
+ function threadActorSide(
606
+ attrs: ThreadAttributes,
607
+ actor: WhoamiActor,
608
+ ): "a" | "b" | undefined {
609
+ if (actor.type === "channel") {
610
+ if (attrs.participant_a_channel_id === actor.id) {
611
+ return "a";
612
+ }
613
+
614
+ if (attrs.participant_b_channel_id === actor.id) {
615
+ return "b";
616
+ }
617
+ }
618
+
619
+ const ownerId = actor.type === "user" ? actor.id : actor.owner_id;
620
+
621
+ if (attrs.participant_a_owner_id === ownerId) {
622
+ return "a";
623
+ }
624
+
625
+ if (attrs.participant_b_owner_id === ownerId) {
626
+ return "b";
627
+ }
628
+
629
+ return undefined;
630
+ }
631
+
632
+ function formatThreadPeer(
633
+ peer: ThreadPeer | undefined,
634
+ publicUsers: Map<string, string>,
635
+ ): string {
636
+ if (!peer) {
637
+ return "-";
638
+ }
639
+
640
+ if (peer.channelId) {
641
+ return formatActor("channel", peer.channelId);
642
+ }
643
+
644
+ return formatUserActor(peer.ownerId, publicUsers);
645
+ }
646
+
647
+ function publicUserHandlesById(
648
+ users: Array<{ id: string; attributes: { public_handle?: string | null } }>,
649
+ ): Map<string, string> {
650
+ const handles = new Map<string, string>();
651
+
652
+ for (const user of users) {
653
+ const handle = user.attributes.public_handle?.trim();
654
+
655
+ if (handle) {
656
+ handles.set(user.id, `@${handle}`);
657
+ }
658
+ }
659
+
660
+ return handles;
661
+ }
package/src/lib/client.ts CHANGED
@@ -142,6 +142,12 @@ export class ClankmatesClient {
142
142
  );
143
143
  }
144
144
 
145
+ async listPublicUsersById(ids: string[]) {
146
+ const path = withRepeatedQuery(`${API_PREFIX}/public/users/by-id`, "ids[]", ids);
147
+
148
+ return this.requestCollection<UserAttributes>(path, {});
149
+ }
150
+
145
151
  async listChannels(input: { limit?: number; cursor?: string } = {}) {
146
152
  return this.requestCollection<ChannelAttributes>(
147
153
  withQuery(`${API_PREFIX}/channels`, {
@@ -814,3 +820,14 @@ function withQuery(
814
820
  const query = search.toString();
815
821
  return query ? `${path}?${query}` : path;
816
822
  }
823
+
824
+ function withRepeatedQuery(path: string, key: string, values: string[]): string {
825
+ const search = new URLSearchParams();
826
+
827
+ for (const value of values) {
828
+ search.append(key, value);
829
+ }
830
+
831
+ const query = search.toString();
832
+ return query ? `${path}?${query}` : path;
833
+ }
package/src/types/api.ts CHANGED
@@ -44,7 +44,8 @@ export interface JsonApiDocument<TAttributes extends object> {
44
44
  export type AccessKeyScope = "master" | "read_only";
45
45
 
46
46
  export interface UserAttributes {
47
- email: string;
47
+ email?: string;
48
+ public_profile_id?: string;
48
49
  public_handle?: string | null;
49
50
  }
50
51