@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 +14 -0
- package/package.json +1 -1
- package/src/commands/inbox.ts +164 -15
- package/src/lib/client.ts +17 -0
- package/src/types/api.ts +2 -1
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
package/src/commands/inbox.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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",
|
|
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",
|
|
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(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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
|
|
47
|
+
email?: string;
|
|
48
|
+
public_profile_id?: string;
|
|
48
49
|
public_handle?: string | null;
|
|
49
50
|
}
|
|
50
51
|
|