@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.
- package/README.md +7 -3
- package/package.json +1 -1
- package/skills/codex/clankmates/SKILL.md +4 -3
- package/src/commands/auth/access-keys.ts +206 -0
- package/src/commands/auth.ts +3 -196
- package/src/commands/channel/render.ts +224 -0
- package/src/commands/channel/tokens.ts +145 -0
- package/src/commands/channel/validation.ts +11 -0
- package/src/commands/channel.ts +11 -340
- package/src/commands/doctor/checks.ts +123 -0
- package/src/commands/doctor/render.ts +140 -0
- package/src/commands/doctor/suggestions.ts +42 -0
- package/src/commands/doctor/types.ts +75 -0
- package/src/commands/doctor.ts +12 -371
- package/src/commands/feed.ts +19 -178
- package/src/commands/inbox/content.ts +31 -0
- package/src/commands/inbox/filters.ts +70 -0
- package/src/commands/inbox/messages.ts +69 -0
- package/src/commands/inbox/participants.ts +152 -0
- package/src/commands/inbox/render.ts +13 -0
- package/src/commands/inbox/resource-output.ts +217 -0
- package/src/commands/inbox/schema.ts +185 -0
- package/src/commands/inbox/screening.ts +76 -0
- package/src/commands/inbox/sync-scopes.ts +59 -0
- package/src/commands/inbox/thread-output.ts +344 -0
- package/src/commands/inbox/watch.ts +203 -0
- package/src/commands/inbox.ts +58 -1220
- package/src/commands/post.ts +24 -116
- package/src/lib/args.ts +8 -0
- package/src/lib/cache/scopes.ts +216 -0
- package/src/lib/cache/store.ts +195 -0
- package/src/lib/cache/types.ts +31 -0
- package/src/lib/cache.ts +18 -382
- package/src/lib/client/auth.ts +122 -0
- package/src/lib/client/channel-keys.ts +57 -0
- package/src/lib/client/channels.ts +364 -0
- package/src/lib/client/core.ts +133 -0
- package/src/lib/client/feed.ts +76 -0
- package/src/lib/client/inbox.ts +361 -0
- package/src/lib/client/posts.ts +213 -0
- package/src/lib/client/raw-api.ts +33 -0
- package/src/lib/client/users.ts +88 -0
- package/src/lib/client.ts +197 -894
- package/src/lib/help.ts +66 -9
- package/src/lib/json_api.ts +74 -9
- package/src/lib/pagination.ts +5 -0
- package/src/lib/polling.ts +146 -0
- package/src/lib/post-output.ts +55 -0
- package/src/types/api.ts +1 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
joinBlocks,
|
|
3
|
+
renderFields,
|
|
4
|
+
renderPagination,
|
|
5
|
+
renderTokenAction,
|
|
6
|
+
} from "../../lib/human";
|
|
7
|
+
import { printJson, printValue, type Io } from "../../lib/output";
|
|
8
|
+
import { paginatedJson, paginationInfo } from "../../lib/pagination";
|
|
9
|
+
import type { ParsedArgs } from "../../lib/args";
|
|
10
|
+
import type {
|
|
11
|
+
ChannelAttributes,
|
|
12
|
+
ChannelDiagnosticsResponse,
|
|
13
|
+
ChannelKeyAttributes,
|
|
14
|
+
ChannelKeyIssueResponse,
|
|
15
|
+
ChannelKeyRevokeResponse,
|
|
16
|
+
ChannelPinResponse,
|
|
17
|
+
ChannelPublicationResponse,
|
|
18
|
+
IdResponse,
|
|
19
|
+
ShareTokenResponse,
|
|
20
|
+
} from "../../types/api";
|
|
21
|
+
|
|
22
|
+
export function printChannelCollection(
|
|
23
|
+
args: ParsedArgs,
|
|
24
|
+
outputMode: "json" | "table",
|
|
25
|
+
io: Io,
|
|
26
|
+
response: {
|
|
27
|
+
items: Array<{ id: string; attributes: ChannelAttributes }>;
|
|
28
|
+
nextCursor?: string;
|
|
29
|
+
},
|
|
30
|
+
): void {
|
|
31
|
+
if (outputMode === "json") {
|
|
32
|
+
printJson(
|
|
33
|
+
io,
|
|
34
|
+
paginatedJson(args, {
|
|
35
|
+
items: response.items,
|
|
36
|
+
nextCursor: response.nextCursor,
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
printValue(
|
|
43
|
+
io,
|
|
44
|
+
outputMode,
|
|
45
|
+
response.items.map((item) => formatChannelRow(item)),
|
|
46
|
+
);
|
|
47
|
+
const pagination = paginationInfo(args, response.nextCursor);
|
|
48
|
+
const message = renderPagination(
|
|
49
|
+
pagination?.nextCursor,
|
|
50
|
+
pagination?.nextCommand,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (message) {
|
|
54
|
+
io.stdout(message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function printChannelKeyCollection(
|
|
59
|
+
args: ParsedArgs,
|
|
60
|
+
outputMode: "json" | "table",
|
|
61
|
+
io: Io,
|
|
62
|
+
response: {
|
|
63
|
+
items: Array<{ id: string; attributes: ChannelKeyAttributes }>;
|
|
64
|
+
nextCursor?: string;
|
|
65
|
+
},
|
|
66
|
+
): void {
|
|
67
|
+
if (outputMode === "json") {
|
|
68
|
+
printJson(
|
|
69
|
+
io,
|
|
70
|
+
paginatedJson(args, {
|
|
71
|
+
items: response.items,
|
|
72
|
+
nextCursor: response.nextCursor,
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
printValue(
|
|
79
|
+
io,
|
|
80
|
+
outputMode,
|
|
81
|
+
response.items.map((item) => formatChannelKeyRow(item)),
|
|
82
|
+
);
|
|
83
|
+
const pagination = paginationInfo(args, response.nextCursor);
|
|
84
|
+
const message = renderPagination(
|
|
85
|
+
pagination?.nextCursor,
|
|
86
|
+
pagination?.nextCommand,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (message) {
|
|
90
|
+
io.stdout(message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function formatChannelRecord(channel: {
|
|
95
|
+
id: string;
|
|
96
|
+
attributes: ChannelAttributes;
|
|
97
|
+
}) {
|
|
98
|
+
return {
|
|
99
|
+
id: channel.id,
|
|
100
|
+
name: channel.attributes.name,
|
|
101
|
+
visibility: channel.attributes.visibility,
|
|
102
|
+
publiclyListed: channel.attributes.publicly_listed ?? false,
|
|
103
|
+
description: channel.attributes.description ?? "",
|
|
104
|
+
postingPausedUntil: channel.attributes.posting_paused_until ?? "",
|
|
105
|
+
pinnedPostId: channel.attributes.pinned_post_id ?? "",
|
|
106
|
+
insertedAt: channel.attributes.inserted_at ?? "",
|
|
107
|
+
updatedAt: channel.attributes.updated_at ?? "",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatChannelDiagnostics(
|
|
112
|
+
diagnostics: ChannelDiagnosticsResponse,
|
|
113
|
+
) {
|
|
114
|
+
return {
|
|
115
|
+
channelId: diagnostics.channel_id,
|
|
116
|
+
channelName: diagnostics.channel_name,
|
|
117
|
+
channelDescription: diagnostics.channel_description ?? "",
|
|
118
|
+
stateLabels: diagnostics.state_labels.join(", "),
|
|
119
|
+
activePublishKeyCount: diagnostics.active_publish_key_count,
|
|
120
|
+
lastPostedAt: diagnostics.last_posted_at ?? "",
|
|
121
|
+
postingPausedUntil: diagnostics.posting_paused_until ?? "",
|
|
122
|
+
latestBlockedWriteAt: diagnostics.latest_blocked_write_at ?? "",
|
|
123
|
+
latestBlockedWriteReason:
|
|
124
|
+
diagnostics.latest_blocked_write_reason_label ??
|
|
125
|
+
diagnostics.latest_blocked_write_reason ??
|
|
126
|
+
"",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function renderShareToken(
|
|
131
|
+
title: string,
|
|
132
|
+
response: ShareTokenResponse,
|
|
133
|
+
): string {
|
|
134
|
+
return joinBlocks([
|
|
135
|
+
title,
|
|
136
|
+
renderFields([["Token", response.token]]),
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function renderIdAction(title: string, response: IdResponse): string {
|
|
141
|
+
return joinBlocks([
|
|
142
|
+
title,
|
|
143
|
+
renderFields([["ID", response.id]]),
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function renderChannelPublicationAction(
|
|
148
|
+
title: string,
|
|
149
|
+
response: ChannelPublicationResponse,
|
|
150
|
+
): string {
|
|
151
|
+
return joinBlocks([
|
|
152
|
+
title,
|
|
153
|
+
renderFields([
|
|
154
|
+
["ID", response.id],
|
|
155
|
+
["Name", response.name],
|
|
156
|
+
["Publicly listed", response.publicly_listed],
|
|
157
|
+
]),
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function renderChannelPinAction(
|
|
162
|
+
title: string,
|
|
163
|
+
response: ChannelPinResponse,
|
|
164
|
+
): string {
|
|
165
|
+
const channel = Array.isArray(response.data)
|
|
166
|
+
? response.data[0]
|
|
167
|
+
: response.data;
|
|
168
|
+
|
|
169
|
+
return joinBlocks([
|
|
170
|
+
title,
|
|
171
|
+
renderFields([
|
|
172
|
+
["Channel", channel?.attributes.name ?? ""],
|
|
173
|
+
["Pinned post", channel?.attributes.pinned_post_id ?? ""],
|
|
174
|
+
]),
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function renderChannelKeyIssue(
|
|
179
|
+
title: string,
|
|
180
|
+
response: ChannelKeyIssueResponse,
|
|
181
|
+
): string {
|
|
182
|
+
return renderTokenAction({
|
|
183
|
+
title,
|
|
184
|
+
id: response.id,
|
|
185
|
+
name: response.name,
|
|
186
|
+
token: response.token,
|
|
187
|
+
issuedAt: response.issued_at,
|
|
188
|
+
expiresAt: response.expires_at,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function renderChannelKeyRevoke(
|
|
193
|
+
title: string,
|
|
194
|
+
response: ChannelKeyRevokeResponse,
|
|
195
|
+
): string {
|
|
196
|
+
return joinBlocks([
|
|
197
|
+
title,
|
|
198
|
+
renderFields([
|
|
199
|
+
["ID", response.id],
|
|
200
|
+
["Name", response.name],
|
|
201
|
+
]),
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function formatChannelRow(channel: { id: string; attributes: ChannelAttributes }) {
|
|
206
|
+
return {
|
|
207
|
+
id: channel.id,
|
|
208
|
+
name: channel.attributes.name,
|
|
209
|
+
visibility: channel.attributes.visibility,
|
|
210
|
+
publiclyListed: channel.attributes.publicly_listed ?? false,
|
|
211
|
+
pinnedPostId: channel.attributes.pinned_post_id ?? "",
|
|
212
|
+
description: channel.attributes.description ?? "",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function formatChannelKeyRow(item: { id: string; attributes: ChannelKeyAttributes }) {
|
|
217
|
+
return {
|
|
218
|
+
id: item.id,
|
|
219
|
+
name: item.attributes.name ?? "",
|
|
220
|
+
expiresAt: item.attributes.expires_at ?? "",
|
|
221
|
+
revokedAt: item.attributes.revoked_at ?? "",
|
|
222
|
+
issuedAt: item.attributes.inserted_at ?? "",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
booleanFlag,
|
|
3
|
+
integerFlag,
|
|
4
|
+
requiredPositional,
|
|
5
|
+
requiredStringFlag,
|
|
6
|
+
stringFlag,
|
|
7
|
+
type ParsedArgs,
|
|
8
|
+
} from "../../lib/args";
|
|
9
|
+
import { storeChannelToken, updateProfile } from "../../lib/config";
|
|
10
|
+
import { createCommandContext } from "../../lib/context";
|
|
11
|
+
import { CliError } from "../../lib/errors";
|
|
12
|
+
import { printValue, type Io } from "../../lib/output";
|
|
13
|
+
import type { ChannelKeyIssueResponse } from "../../types/api";
|
|
14
|
+
import {
|
|
15
|
+
printChannelKeyCollection,
|
|
16
|
+
renderChannelKeyIssue,
|
|
17
|
+
renderChannelKeyRevoke,
|
|
18
|
+
} from "./render";
|
|
19
|
+
|
|
20
|
+
export async function runChannelTokenCommand(
|
|
21
|
+
args: ParsedArgs,
|
|
22
|
+
io: Io,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const context = await createCommandContext(args, io);
|
|
25
|
+
const tokenCommand = requiredPositional(
|
|
26
|
+
args.positionals,
|
|
27
|
+
1,
|
|
28
|
+
"Missing channel token subcommand",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
switch (tokenCommand) {
|
|
32
|
+
case "list": {
|
|
33
|
+
const channelId = await context.client.resolveChannelId(
|
|
34
|
+
requiredPositional(args.positionals, 2, "Missing channel"),
|
|
35
|
+
);
|
|
36
|
+
const response = await context.client.listChannelKeys({
|
|
37
|
+
channelId,
|
|
38
|
+
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
39
|
+
cursor: stringFlag(args.flags, "cursor"),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
printChannelKeyCollection(args, context.outputMode, io, response);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case "issue": {
|
|
47
|
+
const channelId = await context.client.resolveChannelId(
|
|
48
|
+
requiredPositional(args.positionals, 2, "Missing channel"),
|
|
49
|
+
);
|
|
50
|
+
const response = await context.client.issueChannelKey({
|
|
51
|
+
channelId,
|
|
52
|
+
name: requiredStringFlag(args.flags, "name"),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await maybeStoreChannelToken(
|
|
56
|
+
args,
|
|
57
|
+
response,
|
|
58
|
+
channelId,
|
|
59
|
+
context.profileName,
|
|
60
|
+
context.configPath,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (booleanFlag(args.flags, "tokenOnly")) {
|
|
64
|
+
io.stdout(response.token);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
printValue(
|
|
69
|
+
io,
|
|
70
|
+
context.outputMode,
|
|
71
|
+
context.outputMode === "json"
|
|
72
|
+
? response
|
|
73
|
+
: renderChannelKeyIssue("Issued channel token", response),
|
|
74
|
+
);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case "revoke": {
|
|
79
|
+
const response = await context.client.revokeChannelKey(
|
|
80
|
+
requiredPositional(args.positionals, 2, "Missing channel key id"),
|
|
81
|
+
);
|
|
82
|
+
await pruneInvalidStoredChannelTokens(
|
|
83
|
+
context.profileName,
|
|
84
|
+
context.profile.channelTokens,
|
|
85
|
+
context.client,
|
|
86
|
+
context.configPath,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
printValue(
|
|
90
|
+
io,
|
|
91
|
+
context.outputMode,
|
|
92
|
+
context.outputMode === "json"
|
|
93
|
+
? response
|
|
94
|
+
: renderChannelKeyRevoke("Revoked channel token", response),
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
default:
|
|
100
|
+
throw new CliError("Unknown channel token subcommand", 2);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function maybeStoreChannelToken(
|
|
105
|
+
args: ParsedArgs,
|
|
106
|
+
response: ChannelKeyIssueResponse,
|
|
107
|
+
channelId: string,
|
|
108
|
+
profileName: string,
|
|
109
|
+
configPath: string,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
if (!booleanFlag(args.flags, "save")) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await storeChannelToken(profileName, channelId, response.token, configPath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function pruneInvalidStoredChannelTokens(
|
|
119
|
+
profileName: string,
|
|
120
|
+
storedTokens: Record<string, { token: string }>,
|
|
121
|
+
client: Awaited<ReturnType<typeof createCommandContext>>["client"],
|
|
122
|
+
configPath: string,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
const invalidChannelIds: string[] = [];
|
|
125
|
+
|
|
126
|
+
for (const [channelId, storedToken] of Object.entries(storedTokens)) {
|
|
127
|
+
if (!(await client.canAuthenticate(storedToken.token))) {
|
|
128
|
+
invalidChannelIds.push(channelId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (invalidChannelIds.length === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await updateProfile(
|
|
137
|
+
profileName,
|
|
138
|
+
(profile) => {
|
|
139
|
+
for (const channelId of invalidChannelIds) {
|
|
140
|
+
delete profile.channelTokens[channelId];
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
configPath,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CliError } from "../../lib/errors";
|
|
2
|
+
|
|
3
|
+
const CHANNEL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
4
|
+
const CHANNEL_NAME_ERROR =
|
|
5
|
+
"Channel names must start with a lowercase letter or digit and then use only lowercase letters, digits, hyphens, or underscores.";
|
|
6
|
+
|
|
7
|
+
export function assertValidChannelName(name: string): void {
|
|
8
|
+
if (!CHANNEL_NAME_PATTERN.test(name)) {
|
|
9
|
+
throw new CliError(CHANNEL_NAME_ERROR, 2);
|
|
10
|
+
}
|
|
11
|
+
}
|