@clankmates/cli 0.11.1 → 0.13.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.
Files changed (49) hide show
  1. package/README.md +7 -3
  2. package/package.json +1 -1
  3. package/skills/codex/clankmates/SKILL.md +4 -3
  4. package/src/commands/auth/access-keys.ts +206 -0
  5. package/src/commands/auth.ts +3 -196
  6. package/src/commands/channel/render.ts +224 -0
  7. package/src/commands/channel/tokens.ts +145 -0
  8. package/src/commands/channel/validation.ts +11 -0
  9. package/src/commands/channel.ts +11 -340
  10. package/src/commands/doctor/checks.ts +123 -0
  11. package/src/commands/doctor/render.ts +140 -0
  12. package/src/commands/doctor/suggestions.ts +42 -0
  13. package/src/commands/doctor/types.ts +75 -0
  14. package/src/commands/doctor.ts +12 -371
  15. package/src/commands/feed.ts +19 -178
  16. package/src/commands/inbox/content.ts +31 -0
  17. package/src/commands/inbox/filters.ts +70 -0
  18. package/src/commands/inbox/messages.ts +69 -0
  19. package/src/commands/inbox/participants.ts +152 -0
  20. package/src/commands/inbox/render.ts +13 -0
  21. package/src/commands/inbox/resource-output.ts +217 -0
  22. package/src/commands/inbox/schema.ts +185 -0
  23. package/src/commands/inbox/screening.ts +76 -0
  24. package/src/commands/inbox/sync-scopes.ts +59 -0
  25. package/src/commands/inbox/thread-output.ts +344 -0
  26. package/src/commands/inbox/watch.ts +203 -0
  27. package/src/commands/inbox.ts +58 -1220
  28. package/src/commands/post.ts +24 -116
  29. package/src/lib/args.ts +8 -0
  30. package/src/lib/cache/scopes.ts +216 -0
  31. package/src/lib/cache/store.ts +195 -0
  32. package/src/lib/cache/types.ts +31 -0
  33. package/src/lib/cache.ts +18 -382
  34. package/src/lib/client/auth.ts +122 -0
  35. package/src/lib/client/channel-keys.ts +57 -0
  36. package/src/lib/client/channels.ts +364 -0
  37. package/src/lib/client/core.ts +133 -0
  38. package/src/lib/client/feed.ts +76 -0
  39. package/src/lib/client/inbox.ts +361 -0
  40. package/src/lib/client/posts.ts +213 -0
  41. package/src/lib/client/raw-api.ts +33 -0
  42. package/src/lib/client/users.ts +88 -0
  43. package/src/lib/client.ts +197 -894
  44. package/src/lib/help.ts +66 -9
  45. package/src/lib/json_api.ts +74 -9
  46. package/src/lib/pagination.ts +5 -0
  47. package/src/lib/polling.ts +146 -0
  48. package/src/lib/post-output.ts +55 -0
  49. package/src/types/api.ts +1 -0
@@ -0,0 +1,217 @@
1
+ import type { ParsedArgs } from "../../lib/args";
2
+ import type { CommandContext } from "../../lib/context";
3
+ import {
4
+ formatTimestamp,
5
+ indent,
6
+ joinBlocks,
7
+ renderFields,
8
+ renderPagination,
9
+ renderSection,
10
+ shortId,
11
+ } from "../../lib/human";
12
+ import { printValue, type Io } from "../../lib/output";
13
+ import { paginatedJson, paginationInfo } from "../../lib/pagination";
14
+ import type {
15
+ ExternalEmailAcceptance,
16
+ ExternalEmailIntakeAttributes,
17
+ MessageAttachmentAttributes,
18
+ } from "../../types/api";
19
+
20
+ export function printSchemaResource(
21
+ context: CommandContext,
22
+ io: Io,
23
+ resource: {
24
+ id: string;
25
+ attributes: {
26
+ public_handle?: string | null;
27
+ name?: string | null;
28
+ inbox_schema?: Record<string, unknown> | null;
29
+ inbox_schema_hash?: string | null;
30
+ inbox_schema_updated_at?: string | null;
31
+ external_email_acceptance?: ExternalEmailAcceptance | null;
32
+ };
33
+ },
34
+ action?: string,
35
+ ): void {
36
+ printValue(
37
+ io,
38
+ context.outputMode,
39
+ context.outputMode === "json"
40
+ ? resource
41
+ : renderSchemaResource(resource, action),
42
+ );
43
+ }
44
+
45
+ export function printEmailIntakeCollection(
46
+ args: ParsedArgs,
47
+ context: CommandContext,
48
+ io: Io,
49
+ response: {
50
+ items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
51
+ nextCursor?: string;
52
+ },
53
+ ): void {
54
+ printValue(
55
+ io,
56
+ context.outputMode,
57
+ context.outputMode === "json"
58
+ ? paginatedJson(args, {
59
+ items: response.items,
60
+ nextCursor: response.nextCursor,
61
+ })
62
+ : renderEmailIntakeCollection(args, response),
63
+ );
64
+ }
65
+
66
+ export function printEmailIntakeAction(
67
+ context: CommandContext,
68
+ io: Io,
69
+ action: string,
70
+ intake: { id: string; attributes: ExternalEmailIntakeAttributes },
71
+ ): void {
72
+ printValue(
73
+ io,
74
+ context.outputMode,
75
+ context.outputMode === "json"
76
+ ? intake
77
+ : joinBlocks([`${action}: ${intake.id}`, renderEmailIntake(intake)]),
78
+ );
79
+ }
80
+
81
+ export function renderAttachmentCollection(
82
+ args: ParsedArgs,
83
+ response: {
84
+ items: Array<{ id: string; attributes: MessageAttachmentAttributes }>;
85
+ nextCursor?: string;
86
+ },
87
+ ): string {
88
+ const body =
89
+ response.items.length === 0
90
+ ? "No attachments."
91
+ : response.items
92
+ .map((attachment) => {
93
+ const attrs = attachment.attributes;
94
+
95
+ return joinBlocks([
96
+ `Attachment ${attachment.id}`,
97
+ renderFields([
98
+ ["Name", attrs.name ?? ""],
99
+ ["Content type", attrs.content_type ?? ""],
100
+ ["Content length", formatOptionalNumber(attrs.content_length)],
101
+ ["Message", formatActor("message", attrs.message_id)],
102
+ ]),
103
+ ]);
104
+ })
105
+ .join("\n\n");
106
+
107
+ const pagination = paginationInfo(args, response.nextCursor);
108
+ return joinBlocks([
109
+ body,
110
+ renderPagination(pagination?.nextCursor, pagination?.nextCommand),
111
+ ]);
112
+ }
113
+
114
+ function renderSchemaResource(
115
+ resource: {
116
+ id: string;
117
+ attributes: {
118
+ public_handle?: string | null;
119
+ name?: string | null;
120
+ inbox_schema?: Record<string, unknown> | null;
121
+ inbox_schema_hash?: string | null;
122
+ inbox_schema_updated_at?: string | null;
123
+ external_email_acceptance?: ExternalEmailAcceptance | null;
124
+ };
125
+ },
126
+ action?: string,
127
+ ): string {
128
+ const attrs = resource.attributes;
129
+ const schema = attrs.inbox_schema
130
+ ? JSON.stringify(attrs.inbox_schema, null, 2)
131
+ : "No inbox schema.";
132
+
133
+ return joinBlocks([
134
+ action,
135
+ `Inbox schema ${resource.id}`,
136
+ renderFields([
137
+ ["Handle", attrs.public_handle],
138
+ ["Channel", attrs.name],
139
+ ["Email acceptance", attrs.external_email_acceptance],
140
+ ["Hash", attrs.inbox_schema_hash],
141
+ ["Updated", formatTimestamp(attrs.inbox_schema_updated_at)],
142
+ ]),
143
+ renderSection("Schema", indent(schema)),
144
+ ]);
145
+ }
146
+
147
+ function renderEmailIntakeCollection(
148
+ args: ParsedArgs,
149
+ response: {
150
+ items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
151
+ nextCursor?: string;
152
+ },
153
+ ): string {
154
+ const body =
155
+ response.items.length === 0
156
+ ? "No email intakes."
157
+ : response.items.map((intake) => renderEmailIntake(intake)).join("\n\n");
158
+
159
+ const pagination = paginationInfo(args, response.nextCursor);
160
+ return joinBlocks([
161
+ body,
162
+ renderPagination(pagination?.nextCursor, pagination?.nextCommand),
163
+ ]);
164
+ }
165
+
166
+ function renderEmailIntake(intake: {
167
+ id: string;
168
+ attributes: ExternalEmailIntakeAttributes;
169
+ }): string {
170
+ const attrs = intake.attributes;
171
+
172
+ return joinBlocks([
173
+ `Email intake ${intake.id}`,
174
+ renderFields([
175
+ ["Subject", attrs.subject ?? ""],
176
+ ["Status", attrs.status ?? ""],
177
+ ["Decision", attrs.decision ?? ""],
178
+ ["Sender", formatActor("email-sender", attrs.external_email_sender_id)],
179
+ ["Target channel", formatActor("channel", attrs.target_channel_id)],
180
+ ["Raw recipient", attrs.raw_recipient ?? ""],
181
+ ["Received count", formatOptionalNumber(attrs.received_count)],
182
+ ["Attachment count", formatOptionalNumber(attrs.attachment_count)],
183
+ ["Attachments withheld", formatOptionalBoolean(attrs.attachments_withheld)],
184
+ ["Last received", formatTimestamp(attrs.last_received_at)],
185
+ ["Released", formatTimestamp(attrs.released_at)],
186
+ ["Released thread", formatActor("thread", attrs.released_thread_id)],
187
+ ]),
188
+ attrs.body ? renderSection("Body", indent(attrs.body)) : "",
189
+ ]);
190
+ }
191
+
192
+ function formatActor(
193
+ kind: "channel" | "email-sender" | "thread" | "message",
194
+ id?: string | null,
195
+ ): string {
196
+ if (!id) {
197
+ return "-";
198
+ }
199
+
200
+ return `${kind}:${shortId(id)}`;
201
+ }
202
+
203
+ function formatOptionalBoolean(value?: boolean | null): string {
204
+ if (value === undefined || value === null) {
205
+ return "";
206
+ }
207
+
208
+ return value ? "yes" : "no";
209
+ }
210
+
211
+ function formatOptionalNumber(value?: number | null): string {
212
+ if (value === undefined || value === null) {
213
+ return "";
214
+ }
215
+
216
+ return String(value);
217
+ }
@@ -0,0 +1,185 @@
1
+ import { requiredPositional, type ParsedArgs } from "../../lib/args";
2
+ import type { CommandContext } from "../../lib/context";
3
+ import { CliError } from "../../lib/errors";
4
+ import { resolveJsonInput } from "../../lib/json-input";
5
+ import type { Io } from "../../lib/output";
6
+ import { parseExternalEmailAcceptance } from "./filters";
7
+ import { getPublicInboxSchema } from "./participants";
8
+ import { printSchemaResource } from "./render";
9
+
10
+ export async function runSchemaCommand(
11
+ context: CommandContext,
12
+ args: ParsedArgs,
13
+ io: Io,
14
+ ): Promise<void> {
15
+ const subcommand = args.positionals[1];
16
+
17
+ switch (subcommand) {
18
+ case "show": {
19
+ const target = requiredPositional(
20
+ args.positionals,
21
+ 2,
22
+ "Missing schema target",
23
+ );
24
+ const schema = await getPublicInboxSchema(context, target);
25
+ printSchemaResource(context, io, schema);
26
+ return;
27
+ }
28
+
29
+ case "set": {
30
+ const scope = requiredPositional(
31
+ args.positionals,
32
+ 2,
33
+ "Missing schema scope",
34
+ );
35
+ const inboxSchema = await requiredInboxSchema(args);
36
+
37
+ if (scope === "account") {
38
+ printSchemaResource(
39
+ context,
40
+ io,
41
+ await context.client.setAccountInboxSchema(inboxSchema),
42
+ "Updated account inbox schema",
43
+ );
44
+ return;
45
+ }
46
+
47
+ if (scope === "channel") {
48
+ const channelRef = requiredPositional(
49
+ args.positionals,
50
+ 3,
51
+ "Missing channel name or id",
52
+ );
53
+ const channelId = await context.client.resolveChannelId(channelRef);
54
+ printSchemaResource(
55
+ context,
56
+ io,
57
+ await context.client.setChannelInboxSchema({
58
+ channelId,
59
+ inboxSchema,
60
+ }),
61
+ "Updated channel inbox schema",
62
+ );
63
+ return;
64
+ }
65
+
66
+ throw new CliError("Schema scope must be `account` or `channel`", 2);
67
+ }
68
+
69
+ case "remove": {
70
+ const scope = requiredPositional(
71
+ args.positionals,
72
+ 2,
73
+ "Missing schema scope",
74
+ );
75
+
76
+ if (scope === "account") {
77
+ printSchemaResource(
78
+ context,
79
+ io,
80
+ await context.client.removeAccountInboxSchema(),
81
+ "Removed account inbox schema",
82
+ );
83
+ return;
84
+ }
85
+
86
+ if (scope === "channel") {
87
+ const channelRef = requiredPositional(
88
+ args.positionals,
89
+ 3,
90
+ "Missing channel name or id",
91
+ );
92
+ const channelId = await context.client.resolveChannelId(channelRef);
93
+ printSchemaResource(
94
+ context,
95
+ io,
96
+ await context.client.removeChannelInboxSchema(channelId),
97
+ "Removed channel inbox schema",
98
+ );
99
+ return;
100
+ }
101
+
102
+ throw new CliError("Schema scope must be `account` or `channel`", 2);
103
+ }
104
+
105
+ case "acceptance": {
106
+ const scope = requiredPositional(
107
+ args.positionals,
108
+ 2,
109
+ "Missing schema acceptance scope",
110
+ );
111
+
112
+ if (scope === "account") {
113
+ const externalEmailAcceptance = parseExternalEmailAcceptance(
114
+ requiredPositional(
115
+ args.positionals,
116
+ 3,
117
+ "Missing external email acceptance policy",
118
+ ),
119
+ );
120
+
121
+ printSchemaResource(
122
+ context,
123
+ io,
124
+ await context.client.setAccountExternalEmailAcceptance(
125
+ externalEmailAcceptance,
126
+ ),
127
+ "Updated account inbox acceptance",
128
+ );
129
+ return;
130
+ }
131
+
132
+ if (scope === "channel") {
133
+ const channelRef = requiredPositional(
134
+ args.positionals,
135
+ 3,
136
+ "Missing channel name or id",
137
+ );
138
+ const externalEmailAcceptance = parseExternalEmailAcceptance(
139
+ requiredPositional(
140
+ args.positionals,
141
+ 4,
142
+ "Missing external email acceptance policy",
143
+ ),
144
+ );
145
+ const channelId = await context.client.resolveChannelId(channelRef);
146
+ printSchemaResource(
147
+ context,
148
+ io,
149
+ await context.client.setChannelExternalEmailAcceptance({
150
+ channelId,
151
+ externalEmailAcceptance,
152
+ }),
153
+ "Updated channel inbox acceptance",
154
+ );
155
+ return;
156
+ }
157
+
158
+ throw new CliError("Schema acceptance scope must be `account` or `channel`", 2);
159
+ }
160
+
161
+ default:
162
+ throw new CliError("Unknown inbox schema subcommand", 2);
163
+ }
164
+ }
165
+
166
+ async function requiredInboxSchema(
167
+ args: ParsedArgs,
168
+ ): Promise<Record<string, unknown>> {
169
+ const schema = await resolveJsonInput({
170
+ flags: args.flags,
171
+ inlineKey: "schema",
172
+ fileKey: "schemaFile",
173
+ stdinKey: "schemaStdin",
174
+ label: "Inbox schema",
175
+ });
176
+
177
+ if (!schema) {
178
+ throw new CliError(
179
+ "Provide exactly one of `--schema`, `--schema-file`, or `--schema-stdin`",
180
+ 2,
181
+ );
182
+ }
183
+
184
+ return schema;
185
+ }
@@ -0,0 +1,76 @@
1
+ import {
2
+ integerFlag,
3
+ requiredPositional,
4
+ stringFlag,
5
+ type ParsedArgs,
6
+ } from "../../lib/args";
7
+ import type { CommandContext } from "../../lib/context";
8
+ import { CliError } from "../../lib/errors";
9
+ import type { Io } from "../../lib/output";
10
+ import { printEmailIntakeAction, printEmailIntakeCollection } from "./render";
11
+
12
+ export async function runScreeningCommand(
13
+ context: CommandContext,
14
+ args: ParsedArgs,
15
+ io: Io,
16
+ ): Promise<void> {
17
+ const subcommand = args.positionals[1];
18
+ const channelToken = stringFlag(args.flags, "channelToken");
19
+
20
+ switch (subcommand) {
21
+ case "list": {
22
+ const response = await context.client.listEmailScreeningIntakes({
23
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
24
+ cursor: stringFlag(args.flags, "cursor"),
25
+ channelToken,
26
+ });
27
+
28
+ printEmailIntakeCollection(args, context, io, response);
29
+ return;
30
+ }
31
+
32
+ case "processing": {
33
+ const response = await context.client.listEmailProcessingIntakes({
34
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
35
+ cursor: stringFlag(args.flags, "cursor"),
36
+ channelToken,
37
+ });
38
+
39
+ printEmailIntakeCollection(args, context, io, response);
40
+ return;
41
+ }
42
+
43
+ case "approve": {
44
+ const intake = await context.client.approveEmailIntake({
45
+ intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
46
+ channelToken,
47
+ });
48
+
49
+ printEmailIntakeAction(context, io, "Approved email intake", intake);
50
+ return;
51
+ }
52
+
53
+ case "approve-once": {
54
+ const intake = await context.client.approveEmailIntakeOnce({
55
+ intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
56
+ channelToken,
57
+ });
58
+
59
+ printEmailIntakeAction(context, io, "Approved email intake once", intake);
60
+ return;
61
+ }
62
+
63
+ case "ignore": {
64
+ const intake = await context.client.ignoreEmailIntake({
65
+ intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
66
+ channelToken,
67
+ });
68
+
69
+ printEmailIntakeAction(context, io, "Ignored email intake", intake);
70
+ return;
71
+ }
72
+
73
+ default:
74
+ throw new CliError("Unknown inbox screening subcommand", 2);
75
+ }
76
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ authenticatedActorKey,
3
+ cacheFlags,
4
+ inboxMessagesScope,
5
+ inboxThreadsScope,
6
+ type CacheScope,
7
+ } from "../../lib/cache";
8
+ import { booleanFlag, stringFlag, type ParsedArgs } from "../../lib/args";
9
+ import type { CommandContext } from "../../lib/context";
10
+ import type {
11
+ MailboxFilter,
12
+ ParticipantScope,
13
+ ThreadStatusFilter,
14
+ } from "../../types/api";
15
+
16
+ export async function maybeInboxThreadsScope(
17
+ args: ParsedArgs,
18
+ context: CommandContext,
19
+ channelToken: string | undefined,
20
+ status: ThreadStatusFilter | undefined,
21
+ mailbox: MailboxFilter | undefined,
22
+ participantScope: ParticipantScope | undefined,
23
+ ): Promise<CacheScope | undefined> {
24
+ if (!cacheFlags(args).sinceCache && !cacheFlags(args).saveCache) {
25
+ return undefined;
26
+ }
27
+
28
+ return inboxThreadsScope({
29
+ context,
30
+ actorKey: await authenticatedActorKey(context, channelToken),
31
+ status,
32
+ mailbox,
33
+ participant: stringFlag(args.flags, "participant"),
34
+ participantScope,
35
+ query: stringFlag(args.flags, "query"),
36
+ hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
37
+ before: stringFlag(args.flags, "before"),
38
+ });
39
+ }
40
+
41
+ export async function maybeInboxMessagesScope(
42
+ args: ParsedArgs,
43
+ context: CommandContext,
44
+ channelToken: string | undefined,
45
+ threadId: string,
46
+ ): Promise<CacheScope | undefined> {
47
+ if (!cacheFlags(args).sinceCache && !cacheFlags(args).saveCache) {
48
+ return undefined;
49
+ }
50
+
51
+ return inboxMessagesScope({
52
+ context,
53
+ actorKey: await authenticatedActorKey(context, channelToken),
54
+ threadId,
55
+ query: stringFlag(args.flags, "query"),
56
+ hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
57
+ before: stringFlag(args.flags, "before"),
58
+ });
59
+ }