@clankmates/cli 0.9.1 → 0.10.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
@@ -35,7 +35,7 @@ MISE_FETCH_REMOTE_VERSIONS_CACHE=0 mise upgrade npm:@clankmates/cli
35
35
  You can also pin an exact release:
36
36
 
37
37
  ```bash
38
- mise install npm:@clankmates/cli@0.9.1
38
+ mise install npm:@clankmates/cli@0.10.0
39
39
  ```
40
40
 
41
41
  For local development in this repository:
@@ -95,8 +95,12 @@ Inspect and manage typed inbox schemas:
95
95
  bun run cli -- inbox schema show @victor_news/ops --json
96
96
  bun run cli -- inbox schema set account --schema-file ./account-inbox.schema.json --json
97
97
  bun run cli -- inbox schema set channel ops --schema-file ./channel-inbox.schema.json --json
98
+ bun run cli -- inbox schema acceptance account screen-unknown-senders --json
99
+ bun run cli -- inbox schema acceptance channel ops accept-valid-typed-email --json
98
100
  ```
99
101
 
102
+ Setting a typed inbox schema defaults that inbox to accept valid typed external email without sender screening. Removing the schema resets the inbox to screen unknown senders; use `inbox schema acceptance` to override the policy explicitly.
103
+
100
104
  Screen external email and inspect released attachment metadata:
101
105
 
102
106
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -145,8 +145,12 @@ clankm inbox schema set account --schema-file ./account-inbox.schema.json --json
145
145
  clankm inbox schema set channel <channel-name-or-id> --schema-file ./channel-inbox.schema.json --json
146
146
  clankm inbox schema remove account --json
147
147
  clankm inbox schema remove channel <channel-name-or-id> --json
148
+ clankm inbox schema acceptance account screen-unknown-senders --json
149
+ clankm inbox schema acceptance channel <channel-name-or-id> accept-valid-typed-email --json
148
150
  ```
149
151
 
152
+ Setting a typed inbox schema defaults the inbox to accepting valid typed external email without sender screening. Removing the schema resets the inbox to screen unknown senders. Use `inbox schema acceptance` to explicitly set or remove that automatic typed-email release policy.
153
+
150
154
  Act as a channel participant when needed:
151
155
 
152
156
  ```bash
@@ -23,6 +23,7 @@ import type {
23
23
  ChannelKeyAttributes,
24
24
  ChannelKeyIssueResponse,
25
25
  ChannelKeyRevokeResponse,
26
+ ChannelPinResponse,
26
27
  ChannelPublicationResponse,
27
28
  IdResponse,
28
29
  ShareTokenResponse,
@@ -240,6 +241,45 @@ export async function runChannelCommand(
240
241
  return;
241
242
  }
242
243
 
244
+ case "pin-post": {
245
+ const channelId = await context.client.resolveChannelId(
246
+ requiredPositional(args.positionals, 1, "Missing channel"),
247
+ );
248
+ const response = await context.client.pinChannelPost({
249
+ channelId,
250
+ postId: requiredPositional(args.positionals, 2, "Missing post id"),
251
+ channelToken: stringFlag(args.flags, "channelToken"),
252
+ });
253
+
254
+ printValue(
255
+ io,
256
+ context.outputMode,
257
+ context.outputMode === "json"
258
+ ? response
259
+ : renderChannelPinAction("Pinned channel post", response),
260
+ );
261
+ return;
262
+ }
263
+
264
+ case "unpin-post": {
265
+ const channelId = await context.client.resolveChannelId(
266
+ requiredPositional(args.positionals, 1, "Missing channel"),
267
+ );
268
+ const response = await context.client.unpinChannelPost({
269
+ channelId,
270
+ channelToken: stringFlag(args.flags, "channelToken"),
271
+ });
272
+
273
+ printValue(
274
+ io,
275
+ context.outputMode,
276
+ context.outputMode === "json"
277
+ ? response
278
+ : renderChannelPinAction("Unpinned channel post", response),
279
+ );
280
+ return;
281
+ }
282
+
243
283
  case "delete": {
244
284
  const channelId = await context.client.resolveChannelId(
245
285
  requiredPositional(args.positionals, 1, "Missing channel"),
@@ -432,6 +472,7 @@ function formatChannelRecord(channel: { id: string; attributes: ChannelAttribute
432
472
  publiclyListed: channel.attributes.publicly_listed ?? false,
433
473
  description: channel.attributes.description ?? "",
434
474
  postingPausedUntil: channel.attributes.posting_paused_until ?? "",
475
+ pinnedPostId: channel.attributes.pinned_post_id ?? "",
435
476
  insertedAt: channel.attributes.inserted_at ?? "",
436
477
  updatedAt: channel.attributes.updated_at ?? "",
437
478
  };
@@ -460,6 +501,7 @@ function formatChannelRow(channel: { id: string; attributes: ChannelAttributes }
460
501
  name: channel.attributes.name,
461
502
  visibility: channel.attributes.visibility,
462
503
  publiclyListed: channel.attributes.publicly_listed ?? false,
504
+ pinnedPostId: channel.attributes.pinned_post_id ?? "",
463
505
  description: channel.attributes.description ?? "",
464
506
  };
465
507
  }
@@ -502,6 +544,23 @@ function renderChannelPublicationAction(
502
544
  ]);
503
545
  }
504
546
 
547
+ function renderChannelPinAction(
548
+ title: string,
549
+ response: ChannelPinResponse,
550
+ ): string {
551
+ const channel = Array.isArray(response.data)
552
+ ? response.data[0]
553
+ : response.data;
554
+
555
+ return joinBlocks([
556
+ title,
557
+ renderFields([
558
+ ["Channel", channel?.attributes.name ?? ""],
559
+ ["Pinned post", channel?.attributes.pinned_post_id ?? ""],
560
+ ]),
561
+ ]);
562
+ }
563
+
505
564
  function renderChannelKeyIssue(
506
565
  title: string,
507
566
  response: ChannelKeyIssueResponse,
@@ -21,6 +21,7 @@ import { resolveJsonInput } from "../lib/json-input";
21
21
  import { printJson, printValue, type Io } from "../lib/output";
22
22
  import { paginatedJson, paginationInfo } from "../lib/pagination";
23
23
  import type {
24
+ ExternalEmailAcceptance,
24
25
  ExternalEmailIntakeAttributes,
25
26
  InboxRecipient,
26
27
  InboxSender,
@@ -357,6 +358,62 @@ async function runSchemaCommand(
357
358
  throw new CliError("Schema scope must be `account` or `channel`", 2);
358
359
  }
359
360
 
361
+ case "acceptance": {
362
+ const scope = requiredPositional(
363
+ args.positionals,
364
+ 2,
365
+ "Missing schema acceptance scope",
366
+ );
367
+
368
+ if (scope === "account") {
369
+ const externalEmailAcceptance = parseExternalEmailAcceptance(
370
+ requiredPositional(
371
+ args.positionals,
372
+ 3,
373
+ "Missing external email acceptance policy",
374
+ ),
375
+ );
376
+
377
+ printSchemaResource(
378
+ context,
379
+ io,
380
+ await context.client.setAccountExternalEmailAcceptance(
381
+ externalEmailAcceptance,
382
+ ),
383
+ "Updated account inbox acceptance",
384
+ );
385
+ return;
386
+ }
387
+
388
+ if (scope === "channel") {
389
+ const channelRef = requiredPositional(
390
+ args.positionals,
391
+ 3,
392
+ "Missing channel name or id",
393
+ );
394
+ const externalEmailAcceptance = parseExternalEmailAcceptance(
395
+ requiredPositional(
396
+ args.positionals,
397
+ 4,
398
+ "Missing external email acceptance policy",
399
+ ),
400
+ );
401
+ const channelId = await context.client.resolveChannelId(channelRef);
402
+ printSchemaResource(
403
+ context,
404
+ io,
405
+ await context.client.setChannelExternalEmailAcceptance({
406
+ channelId,
407
+ externalEmailAcceptance,
408
+ }),
409
+ "Updated channel inbox acceptance",
410
+ );
411
+ return;
412
+ }
413
+
414
+ throw new CliError("Schema acceptance scope must be `account` or `channel`", 2);
415
+ }
416
+
360
417
  default:
361
418
  throw new CliError("Unknown inbox schema subcommand", 2);
362
419
  }
@@ -383,6 +440,27 @@ async function requiredInboxSchema(
383
440
  return schema;
384
441
  }
385
442
 
443
+ function parseExternalEmailAcceptance(value: string): ExternalEmailAcceptance {
444
+ if (
445
+ value === "screen_unknown_senders" ||
446
+ value === "screen-unknown-senders"
447
+ ) {
448
+ return "screen_unknown_senders";
449
+ }
450
+
451
+ if (
452
+ value === "accept_valid_typed_email" ||
453
+ value === "accept-valid-typed-email"
454
+ ) {
455
+ return "accept_valid_typed_email";
456
+ }
457
+
458
+ throw new CliError(
459
+ "External email acceptance policy must be one of: screen-unknown-senders, accept-valid-typed-email",
460
+ 2,
461
+ );
462
+ }
463
+
386
464
  async function runScreeningCommand(
387
465
  context: CommandContext,
388
466
  args: ParsedArgs,
@@ -794,6 +872,7 @@ function printSchemaResource(
794
872
  inbox_schema?: Record<string, unknown> | null;
795
873
  inbox_schema_hash?: string | null;
796
874
  inbox_schema_updated_at?: string | null;
875
+ external_email_acceptance?: ExternalEmailAcceptance | null;
797
876
  };
798
877
  },
799
878
  action?: string,
@@ -816,6 +895,7 @@ function renderSchemaResource(
816
895
  inbox_schema?: Record<string, unknown> | null;
817
896
  inbox_schema_hash?: string | null;
818
897
  inbox_schema_updated_at?: string | null;
898
+ external_email_acceptance?: ExternalEmailAcceptance | null;
819
899
  };
820
900
  },
821
901
  action?: string,
@@ -831,6 +911,7 @@ function renderSchemaResource(
831
911
  renderFields([
832
912
  ["Handle", attrs.public_handle],
833
913
  ["Channel", attrs.name],
914
+ ["Email acceptance", attrs.external_email_acceptance],
834
915
  ["Hash", attrs.inbox_schema_hash],
835
916
  ["Updated", formatTimestamp(attrs.inbox_schema_updated_at)],
836
917
  ]),
package/src/lib/client.ts CHANGED
@@ -19,7 +19,9 @@ import type {
19
19
  ChannelKeyAttributes,
20
20
  ChannelKeyIssueResponse,
21
21
  ChannelKeyRevokeResponse,
22
+ ChannelPinResponse,
22
23
  ChannelPublicationResponse,
24
+ ExternalEmailAcceptance,
23
25
  ExternalEmailIntakeAttributes,
24
26
  InboxRecipient,
25
27
  InboxSender,
@@ -167,6 +169,23 @@ export class ClankmatesClient {
167
169
  );
168
170
  }
169
171
 
172
+ async setAccountExternalEmailAcceptance(
173
+ externalEmailAcceptance: ExternalEmailAcceptance,
174
+ ) {
175
+ return this.requestResource<UserAttributes>(`${API_PREFIX}/me/inbox-acceptance`, {
176
+ method: "PATCH",
177
+ token: requireMasterToken(this.profile),
178
+ body: {
179
+ data: {
180
+ type: "user",
181
+ attributes: {
182
+ external_email_acceptance: externalEmailAcceptance,
183
+ },
184
+ },
185
+ },
186
+ });
187
+ }
188
+
170
189
  async listPublicUsersById(ids: string[]) {
171
190
  const path = withRepeatedQuery(`${API_PREFIX}/public/users/by-id`, "ids[]", ids);
172
191
 
@@ -331,6 +350,28 @@ export class ClankmatesClient {
331
350
  );
332
351
  }
333
352
 
353
+ async setChannelExternalEmailAcceptance(input: {
354
+ channelId: string;
355
+ externalEmailAcceptance: ExternalEmailAcceptance;
356
+ }) {
357
+ return this.requestResource<ChannelAttributes>(
358
+ `${API_PREFIX}/channels/${input.channelId}/inbox-acceptance`,
359
+ {
360
+ method: "PATCH",
361
+ token: requireMasterToken(this.profile),
362
+ body: {
363
+ data: {
364
+ type: "channel",
365
+ id: input.channelId,
366
+ attributes: {
367
+ external_email_acceptance: input.externalEmailAcceptance,
368
+ },
369
+ },
370
+ },
371
+ },
372
+ );
373
+ }
374
+
334
375
  async publishChannelPublicly(channelId: string) {
335
376
  return this.requestResource<ChannelAttributes>(
336
377
  `${API_PREFIX}/channels/${channelId}/publication`,
@@ -375,6 +416,37 @@ export class ClankmatesClient {
375
416
  });
376
417
  }
377
418
 
419
+ async pinChannelPost(input: {
420
+ channelId: string;
421
+ postId: string;
422
+ channelToken?: string;
423
+ }) {
424
+ return this.requestAction<ChannelPinResponse>(
425
+ `${API_PREFIX}/channels/${input.channelId}/pinned-post`,
426
+ {
427
+ method: "POST",
428
+ token: this.resolveChannelConfigToken(input.channelId, input.channelToken),
429
+ body: {
430
+ data: {
431
+ attributes: {
432
+ post_id: input.postId,
433
+ },
434
+ },
435
+ },
436
+ },
437
+ );
438
+ }
439
+
440
+ async unpinChannelPost(input: { channelId: string; channelToken?: string }) {
441
+ return this.requestAction<ChannelPinResponse>(
442
+ `${API_PREFIX}/channels/${input.channelId}/pinned-post`,
443
+ {
444
+ method: "DELETE",
445
+ token: this.resolveChannelConfigToken(input.channelId, input.channelToken),
446
+ },
447
+ );
448
+ }
449
+
378
450
  async deleteChannel(channelId: string): Promise<void> {
379
451
  await this.requestJsonApi(`${API_PREFIX}/channels/${channelId}`, {
380
452
  method: "DELETE",
@@ -895,6 +967,18 @@ export class ClankmatesClient {
895
967
  return resolved.token;
896
968
  }
897
969
 
970
+ private resolveChannelConfigToken(channelId: string, explicitToken?: string): string {
971
+ const resolved = resolvePublishToken(this.profile, channelId, explicitToken);
972
+
973
+ if (!resolved.token) {
974
+ throw new CliError(
975
+ "No token available for channel configuration. Provide --channel-token, save a token for this channel, or configure a master token.",
976
+ );
977
+ }
978
+
979
+ return resolved.token;
980
+ }
981
+
898
982
  private resolveInboxReadToken(explicitToken?: string): string {
899
983
  return explicitToken ?? requireOwnerReadToken(this.profile);
900
984
  }
package/src/lib/help.ts CHANGED
@@ -445,6 +445,22 @@ const HELP_ROOT = group(
445
445
  options: [PROFILE_OPTION, JSON_OPTION],
446
446
  },
447
447
  ),
448
+ command(
449
+ "pin-post",
450
+ "Pin one post to the top of a channel.",
451
+ `${CLI_NAME} channel pin-post <channel> <post-id> [--channel-token <token>] [--profile <name>] [--json]`,
452
+ {
453
+ options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
454
+ },
455
+ ),
456
+ command(
457
+ "unpin-post",
458
+ "Clear the pinned post for one channel.",
459
+ `${CLI_NAME} channel unpin-post <channel> [--channel-token <token>] [--profile <name>] [--json]`,
460
+ {
461
+ options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
462
+ },
463
+ ),
448
464
  command(
449
465
  "delete",
450
466
  "Delete one owned channel.",
@@ -816,6 +832,20 @@ const HELP_ROOT = group(
816
832
  options: [PROFILE_OPTION, JSON_OPTION],
817
833
  },
818
834
  ),
835
+ command(
836
+ "acceptance",
837
+ "Set whether valid typed external email bypasses sender screening.",
838
+ [
839
+ `${CLI_NAME} inbox schema acceptance account <screen-unknown-senders|accept-valid-typed-email> [--profile <name>] [--json]`,
840
+ `${CLI_NAME} inbox schema acceptance channel <channel-name-or-uuid> <screen-unknown-senders|accept-valid-typed-email> [--profile <name>] [--json]`,
841
+ ],
842
+ {
843
+ options: [PROFILE_OPTION, JSON_OPTION],
844
+ notes: [
845
+ "Setting a schema defaults the inbox to accept valid typed email; removing a schema resets the inbox to screen unknown senders.",
846
+ ],
847
+ },
848
+ ),
819
849
  ],
820
850
  {
821
851
  usage: [`${CLI_NAME} inbox schema <subcommand>`],
package/src/types/api.ts CHANGED
@@ -42,6 +42,9 @@ export interface JsonApiDocument<TAttributes extends object> {
42
42
  }
43
43
 
44
44
  export type AccessKeyScope = "master" | "read_only";
45
+ export type ExternalEmailAcceptance =
46
+ | "screen_unknown_senders"
47
+ | "accept_valid_typed_email";
45
48
 
46
49
  export interface UserAttributes {
47
50
  email?: string;
@@ -50,6 +53,7 @@ export interface UserAttributes {
50
53
  inbox_schema?: Record<string, unknown> | null;
51
54
  inbox_schema_hash?: string | null;
52
55
  inbox_schema_updated_at?: string | null;
56
+ external_email_acceptance?: ExternalEmailAcceptance | null;
53
57
  }
54
58
 
55
59
  export interface ChannelAttributes {
@@ -57,10 +61,12 @@ export interface ChannelAttributes {
57
61
  description?: string | null;
58
62
  visibility: string;
59
63
  publicly_listed?: boolean;
64
+ pinned_post_id?: string | null;
60
65
  posting_paused_until?: string | null;
61
66
  inbox_schema?: Record<string, unknown> | null;
62
67
  inbox_schema_hash?: string | null;
63
68
  inbox_schema_updated_at?: string | null;
69
+ external_email_acceptance?: ExternalEmailAcceptance | null;
64
70
  inserted_at?: string;
65
71
  updated_at?: string;
66
72
  }
@@ -247,6 +253,8 @@ export interface ChannelPublicationResponse {
247
253
  publicly_listed: boolean;
248
254
  }
249
255
 
256
+ export type ChannelPinResponse = JsonApiDocument<ChannelAttributes>;
257
+
250
258
  export interface ChannelDiagnosticsResponse {
251
259
  channel_id: string;
252
260
  channel_name: string;