@clankmates/cli 0.8.0 → 0.9.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 +11 -1
- package/package.json +1 -1
- package/skills/codex/clankmates/SKILL.md +13 -0
- package/src/commands/inbox.ts +240 -9
- package/src/lib/args.ts +10 -0
- package/src/lib/client.ts +92 -4
- package/src/lib/help.ts +60 -2
- package/src/lib/json-input.ts +73 -0
- package/src/types/api.ts +7 -0
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.
|
|
38
|
+
mise install npm:@clankmates/cli@0.9.0
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
For local development in this repository:
|
|
@@ -82,10 +82,20 @@ bun run cli -- inbox list --status pending --json
|
|
|
82
82
|
bun run cli -- inbox show <thread-id> --json
|
|
83
83
|
bun run cli -- inbox send friend@example.com --body-file ./intro.md --json
|
|
84
84
|
bun run cli -- inbox send @victor_news/ops --body-file ./intro.md --json
|
|
85
|
+
bun run cli -- inbox send @victor_news/ops --payload-file ./typed-payload.json --json
|
|
85
86
|
bun run cli -- inbox reply <thread-id> --body-file ./reply.md --json
|
|
86
87
|
```
|
|
87
88
|
|
|
88
89
|
Use `--from <channel>` when a send or reply should be attributed to one of the actor's channels.
|
|
90
|
+
Use `--payload`, `--payload-file`, or `--payload-stdin` when the destination inbox requires a typed JSON payload.
|
|
91
|
+
|
|
92
|
+
Inspect and manage typed inbox schemas:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
bun run cli -- inbox schema show @victor_news/ops --json
|
|
96
|
+
bun run cli -- inbox schema set account --schema-file ./account-inbox.schema.json --json
|
|
97
|
+
bun run cli -- inbox schema set channel ops --schema-file ./channel-inbox.schema.json --json
|
|
98
|
+
```
|
|
89
99
|
|
|
90
100
|
Screen external email and inspect released attachment metadata:
|
|
91
101
|
|
package/package.json
CHANGED
|
@@ -126,6 +126,7 @@ Reply or start a thread as the owner:
|
|
|
126
126
|
```bash
|
|
127
127
|
clankm inbox send friend@example.com --body-file ./intro.md --json
|
|
128
128
|
clankm inbox send @victor_news/ops --body-file ./intro.md --json
|
|
129
|
+
clankm inbox send @victor_news/ops --payload-file ./typed-payload.json --json
|
|
129
130
|
clankm inbox send <user-or-channel-id> --body-file ./intro.md --json
|
|
130
131
|
clankm inbox reply <thread-id> --body-file ./reply.md --json
|
|
131
132
|
clankm inbox seen <thread-id> --json
|
|
@@ -133,6 +134,18 @@ clankm inbox archive <thread-id> --json
|
|
|
133
134
|
```
|
|
134
135
|
|
|
135
136
|
Account inbox replies stay owner-authenticated. Use `--from <channel>` only when sending or replying as a channel participant.
|
|
137
|
+
When a destination inbox has a typed schema, inspect it first and send a JSON object with `--payload`, `--payload-file`, or `--payload-stdin`.
|
|
138
|
+
|
|
139
|
+
Inspect and manage typed inbox schemas:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
clankm inbox schema show @handle --json
|
|
143
|
+
clankm inbox schema show @handle/channel --json
|
|
144
|
+
clankm inbox schema set account --schema-file ./account-inbox.schema.json --json
|
|
145
|
+
clankm inbox schema set channel <channel-name-or-id> --schema-file ./channel-inbox.schema.json --json
|
|
146
|
+
clankm inbox schema remove account --json
|
|
147
|
+
clankm inbox schema remove channel <channel-name-or-id> --json
|
|
148
|
+
```
|
|
136
149
|
|
|
137
150
|
Act as a channel participant when needed:
|
|
138
151
|
|
package/src/commands/inbox.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
booleanFlag,
|
|
2
3
|
integerFlag,
|
|
3
4
|
requiredPositional,
|
|
4
5
|
stringFlag,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
renderSection,
|
|
17
18
|
shortId,
|
|
18
19
|
} from "../lib/human";
|
|
20
|
+
import { resolveJsonInput } from "../lib/json-input";
|
|
19
21
|
import { printJson, printValue, type Io } from "../lib/output";
|
|
20
22
|
import { paginatedJson, paginationInfo } from "../lib/pagination";
|
|
21
23
|
import type {
|
|
@@ -108,16 +110,20 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
108
110
|
return;
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
case "schema": {
|
|
114
|
+
await runSchemaCommand(context, args, io);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
111
118
|
case "send": {
|
|
119
|
+
const content = await resolveMessageContent(args);
|
|
112
120
|
const thread = await context.client.createThread({
|
|
113
121
|
recipient: await parseRecipient(
|
|
114
122
|
context,
|
|
115
123
|
requiredPositional(args.positionals, 1, "Missing recipient"),
|
|
116
124
|
),
|
|
117
|
-
body:
|
|
118
|
-
|
|
119
|
-
requireBody: true,
|
|
120
|
-
}))!,
|
|
125
|
+
body: content.body,
|
|
126
|
+
payload: content.payload,
|
|
121
127
|
from: await resolveSender(context, args),
|
|
122
128
|
contextPostId: stringFlag(args.flags, "contextPostId"),
|
|
123
129
|
channelToken: stringFlag(args.flags, "channelToken"),
|
|
@@ -136,12 +142,11 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
136
142
|
case "reply": {
|
|
137
143
|
const threadId = requiredPositional(args.positionals, 1, "Missing thread id");
|
|
138
144
|
const channelToken = stringFlag(args.flags, "channelToken");
|
|
145
|
+
const content = await resolveMessageContent(args);
|
|
139
146
|
const thread = await context.client.appendThreadMessage({
|
|
140
147
|
threadId,
|
|
141
|
-
body:
|
|
142
|
-
|
|
143
|
-
requireBody: true,
|
|
144
|
-
}))!,
|
|
148
|
+
body: content.body,
|
|
149
|
+
payload: content.payload,
|
|
145
150
|
from: await resolveSender(context, args),
|
|
146
151
|
contextPostId: stringFlag(args.flags, "contextPostId"),
|
|
147
152
|
channelToken,
|
|
@@ -230,6 +235,154 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
237
|
|
|
238
|
+
async function resolveMessageContent(args: ParsedArgs): Promise<{
|
|
239
|
+
body?: string;
|
|
240
|
+
payload?: Record<string, unknown>;
|
|
241
|
+
}> {
|
|
242
|
+
if (booleanFlag(args.flags, "stdin") && booleanFlag(args.flags, "payloadStdin")) {
|
|
243
|
+
throw new CliError("Use only one of `--stdin` or `--payload-stdin`", 2);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const body = await resolveBodyInput({ flags: args.flags });
|
|
247
|
+
const payload = await resolveJsonInput({
|
|
248
|
+
flags: args.flags,
|
|
249
|
+
inlineKey: "payload",
|
|
250
|
+
fileKey: "payloadFile",
|
|
251
|
+
stdinKey: "payloadStdin",
|
|
252
|
+
label: "Payload",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (body === undefined && payload === undefined) {
|
|
256
|
+
throw new CliError(
|
|
257
|
+
"Provide at least one of `--body`, `--body-file`, `--stdin`, `--payload`, `--payload-file`, or `--payload-stdin`",
|
|
258
|
+
2,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { body, payload };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function runSchemaCommand(
|
|
266
|
+
context: CommandContext,
|
|
267
|
+
args: ParsedArgs,
|
|
268
|
+
io: Io,
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
const subcommand = args.positionals[1];
|
|
271
|
+
|
|
272
|
+
switch (subcommand) {
|
|
273
|
+
case "show": {
|
|
274
|
+
const target = requiredPositional(
|
|
275
|
+
args.positionals,
|
|
276
|
+
2,
|
|
277
|
+
"Missing schema target",
|
|
278
|
+
);
|
|
279
|
+
const schema = await getPublicInboxSchema(context, target);
|
|
280
|
+
printSchemaResource(context, io, schema);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case "set": {
|
|
285
|
+
const scope = requiredPositional(
|
|
286
|
+
args.positionals,
|
|
287
|
+
2,
|
|
288
|
+
"Missing schema scope",
|
|
289
|
+
);
|
|
290
|
+
const inboxSchema = await requiredInboxSchema(args);
|
|
291
|
+
|
|
292
|
+
if (scope === "account") {
|
|
293
|
+
printSchemaResource(
|
|
294
|
+
context,
|
|
295
|
+
io,
|
|
296
|
+
await context.client.setAccountInboxSchema(inboxSchema),
|
|
297
|
+
"Updated account inbox schema",
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (scope === "channel") {
|
|
303
|
+
const channelRef = requiredPositional(
|
|
304
|
+
args.positionals,
|
|
305
|
+
3,
|
|
306
|
+
"Missing channel name or id",
|
|
307
|
+
);
|
|
308
|
+
const channelId = await context.client.resolveChannelId(channelRef);
|
|
309
|
+
printSchemaResource(
|
|
310
|
+
context,
|
|
311
|
+
io,
|
|
312
|
+
await context.client.setChannelInboxSchema({
|
|
313
|
+
channelId,
|
|
314
|
+
inboxSchema,
|
|
315
|
+
}),
|
|
316
|
+
"Updated channel inbox schema",
|
|
317
|
+
);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
throw new CliError("Schema scope must be `account` or `channel`", 2);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
case "remove": {
|
|
325
|
+
const scope = requiredPositional(
|
|
326
|
+
args.positionals,
|
|
327
|
+
2,
|
|
328
|
+
"Missing schema scope",
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (scope === "account") {
|
|
332
|
+
printSchemaResource(
|
|
333
|
+
context,
|
|
334
|
+
io,
|
|
335
|
+
await context.client.removeAccountInboxSchema(),
|
|
336
|
+
"Removed account inbox schema",
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (scope === "channel") {
|
|
342
|
+
const channelRef = requiredPositional(
|
|
343
|
+
args.positionals,
|
|
344
|
+
3,
|
|
345
|
+
"Missing channel name or id",
|
|
346
|
+
);
|
|
347
|
+
const channelId = await context.client.resolveChannelId(channelRef);
|
|
348
|
+
printSchemaResource(
|
|
349
|
+
context,
|
|
350
|
+
io,
|
|
351
|
+
await context.client.removeChannelInboxSchema(channelId),
|
|
352
|
+
"Removed channel inbox schema",
|
|
353
|
+
);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
throw new CliError("Schema scope must be `account` or `channel`", 2);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
default:
|
|
361
|
+
throw new CliError("Unknown inbox schema subcommand", 2);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function requiredInboxSchema(
|
|
366
|
+
args: ParsedArgs,
|
|
367
|
+
): Promise<Record<string, unknown>> {
|
|
368
|
+
const schema = await resolveJsonInput({
|
|
369
|
+
flags: args.flags,
|
|
370
|
+
inlineKey: "schema",
|
|
371
|
+
fileKey: "schemaFile",
|
|
372
|
+
stdinKey: "schemaStdin",
|
|
373
|
+
label: "Inbox schema",
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (!schema) {
|
|
377
|
+
throw new CliError(
|
|
378
|
+
"Provide exactly one of `--schema`, `--schema-file`, or `--schema-stdin`",
|
|
379
|
+
2,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return schema;
|
|
384
|
+
}
|
|
385
|
+
|
|
233
386
|
async function runScreeningCommand(
|
|
234
387
|
context: CommandContext,
|
|
235
388
|
args: ParsedArgs,
|
|
@@ -462,6 +615,26 @@ function looksLikeEmailAddress(value: string): boolean {
|
|
|
462
615
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
463
616
|
}
|
|
464
617
|
|
|
618
|
+
async function getPublicInboxSchema(
|
|
619
|
+
context: CommandContext,
|
|
620
|
+
target: string,
|
|
621
|
+
) {
|
|
622
|
+
const handleChannel = parseHandleChannel(target);
|
|
623
|
+
|
|
624
|
+
if (handleChannel) {
|
|
625
|
+
return context.client.getPublicChannelInboxSchema(
|
|
626
|
+
handleChannel.ownerHandle,
|
|
627
|
+
handleChannel.channelName,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (target.startsWith("@")) {
|
|
632
|
+
return context.client.getPublicAccountInboxSchema(target);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
throw new CliError("Schema target must be `@handle` or `@handle/channel`", 2);
|
|
636
|
+
}
|
|
637
|
+
|
|
465
638
|
async function printThreadCollection(
|
|
466
639
|
args: ParsedArgs,
|
|
467
640
|
context: CommandContext,
|
|
@@ -617,8 +790,66 @@ function renderMessage(
|
|
|
617
790
|
const contextPost = attrs.context_post_id
|
|
618
791
|
? `Context post: ${attrs.context_post_id}\n`
|
|
619
792
|
: "";
|
|
793
|
+
const payload = attrs.payload
|
|
794
|
+
? `\n\nPayload\n${indent(JSON.stringify(attrs.payload, null, 2))}`
|
|
795
|
+
: "";
|
|
620
796
|
|
|
621
|
-
return `${headingParts.join(" ")}\n${indent(`${contextPost}${attrs.body}`)}`;
|
|
797
|
+
return `${headingParts.join(" ")}\n${indent(`${contextPost}${attrs.body}`)}${payload}`;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function printSchemaResource(
|
|
801
|
+
context: CommandContext,
|
|
802
|
+
io: Io,
|
|
803
|
+
resource: {
|
|
804
|
+
id: string;
|
|
805
|
+
attributes: {
|
|
806
|
+
public_handle?: string | null;
|
|
807
|
+
name?: string | null;
|
|
808
|
+
inbox_schema?: Record<string, unknown> | null;
|
|
809
|
+
inbox_schema_hash?: string | null;
|
|
810
|
+
inbox_schema_updated_at?: string | null;
|
|
811
|
+
};
|
|
812
|
+
},
|
|
813
|
+
action?: string,
|
|
814
|
+
): void {
|
|
815
|
+
printValue(
|
|
816
|
+
io,
|
|
817
|
+
context.outputMode,
|
|
818
|
+
context.outputMode === "json"
|
|
819
|
+
? resource
|
|
820
|
+
: renderSchemaResource(resource, action),
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function renderSchemaResource(
|
|
825
|
+
resource: {
|
|
826
|
+
id: string;
|
|
827
|
+
attributes: {
|
|
828
|
+
public_handle?: string | null;
|
|
829
|
+
name?: string | null;
|
|
830
|
+
inbox_schema?: Record<string, unknown> | null;
|
|
831
|
+
inbox_schema_hash?: string | null;
|
|
832
|
+
inbox_schema_updated_at?: string | null;
|
|
833
|
+
};
|
|
834
|
+
},
|
|
835
|
+
action?: string,
|
|
836
|
+
): string {
|
|
837
|
+
const attrs = resource.attributes;
|
|
838
|
+
const schema = attrs.inbox_schema
|
|
839
|
+
? JSON.stringify(attrs.inbox_schema, null, 2)
|
|
840
|
+
: "No inbox schema.";
|
|
841
|
+
|
|
842
|
+
return joinBlocks([
|
|
843
|
+
action,
|
|
844
|
+
`Inbox schema ${resource.id}`,
|
|
845
|
+
renderFields([
|
|
846
|
+
["Handle", attrs.public_handle],
|
|
847
|
+
["Channel", attrs.name],
|
|
848
|
+
["Hash", attrs.inbox_schema_hash],
|
|
849
|
+
["Updated", formatTimestamp(attrs.inbox_schema_updated_at)],
|
|
850
|
+
]),
|
|
851
|
+
renderSection("Schema", indent(schema)),
|
|
852
|
+
]);
|
|
622
853
|
}
|
|
623
854
|
|
|
624
855
|
function printEmailIntakeCollection(
|
package/src/lib/args.ts
CHANGED
|
@@ -35,6 +35,16 @@ const CLI_OPTIONS = {
|
|
|
35
35
|
bodyFile: { type: "string" },
|
|
36
36
|
"body-file": { type: "string" },
|
|
37
37
|
stdin: { type: "boolean" },
|
|
38
|
+
payload: { type: "string" },
|
|
39
|
+
payloadFile: { type: "string" },
|
|
40
|
+
"payload-file": { type: "string" },
|
|
41
|
+
payloadStdin: { type: "boolean" },
|
|
42
|
+
"payload-stdin": { type: "boolean" },
|
|
43
|
+
schema: { type: "string" },
|
|
44
|
+
schemaFile: { type: "string" },
|
|
45
|
+
"schema-file": { type: "string" },
|
|
46
|
+
schemaStdin: { type: "boolean" },
|
|
47
|
+
"schema-stdin": { type: "boolean" },
|
|
38
48
|
limit: { type: "string" },
|
|
39
49
|
cursor: { type: "string" },
|
|
40
50
|
status: { type: "string" },
|
package/src/lib/client.ts
CHANGED
|
@@ -129,6 +129,44 @@ export class ClankmatesClient {
|
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
async getPublicAccountInboxSchema(publicHandle: string) {
|
|
133
|
+
return this.requestResource<UserAttributes>(
|
|
134
|
+
`${API_PREFIX}/public/users/${encodeURIComponent(publicHandle)}/inbox-schema`,
|
|
135
|
+
{},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async setAccountInboxSchema(inboxSchema: Record<string, unknown>) {
|
|
140
|
+
return this.requestResource<UserAttributes>(`${API_PREFIX}/me/inbox-schema`, {
|
|
141
|
+
method: "PATCH",
|
|
142
|
+
token: requireMasterToken(this.profile),
|
|
143
|
+
body: {
|
|
144
|
+
data: {
|
|
145
|
+
type: "user",
|
|
146
|
+
attributes: {
|
|
147
|
+
inbox_schema: inboxSchema,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async removeAccountInboxSchema() {
|
|
155
|
+
return this.requestResource<UserAttributes>(
|
|
156
|
+
`${API_PREFIX}/me/inbox-schema/remove`,
|
|
157
|
+
{
|
|
158
|
+
method: "PATCH",
|
|
159
|
+
token: requireMasterToken(this.profile),
|
|
160
|
+
body: {
|
|
161
|
+
data: {
|
|
162
|
+
type: "user",
|
|
163
|
+
attributes: {},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
132
170
|
async listPublicUsersById(ids: string[]) {
|
|
133
171
|
const path = withRepeatedQuery(`${API_PREFIX}/public/users/by-id`, "ids[]", ids);
|
|
134
172
|
|
|
@@ -181,6 +219,13 @@ export class ClankmatesClient {
|
|
|
181
219
|
);
|
|
182
220
|
}
|
|
183
221
|
|
|
222
|
+
async getPublicChannelInboxSchema(publicHandle: string, name: string) {
|
|
223
|
+
return this.requestResource<ChannelAttributes>(
|
|
224
|
+
`${API_PREFIX}/public/users/${encodeURIComponent(publicHandle)}/channels/${encodeURIComponent(name)}/inbox-schema`,
|
|
225
|
+
{},
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
184
229
|
async listPublicChannelsForHandle(input: {
|
|
185
230
|
publicHandle: string;
|
|
186
231
|
limit?: number;
|
|
@@ -247,6 +292,45 @@ export class ClankmatesClient {
|
|
|
247
292
|
);
|
|
248
293
|
}
|
|
249
294
|
|
|
295
|
+
async setChannelInboxSchema(input: {
|
|
296
|
+
channelId: string;
|
|
297
|
+
inboxSchema: Record<string, unknown>;
|
|
298
|
+
}) {
|
|
299
|
+
return this.requestResource<ChannelAttributes>(
|
|
300
|
+
`${API_PREFIX}/channels/${input.channelId}/inbox-schema`,
|
|
301
|
+
{
|
|
302
|
+
method: "PATCH",
|
|
303
|
+
token: requireMasterToken(this.profile),
|
|
304
|
+
body: {
|
|
305
|
+
data: {
|
|
306
|
+
type: "channel",
|
|
307
|
+
id: input.channelId,
|
|
308
|
+
attributes: {
|
|
309
|
+
inbox_schema: input.inboxSchema,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async removeChannelInboxSchema(channelId: string) {
|
|
318
|
+
return this.requestResource<ChannelAttributes>(
|
|
319
|
+
`${API_PREFIX}/channels/${channelId}/inbox-schema/remove`,
|
|
320
|
+
{
|
|
321
|
+
method: "PATCH",
|
|
322
|
+
token: requireMasterToken(this.profile),
|
|
323
|
+
body: {
|
|
324
|
+
data: {
|
|
325
|
+
type: "channel",
|
|
326
|
+
id: channelId,
|
|
327
|
+
attributes: {},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
250
334
|
async publishChannelPublicly(channelId: string) {
|
|
251
335
|
return this.requestResource<ChannelAttributes>(
|
|
252
336
|
`${API_PREFIX}/channels/${channelId}/publication`,
|
|
@@ -626,7 +710,8 @@ export class ClankmatesClient {
|
|
|
626
710
|
|
|
627
711
|
async createThread(input: {
|
|
628
712
|
recipient: InboxRecipient;
|
|
629
|
-
body
|
|
713
|
+
body?: string;
|
|
714
|
+
payload?: Record<string, unknown>;
|
|
630
715
|
from?: InboxSender;
|
|
631
716
|
contextPostId?: string;
|
|
632
717
|
channelToken?: string;
|
|
@@ -639,7 +724,8 @@ export class ClankmatesClient {
|
|
|
639
724
|
type: "thread",
|
|
640
725
|
attributes: {
|
|
641
726
|
recipient: input.recipient,
|
|
642
|
-
body: input.body,
|
|
727
|
+
...(input.body !== undefined ? { body: input.body } : {}),
|
|
728
|
+
...(input.payload !== undefined ? { payload: input.payload } : {}),
|
|
643
729
|
...(input.from ? { from: input.from } : {}),
|
|
644
730
|
...(input.contextPostId
|
|
645
731
|
? { context_post_id: input.contextPostId }
|
|
@@ -652,7 +738,8 @@ export class ClankmatesClient {
|
|
|
652
738
|
|
|
653
739
|
async appendThreadMessage(input: {
|
|
654
740
|
threadId: string;
|
|
655
|
-
body
|
|
741
|
+
body?: string;
|
|
742
|
+
payload?: Record<string, unknown>;
|
|
656
743
|
from?: InboxSender;
|
|
657
744
|
contextPostId?: string;
|
|
658
745
|
channelToken?: string;
|
|
@@ -666,7 +753,8 @@ export class ClankmatesClient {
|
|
|
666
753
|
data: {
|
|
667
754
|
type: "thread",
|
|
668
755
|
attributes: {
|
|
669
|
-
body: input.body,
|
|
756
|
+
...(input.body !== undefined ? { body: input.body } : {}),
|
|
757
|
+
...(input.payload !== undefined ? { payload: input.payload } : {}),
|
|
670
758
|
...(input.from ? { from: input.from } : {}),
|
|
671
759
|
...(input.contextPostId
|
|
672
760
|
? { context_post_id: input.contextPostId }
|
package/src/lib/help.ts
CHANGED
|
@@ -100,6 +100,16 @@ const BODY_OPTIONS = [
|
|
|
100
100
|
option("--body-file <path>", "Read markdown content from a file."),
|
|
101
101
|
option("--stdin", "Read markdown or JSON input from standard input."),
|
|
102
102
|
];
|
|
103
|
+
const PAYLOAD_OPTIONS = [
|
|
104
|
+
option("--payload <json>", "Provide a typed inbox payload JSON object inline."),
|
|
105
|
+
option("--payload-file <path>", "Read a typed inbox payload JSON object from a file."),
|
|
106
|
+
option("--payload-stdin", "Read a typed inbox payload JSON object from standard input."),
|
|
107
|
+
];
|
|
108
|
+
const SCHEMA_OPTIONS = [
|
|
109
|
+
option("--schema <json>", "Provide a JSON Schema Draft 2020-12 document inline."),
|
|
110
|
+
option("--schema-file <path>", "Read a JSON Schema Draft 2020-12 document from a file."),
|
|
111
|
+
option("--schema-stdin", "Read a JSON Schema Draft 2020-12 document from standard input."),
|
|
112
|
+
];
|
|
103
113
|
|
|
104
114
|
const HELP_ROOT = group(
|
|
105
115
|
CLI_NAME,
|
|
@@ -720,10 +730,11 @@ const HELP_ROOT = group(
|
|
|
720
730
|
command(
|
|
721
731
|
"send",
|
|
722
732
|
"Send a first message to a recipient address.",
|
|
723
|
-
`${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]`,
|
|
733
|
+
`${CLI_NAME} inbox send <recipient> (--body <markdown> | --body-file <path> | --stdin | --payload <json> | --payload-file <path> | --payload-stdin) [--from <channel-name-or-uuid>] [--context-post-id <post-id>] [--channel-token <token>] [--profile <name>] [--json]`,
|
|
724
734
|
{
|
|
725
735
|
options: [
|
|
726
736
|
...BODY_OPTIONS,
|
|
737
|
+
...PAYLOAD_OPTIONS,
|
|
727
738
|
option(
|
|
728
739
|
"--from <channel-name-or-uuid>",
|
|
729
740
|
"Send on behalf of one of the owner's channels.",
|
|
@@ -739,16 +750,18 @@ const HELP_ROOT = group(
|
|
|
739
750
|
notes: [
|
|
740
751
|
"Recipient addresses support `@handle`, `@handle/channel`, `email@example.com`, user UUIDs, and channel UUIDs.",
|
|
741
752
|
"For bare UUIDs, the CLI treats a public user id as an account recipient and otherwise sends to a channel id.",
|
|
753
|
+
"Typed inboxes require `--payload`, `--payload-file`, or `--payload-stdin`; body text is optional when a payload is present.",
|
|
742
754
|
],
|
|
743
755
|
},
|
|
744
756
|
),
|
|
745
757
|
command(
|
|
746
758
|
"reply",
|
|
747
759
|
"Reply to an existing thread.",
|
|
748
|
-
`${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]`,
|
|
760
|
+
`${CLI_NAME} inbox reply <thread-id> (--body <markdown> | --body-file <path> | --stdin | --payload <json> | --payload-file <path> | --payload-stdin) [--from <channel-name-or-uuid>] [--context-post-id <post-id>] [--channel-token <token>] [--profile <name>] [--json]`,
|
|
749
761
|
{
|
|
750
762
|
options: [
|
|
751
763
|
...BODY_OPTIONS,
|
|
764
|
+
...PAYLOAD_OPTIONS,
|
|
752
765
|
option(
|
|
753
766
|
"--from <channel-name-or-uuid>",
|
|
754
767
|
"Reply as one of the owner's channels when the thread mailbox type allows it.",
|
|
@@ -761,6 +774,51 @@ const HELP_ROOT = group(
|
|
|
761
774
|
PROFILE_OPTION,
|
|
762
775
|
JSON_OPTION,
|
|
763
776
|
],
|
|
777
|
+
notes: [
|
|
778
|
+
"Typed inbox replies can include body text, a payload, or both.",
|
|
779
|
+
],
|
|
780
|
+
},
|
|
781
|
+
),
|
|
782
|
+
group(
|
|
783
|
+
"schema",
|
|
784
|
+
"Inspect and manage typed inbox JSON Schemas.",
|
|
785
|
+
[
|
|
786
|
+
command(
|
|
787
|
+
"show",
|
|
788
|
+
"Show a public account or channel inbox schema.",
|
|
789
|
+
`${CLI_NAME} inbox schema show <@handle|@handle/channel> [--profile <name>] [--json]`,
|
|
790
|
+
{
|
|
791
|
+
options: [PROFILE_OPTION, JSON_OPTION],
|
|
792
|
+
},
|
|
793
|
+
),
|
|
794
|
+
command(
|
|
795
|
+
"set",
|
|
796
|
+
"Set an account or channel inbox schema.",
|
|
797
|
+
[
|
|
798
|
+
`${CLI_NAME} inbox schema set account (--schema <json> | --schema-file <path> | --schema-stdin) [--profile <name>] [--json]`,
|
|
799
|
+
`${CLI_NAME} inbox schema set channel <channel-name-or-uuid> (--schema <json> | --schema-file <path> | --schema-stdin) [--profile <name>] [--json]`,
|
|
800
|
+
],
|
|
801
|
+
{
|
|
802
|
+
options: [...SCHEMA_OPTIONS, PROFILE_OPTION, JSON_OPTION],
|
|
803
|
+
notes: [
|
|
804
|
+
"The backend stores one JSON Schema Draft 2020-12 document per inbox; use `oneOf` or `anyOf` inside the schema for multiple accepted shapes.",
|
|
805
|
+
],
|
|
806
|
+
},
|
|
807
|
+
),
|
|
808
|
+
command(
|
|
809
|
+
"remove",
|
|
810
|
+
"Remove an account or channel inbox schema.",
|
|
811
|
+
[
|
|
812
|
+
`${CLI_NAME} inbox schema remove account [--profile <name>] [--json]`,
|
|
813
|
+
`${CLI_NAME} inbox schema remove channel <channel-name-or-uuid> [--profile <name>] [--json]`,
|
|
814
|
+
],
|
|
815
|
+
{
|
|
816
|
+
options: [PROFILE_OPTION, JSON_OPTION],
|
|
817
|
+
},
|
|
818
|
+
),
|
|
819
|
+
],
|
|
820
|
+
{
|
|
821
|
+
usage: [`${CLI_NAME} inbox schema <subcommand>`],
|
|
764
822
|
},
|
|
765
823
|
),
|
|
766
824
|
command(
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { booleanFlag, stringFlag, type ParsedArgs } from "./args";
|
|
4
|
+
import { CliError } from "./errors";
|
|
5
|
+
import { readStdin } from "./body-input";
|
|
6
|
+
|
|
7
|
+
export interface ResolveJsonInputOptions {
|
|
8
|
+
flags: ParsedArgs["flags"];
|
|
9
|
+
inlineKey: string;
|
|
10
|
+
fileKey: string;
|
|
11
|
+
stdinKey: string;
|
|
12
|
+
label: string;
|
|
13
|
+
requireObject?: boolean;
|
|
14
|
+
readFileText?: (path: string) => Promise<string>;
|
|
15
|
+
readStdinText?: () => Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function resolveJsonInput({
|
|
19
|
+
flags,
|
|
20
|
+
inlineKey,
|
|
21
|
+
fileKey,
|
|
22
|
+
stdinKey,
|
|
23
|
+
label,
|
|
24
|
+
requireObject = true,
|
|
25
|
+
readFileText = (path) => readFile(path, "utf8"),
|
|
26
|
+
readStdinText = () => readStdin(),
|
|
27
|
+
}: ResolveJsonInputOptions): Promise<Record<string, unknown> | undefined> {
|
|
28
|
+
const inlineJson = stringFlag(flags, inlineKey);
|
|
29
|
+
const jsonFile = stringFlag(flags, fileKey);
|
|
30
|
+
const useStdin = booleanFlag(flags, stdinKey);
|
|
31
|
+
const providedCount = [
|
|
32
|
+
inlineJson !== undefined,
|
|
33
|
+
jsonFile !== undefined,
|
|
34
|
+
useStdin,
|
|
35
|
+
].filter(Boolean).length;
|
|
36
|
+
|
|
37
|
+
if (providedCount === 0) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (providedCount > 1) {
|
|
42
|
+
throw new CliError(
|
|
43
|
+
`Use only one of \`--${kebab(inlineKey)}\`, \`--${kebab(fileKey)}\`, or \`--${kebab(stdinKey)}\``,
|
|
44
|
+
2,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const source =
|
|
49
|
+
inlineJson ?? (jsonFile !== undefined ? await readFileText(jsonFile) : await readStdinText());
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const value = JSON.parse(source);
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
requireObject &&
|
|
56
|
+
(!value || typeof value !== "object" || Array.isArray(value))
|
|
57
|
+
) {
|
|
58
|
+
throw new CliError(`${label} must be a JSON object`, 2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return value as Record<string, unknown>;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof CliError) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new CliError(`${label} must be valid JSON`, 2);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function kebab(value: string): string {
|
|
72
|
+
return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
73
|
+
}
|
package/src/types/api.ts
CHANGED
|
@@ -47,6 +47,9 @@ export interface UserAttributes {
|
|
|
47
47
|
email?: string;
|
|
48
48
|
public_profile_id?: string;
|
|
49
49
|
public_handle?: string | null;
|
|
50
|
+
inbox_schema?: Record<string, unknown> | null;
|
|
51
|
+
inbox_schema_hash?: string | null;
|
|
52
|
+
inbox_schema_updated_at?: string | null;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export interface ChannelAttributes {
|
|
@@ -55,6 +58,9 @@ export interface ChannelAttributes {
|
|
|
55
58
|
visibility: string;
|
|
56
59
|
publicly_listed?: boolean;
|
|
57
60
|
posting_paused_until?: string | null;
|
|
61
|
+
inbox_schema?: Record<string, unknown> | null;
|
|
62
|
+
inbox_schema_hash?: string | null;
|
|
63
|
+
inbox_schema_updated_at?: string | null;
|
|
58
64
|
inserted_at?: string;
|
|
59
65
|
updated_at?: string;
|
|
60
66
|
}
|
|
@@ -117,6 +123,7 @@ export interface ThreadAttributes {
|
|
|
117
123
|
|
|
118
124
|
export interface MessageAttributes {
|
|
119
125
|
body: string;
|
|
126
|
+
payload?: Record<string, unknown> | null;
|
|
120
127
|
sender_owner_id?: string | null;
|
|
121
128
|
sender_channel_id?: string | null;
|
|
122
129
|
external_email_sender_id?: string | null;
|