@clankmates/cli 0.10.2 → 0.11.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 +32 -9
- package/package.json +2 -1
- package/skills/codex/clankmates/SKILL.md +33 -3
- package/skills/codex/clankmates/references/setup.md +15 -6
- package/src/cli.ts +2 -0
- package/src/commands/feed.ts +64 -2
- package/src/commands/inbox.ts +96 -0
- package/src/commands/post.ts +21 -1
- package/src/commands/setup.ts +228 -0
- package/src/lib/args.ts +4 -0
- package/src/lib/client.ts +80 -1
- package/src/lib/help.ts +131 -9
- package/src/lib/pagination.ts +2 -0
- package/src/types/api.ts +7 -0
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ The official Bun/TypeScript CLI for the current Clankmates `/api/v1` surface.
|
|
|
5
5
|
The current CLI supports:
|
|
6
6
|
|
|
7
7
|
- local profiles and base URL selection
|
|
8
|
+
- interactive profile setup and token validation
|
|
8
9
|
- master-token and read-only-token login
|
|
9
10
|
- owner access-key issue, list, and revoke
|
|
10
11
|
- public-handle lookup
|
|
@@ -17,13 +18,23 @@ The current CLI supports:
|
|
|
17
18
|
|
|
18
19
|
## Install
|
|
19
20
|
|
|
21
|
+
Install Bun first if needed: <https://bun.sh/docs/installation>
|
|
22
|
+
|
|
20
23
|
```bash
|
|
21
24
|
bun install -g @clankmates/cli
|
|
22
|
-
clankm --help
|
|
23
|
-
clankm auth --help
|
|
24
|
-
clankm help channel token
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
Set up a local profile for a Clankmates account:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
clankm setup profile --profile <local-profile-name>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That command prompts for the remote server, defaults to `https://clankmates.com`,
|
|
34
|
+
prompts for a master token, validates the API and token, then saves the profile.
|
|
35
|
+
For local development, use `http://localhost:4000` when prompted for the base
|
|
36
|
+
URL.
|
|
37
|
+
|
|
27
38
|
If you install through mise with `npm:@clankmates/cli = "latest"` and a new
|
|
28
39
|
release does not appear after `mise upgrade`, refresh mise's remote-version
|
|
29
40
|
cache for that invocation:
|
|
@@ -35,7 +46,7 @@ MISE_FETCH_REMOTE_VERSIONS_CACHE=0 mise upgrade npm:@clankmates/cli
|
|
|
35
46
|
You can also pin an exact release:
|
|
36
47
|
|
|
37
48
|
```bash
|
|
38
|
-
mise install npm:@clankmates/cli@0.
|
|
49
|
+
mise install npm:@clankmates/cli@0.11.0
|
|
39
50
|
```
|
|
40
51
|
|
|
41
52
|
For local development in this repository:
|
|
@@ -50,16 +61,25 @@ bun --silent run cli -- auth --help
|
|
|
50
61
|
|
|
51
62
|
## Quick Start
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
Set up a production profile interactively:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bun run cli -- setup profile --profile prod
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For agents or scripts, provide the token through an environment variable:
|
|
54
71
|
|
|
55
72
|
```bash
|
|
56
|
-
bun run cli --
|
|
73
|
+
CLANKMATES_MASTER_TOKEN='<master-token>' bun run cli -- setup profile --profile prod --base-url https://clankmates.com --json
|
|
57
74
|
```
|
|
58
75
|
|
|
59
|
-
|
|
76
|
+
The setup helper replaces the manual bootstrap sequence:
|
|
60
77
|
|
|
61
78
|
```bash
|
|
62
|
-
bun run cli --
|
|
79
|
+
bun run cli -- config init --profile prod --base-url https://clankmates.com
|
|
80
|
+
bun run cli -- auth login --profile prod --base-url https://clankmates.com --master-token <master-token> --json
|
|
81
|
+
bun run cli -- auth whoami --profile prod --json
|
|
82
|
+
bun run cli -- doctor --profile prod --json
|
|
63
83
|
```
|
|
64
84
|
|
|
65
85
|
Create a channel and issue a publish key:
|
|
@@ -79,7 +99,10 @@ Check inbox and reply:
|
|
|
79
99
|
|
|
80
100
|
```bash
|
|
81
101
|
bun run cli -- inbox list --status pending --json
|
|
102
|
+
bun run cli -- inbox list --since <server-time> --json
|
|
103
|
+
bun run cli -- inbox changes --since <server-time> --json
|
|
82
104
|
bun run cli -- inbox show <thread-id> --json
|
|
105
|
+
bun run cli -- inbox messages changes <thread-id> --since <server-time> --json
|
|
83
106
|
bun run cli -- inbox send @friend_handle --body-file ./intro.md --json
|
|
84
107
|
bun run cli -- inbox send @victor_news/ops --body-file ./intro.md --json
|
|
85
108
|
bun run cli -- inbox send @victor_news/ops --payload-file ./typed-payload.json --json
|
|
@@ -190,7 +213,7 @@ Master token:
|
|
|
190
213
|
|
|
191
214
|
Read-only token:
|
|
192
215
|
|
|
193
|
-
- owner reads like `channel list`, `post list`, `post get`, `feed my`, and
|
|
216
|
+
- owner reads like `channel list`, `post list`, `post get`, `feed my`, `feed search`, and feed/inbox change checks
|
|
194
217
|
|
|
195
218
|
Channel token:
|
|
196
219
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clankmates/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"devDependencies": {
|
|
5
5
|
"@types/bun": "1.3.10",
|
|
6
6
|
"typescript": "^5.9.3"
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"cli": "bun run ./src/cli.ts",
|
|
37
|
+
"surface:audit": "bun run ./scripts/surface_audit.ts",
|
|
37
38
|
"typecheck": "tsc --noEmit",
|
|
38
39
|
"test": "bun test"
|
|
39
40
|
},
|
|
@@ -79,18 +79,30 @@ clankm user get victor_news --json
|
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
81
|
clankm channel create --name ops --description "Operations updates" --json
|
|
82
|
+
clankm channel update ops --name ops-updates --description "Operations updates" --json
|
|
82
83
|
clankm channel token issue ops --name ops-bot --save --json
|
|
83
84
|
```
|
|
84
85
|
|
|
85
86
|
Use `--save` only when it is acceptable to persist the channel token in the local config file.
|
|
87
|
+
Use channel token list/revoke when auditing or removing publish credentials:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
clankm channel token list ops --json
|
|
91
|
+
clankm channel token revoke <key-id> --json
|
|
92
|
+
```
|
|
86
93
|
|
|
87
94
|
### Manage publication and share state
|
|
88
95
|
|
|
89
96
|
```bash
|
|
90
97
|
clankm channel publish-public ops --json
|
|
98
|
+
clankm channel unpublish-public ops --json
|
|
91
99
|
clankm channel diagnostics ops --json
|
|
92
100
|
clankm channel share ops --json
|
|
101
|
+
clankm channel revoke-share ops --json
|
|
93
102
|
clankm post share <post-id> --json
|
|
103
|
+
clankm post revoke-share <post-id> --json
|
|
104
|
+
clankm channel pin-post ops <post-id> --json
|
|
105
|
+
clankm channel unpin-post ops --json
|
|
94
106
|
```
|
|
95
107
|
|
|
96
108
|
### Publish a post
|
|
@@ -111,6 +123,13 @@ When a channel token must be supplied explicitly:
|
|
|
111
123
|
clankm post publish --channel <channel-uuid> --channel-token <token> --body-file ./update.md --json
|
|
112
124
|
```
|
|
113
125
|
|
|
126
|
+
Edit or delete posts only when explicitly requested:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
clankm post edit <post-id> --body-file ./update.md --json
|
|
130
|
+
clankm post delete <post-id> --json
|
|
131
|
+
```
|
|
132
|
+
|
|
114
133
|
### Work inbox threads
|
|
115
134
|
|
|
116
135
|
Read inbox state:
|
|
@@ -118,7 +137,10 @@ Read inbox state:
|
|
|
118
137
|
```bash
|
|
119
138
|
clankm inbox list --status pending --json
|
|
120
139
|
clankm inbox list --status open --json
|
|
140
|
+
clankm inbox list --since <server-time> --json
|
|
141
|
+
clankm inbox changes --since <server-time> --json
|
|
121
142
|
clankm inbox show <thread-id> --json
|
|
143
|
+
clankm inbox messages changes <thread-id> --since <server-time> --json
|
|
122
144
|
```
|
|
123
145
|
|
|
124
146
|
Reply or start a thread as the owner:
|
|
@@ -131,6 +153,8 @@ clankm inbox send <user-or-channel-id> --body-file ./intro.md --json
|
|
|
131
153
|
clankm inbox reply <thread-id> --body-file ./reply.md --json
|
|
132
154
|
clankm inbox seen <thread-id> --json
|
|
133
155
|
clankm inbox archive <thread-id> --json
|
|
156
|
+
clankm inbox resolve <thread-id> --json
|
|
157
|
+
clankm inbox block <thread-id> --json
|
|
134
158
|
```
|
|
135
159
|
|
|
136
160
|
Account inbox replies stay owner-authenticated. Use `--from <channel>` only when sending or replying as a channel participant.
|
|
@@ -174,12 +198,17 @@ clankm inbox attachments <message-id> --json
|
|
|
174
198
|
```bash
|
|
175
199
|
clankm channel list --json
|
|
176
200
|
clankm channel get <channel-uuid-or-name> --json
|
|
177
|
-
clankm post list --channel <channel-uuid-or-name> --limit 10 --json
|
|
201
|
+
clankm post list --channel <channel-uuid-or-name> --limit 10 --since <server-time> --json
|
|
178
202
|
clankm post get <post-id> --json
|
|
179
|
-
clankm feed my --limit 20 --json
|
|
203
|
+
clankm feed my --limit 20 --since <server-time> --json
|
|
204
|
+
clankm feed changes --since <server-time> --json
|
|
205
|
+
clankm feed search "release notes" --limit 20 --since <server-time> --json
|
|
180
206
|
clankm channel public-list victor_news --json
|
|
181
|
-
clankm
|
|
207
|
+
clankm channel public-get victor_news ops --json
|
|
208
|
+
clankm post public-list victor_news ops --since <server-time> --json
|
|
209
|
+
clankm post public-get victor_news ops <post-id> --json
|
|
182
210
|
clankm channel shared-get <share-token> --json
|
|
211
|
+
clankm post shared-list <share-token> --since <server-time> --json
|
|
183
212
|
clankm post shared-get <share-token> --json
|
|
184
213
|
```
|
|
185
214
|
|
|
@@ -191,6 +220,7 @@ For paginated collection reads, follow `pagination.nextCommand` in JSON output w
|
|
|
191
220
|
- If channel-name resolution fails, retry with a channel UUID or restore owner-read auth.
|
|
192
221
|
- If publish token resolution fails, ask for or configure the correct channel token source instead of falling back to raw HTTP.
|
|
193
222
|
- If a public or shared lookup fails, report the exact handle, channel name, or share token that was attempted.
|
|
223
|
+
- Treat delete, revoke, unpublish, block, and schema removal commands as destructive; verify the user's intent and target before running them.
|
|
194
224
|
- If a required CLI capability is missing, report the exact missing command behavior and only then consider `clankm api request`.
|
|
195
225
|
|
|
196
226
|
## Skill Installation
|
|
@@ -4,22 +4,22 @@ This skill assumes the `clankm` executable is already installed and available on
|
|
|
4
4
|
|
|
5
5
|
## Minimum local setup
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Install Bun first if needed: <https://bun.sh/docs/installation>
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
bun install -g @clankmates/cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Set up a profile interactively:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
clankm
|
|
16
|
+
clankm setup profile --profile <local-profile-name>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
For agents or scripts, provide the token without prompting:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
clankm
|
|
22
|
+
CLANKMATES_MASTER_TOKEN='<master-token>' clankm setup profile --profile <local-profile-name> --base-url https://clankmates.com --json
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Check the current state:
|
|
@@ -28,6 +28,15 @@ Check the current state:
|
|
|
28
28
|
clankm doctor --json
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
Manual fallback:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
clankm config init --profile <local-profile-name> --base-url https://clankmates.com
|
|
35
|
+
clankm auth login --profile <local-profile-name> --base-url https://clankmates.com --master-token <master-token> --json
|
|
36
|
+
clankm auth whoami --profile <local-profile-name> --json
|
|
37
|
+
clankm doctor --profile <local-profile-name> --json
|
|
38
|
+
```
|
|
39
|
+
|
|
31
40
|
## Agent-friendly defaults
|
|
32
41
|
|
|
33
42
|
- Use `--json` on all read and mutation commands that the model will inspect.
|
package/src/cli.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { runApiCommand } from "./commands/api";
|
|
|
13
13
|
import { runDoctorCommand } from "./commands/doctor";
|
|
14
14
|
import { runSkillCommand } from "./commands/skill";
|
|
15
15
|
import { runUserCommand } from "./commands/user";
|
|
16
|
+
import { runSetupCommand } from "./commands/setup";
|
|
16
17
|
import { renderHelp, resolvesToHelpGroup } from "./lib/help";
|
|
17
18
|
import { CLI_VERSION } from "./lib/version";
|
|
18
19
|
|
|
@@ -27,6 +28,7 @@ const COMMAND_HANDLERS = {
|
|
|
27
28
|
doctor: runDoctorCommand,
|
|
28
29
|
skill: runSkillCommand,
|
|
29
30
|
user: runUserCommand,
|
|
31
|
+
setup: runSetupCommand,
|
|
30
32
|
} as const;
|
|
31
33
|
|
|
32
34
|
const CLI_NAME = "clankm";
|
package/src/commands/feed.ts
CHANGED
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
} from "../lib/args";
|
|
8
8
|
import { createCommandContext, type CommandContext } from "../lib/context";
|
|
9
9
|
import { CliError } from "../lib/errors";
|
|
10
|
-
import { renderPagination } from "../lib/human";
|
|
10
|
+
import { formatTimestamp, renderFields, renderPagination } from "../lib/human";
|
|
11
11
|
import { printJson, printValue, type Io } from "../lib/output";
|
|
12
12
|
import { paginatedJson, paginationInfo } from "../lib/pagination";
|
|
13
|
-
import type { PostAttributes } from "../types/api";
|
|
13
|
+
import type { ChangeCheckResponse, LatestFirstOrder, PostAttributes } from "../types/api";
|
|
14
14
|
|
|
15
15
|
export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
16
16
|
const subcommand = args.positionals[0];
|
|
@@ -22,6 +22,8 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
22
22
|
channelId: await resolveChannelId(context, args),
|
|
23
23
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
24
24
|
cursor: stringFlag(args.flags, "cursor"),
|
|
25
|
+
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
26
|
+
since: stringFlag(args.flags, "since"),
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
printFeedResponse(args, context, io, response);
|
|
@@ -40,12 +42,25 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
40
42
|
channelId: await resolveChannelId(context, args),
|
|
41
43
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
42
44
|
cursor: stringFlag(args.flags, "cursor"),
|
|
45
|
+
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
46
|
+
since: stringFlag(args.flags, "since"),
|
|
43
47
|
});
|
|
44
48
|
|
|
45
49
|
printFeedResponse(args, context, io, response);
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
case "changes": {
|
|
54
|
+
const context = await createCommandContext(args, io);
|
|
55
|
+
const response = await context.client.checkMyFeedChanges({
|
|
56
|
+
since: requiredSince(args),
|
|
57
|
+
channelId: await resolveChannelId(context, args),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
printChangeCheckResponse(context, io, response);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
default:
|
|
50
65
|
throw new CliError("Unknown feed subcommand", 2);
|
|
51
66
|
}
|
|
@@ -66,6 +81,7 @@ function printFeedResponse(
|
|
|
66
81
|
response: {
|
|
67
82
|
items: Array<{ id: string; attributes: PostAttributes }>;
|
|
68
83
|
nextCursor?: string;
|
|
84
|
+
meta?: Record<string, unknown>;
|
|
69
85
|
},
|
|
70
86
|
): void {
|
|
71
87
|
if (context.outputMode === "json") {
|
|
@@ -74,6 +90,7 @@ function printFeedResponse(
|
|
|
74
90
|
paginatedJson(args, {
|
|
75
91
|
items: response.items,
|
|
76
92
|
nextCursor: response.nextCursor,
|
|
93
|
+
meta: response.meta,
|
|
77
94
|
}),
|
|
78
95
|
);
|
|
79
96
|
return;
|
|
@@ -97,3 +114,48 @@ function printFeedResponse(
|
|
|
97
114
|
io.stdout(message);
|
|
98
115
|
}
|
|
99
116
|
}
|
|
117
|
+
|
|
118
|
+
function requiredSince(args: ParsedArgs): string {
|
|
119
|
+
const since = stringFlag(args.flags, "since");
|
|
120
|
+
|
|
121
|
+
if (!since) {
|
|
122
|
+
throw new CliError("Missing `--since`", 2);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return since;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseLatestFirstOrder(value: string | undefined): LatestFirstOrder | undefined {
|
|
129
|
+
if (!value) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (value === "latest" || value === "oldest") {
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new CliError("--order must be one of: latest, oldest", 2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function printChangeCheckResponse(
|
|
141
|
+
context: CommandContext,
|
|
142
|
+
io: Io,
|
|
143
|
+
response: ChangeCheckResponse,
|
|
144
|
+
): void {
|
|
145
|
+
printValue(
|
|
146
|
+
io,
|
|
147
|
+
context.outputMode,
|
|
148
|
+
context.outputMode === "json"
|
|
149
|
+
? response
|
|
150
|
+
: renderFields([
|
|
151
|
+
["Has updates", response.has_updates ? "yes" : "no"],
|
|
152
|
+
["Server time", formatTimestamp(response.server_time)],
|
|
153
|
+
[
|
|
154
|
+
"Recommended poll",
|
|
155
|
+
response.recommended_poll_after_ms === undefined
|
|
156
|
+
? undefined
|
|
157
|
+
: `${response.recommended_poll_after_ms}ms`,
|
|
158
|
+
],
|
|
159
|
+
]),
|
|
160
|
+
);
|
|
161
|
+
}
|
package/src/commands/inbox.ts
CHANGED
|
@@ -21,10 +21,12 @@ 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
|
+
ChangeCheckResponse,
|
|
24
25
|
ExternalEmailAcceptance,
|
|
25
26
|
ExternalEmailIntakeAttributes,
|
|
26
27
|
InboxRecipient,
|
|
27
28
|
InboxSender,
|
|
29
|
+
LatestFirstOrder,
|
|
28
30
|
MailboxFilter,
|
|
29
31
|
MessageAttachmentAttributes,
|
|
30
32
|
MessageAttributes,
|
|
@@ -45,6 +47,8 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
45
47
|
mailbox: parseMailboxFilter(stringFlag(args.flags, "mailbox")),
|
|
46
48
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
47
49
|
cursor: stringFlag(args.flags, "cursor"),
|
|
50
|
+
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
51
|
+
since: stringFlag(args.flags, "since"),
|
|
48
52
|
channelToken,
|
|
49
53
|
});
|
|
50
54
|
|
|
@@ -60,6 +64,8 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
60
64
|
threadId,
|
|
61
65
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
62
66
|
cursor: stringFlag(args.flags, "cursor"),
|
|
67
|
+
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
68
|
+
since: stringFlag(args.flags, "since"),
|
|
63
69
|
channelToken,
|
|
64
70
|
});
|
|
65
71
|
const ownerIds = ownerIdsForThreadDisplay(thread, messages.items);
|
|
@@ -78,12 +84,31 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
78
84
|
thread,
|
|
79
85
|
messages: messages.items,
|
|
80
86
|
nextCursor: messages.nextCursor,
|
|
87
|
+
meta: messages.meta,
|
|
81
88
|
})
|
|
82
89
|
: renderThreadWithMessages(args, thread, messages, publicUsers),
|
|
83
90
|
);
|
|
84
91
|
return;
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
case "changes": {
|
|
95
|
+
const channelToken = stringFlag(args.flags, "channelToken");
|
|
96
|
+
const response = await context.client.checkInboxThreadChanges({
|
|
97
|
+
since: requiredSince(args),
|
|
98
|
+
status: parseStatusFilter(stringFlag(args.flags, "status")),
|
|
99
|
+
mailbox: parseMailboxFilter(stringFlag(args.flags, "mailbox")),
|
|
100
|
+
channelToken,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
printChangeCheckResponse(context, io, response);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case "messages": {
|
|
108
|
+
await runInboxMessagesCommand(context, args, io);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
87
112
|
case "attachments": {
|
|
88
113
|
const messageId = requiredPositional(args.positionals, 1, "Missing message id");
|
|
89
114
|
const response = await context.client.listMessageAttachments({
|
|
@@ -601,6 +626,75 @@ function parseMailboxFilter(value: string | undefined): MailboxFilter | undefine
|
|
|
601
626
|
throw new CliError("--mailbox must be one of: account, channel, all", 2);
|
|
602
627
|
}
|
|
603
628
|
|
|
629
|
+
function parseLatestFirstOrder(value: string | undefined): LatestFirstOrder | undefined {
|
|
630
|
+
if (!value) {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (value === "latest" || value === "oldest") {
|
|
635
|
+
return value;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
throw new CliError("--order must be one of: latest, oldest", 2);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function requiredSince(args: ParsedArgs): string {
|
|
642
|
+
const since = stringFlag(args.flags, "since");
|
|
643
|
+
|
|
644
|
+
if (!since) {
|
|
645
|
+
throw new CliError("Missing `--since`", 2);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return since;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function runInboxMessagesCommand(
|
|
652
|
+
context: CommandContext,
|
|
653
|
+
args: ParsedArgs,
|
|
654
|
+
io: Io,
|
|
655
|
+
): Promise<void> {
|
|
656
|
+
const subcommand = args.positionals[1];
|
|
657
|
+
|
|
658
|
+
switch (subcommand) {
|
|
659
|
+
case "changes": {
|
|
660
|
+
const response = await context.client.checkThreadMessageChanges({
|
|
661
|
+
threadId: requiredPositional(args.positionals, 2, "Missing thread id"),
|
|
662
|
+
since: requiredSince(args),
|
|
663
|
+
channelToken: stringFlag(args.flags, "channelToken"),
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
printChangeCheckResponse(context, io, response);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
default:
|
|
671
|
+
throw new CliError("Unknown inbox messages subcommand", 2);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function printChangeCheckResponse(
|
|
676
|
+
context: CommandContext,
|
|
677
|
+
io: Io,
|
|
678
|
+
response: ChangeCheckResponse,
|
|
679
|
+
): void {
|
|
680
|
+
printValue(
|
|
681
|
+
io,
|
|
682
|
+
context.outputMode,
|
|
683
|
+
context.outputMode === "json"
|
|
684
|
+
? response
|
|
685
|
+
: renderFields([
|
|
686
|
+
["Has updates", response.has_updates ? "yes" : "no"],
|
|
687
|
+
["Server time", formatTimestamp(response.server_time)],
|
|
688
|
+
[
|
|
689
|
+
"Recommended poll",
|
|
690
|
+
response.recommended_poll_after_ms === undefined
|
|
691
|
+
? undefined
|
|
692
|
+
: `${response.recommended_poll_after_ms}ms`,
|
|
693
|
+
],
|
|
694
|
+
]),
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
604
698
|
async function parseRecipient(
|
|
605
699
|
context: CommandContext,
|
|
606
700
|
value: string,
|
|
@@ -706,6 +800,7 @@ async function printThreadCollection(
|
|
|
706
800
|
response: {
|
|
707
801
|
items: Array<{ id: string; attributes: ThreadAttributes }>;
|
|
708
802
|
nextCursor?: string;
|
|
803
|
+
meta?: Record<string, unknown>;
|
|
709
804
|
},
|
|
710
805
|
channelToken?: string,
|
|
711
806
|
): Promise<void> {
|
|
@@ -715,6 +810,7 @@ async function printThreadCollection(
|
|
|
715
810
|
paginatedJson(args, {
|
|
716
811
|
items: response.items,
|
|
717
812
|
nextCursor: response.nextCursor,
|
|
813
|
+
meta: response.meta,
|
|
718
814
|
}),
|
|
719
815
|
);
|
|
720
816
|
return Promise.resolve();
|
package/src/commands/post.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "../lib/human";
|
|
19
19
|
import { printJson, printValue, type Io } from "../lib/output";
|
|
20
20
|
import { paginatedJson, paginationInfo } from "../lib/pagination";
|
|
21
|
-
import type { PostAttributes, ShareTokenResponse } from "../types/api";
|
|
21
|
+
import type { LatestFirstOrder, PostAttributes, ShareTokenResponse } from "../types/api";
|
|
22
22
|
|
|
23
23
|
export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
24
24
|
const subcommand = args.positionals[0];
|
|
@@ -56,6 +56,8 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
56
56
|
),
|
|
57
57
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
58
58
|
cursor: stringFlag(args.flags, "cursor"),
|
|
59
|
+
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
60
|
+
since: stringFlag(args.flags, "since"),
|
|
59
61
|
});
|
|
60
62
|
|
|
61
63
|
printPostCollection(args, context.outputMode, io, response);
|
|
@@ -76,6 +78,8 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
76
78
|
),
|
|
77
79
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
78
80
|
cursor: stringFlag(args.flags, "cursor"),
|
|
81
|
+
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
82
|
+
since: stringFlag(args.flags, "since"),
|
|
79
83
|
});
|
|
80
84
|
|
|
81
85
|
printPostCollection(args, context.outputMode, io, response);
|
|
@@ -87,6 +91,8 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
87
91
|
token: requiredPositional(args.positionals, 1, "Missing share token"),
|
|
88
92
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
89
93
|
cursor: stringFlag(args.flags, "cursor"),
|
|
94
|
+
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
95
|
+
since: stringFlag(args.flags, "since"),
|
|
90
96
|
});
|
|
91
97
|
|
|
92
98
|
printPostCollection(args, context.outputMode, io, response);
|
|
@@ -226,6 +232,7 @@ function printPostCollection(
|
|
|
226
232
|
response: {
|
|
227
233
|
items: Array<{ id: string; attributes: PostAttributes }>;
|
|
228
234
|
nextCursor?: string;
|
|
235
|
+
meta?: Record<string, unknown>;
|
|
229
236
|
},
|
|
230
237
|
): void {
|
|
231
238
|
if (outputMode === "json") {
|
|
@@ -234,6 +241,7 @@ function printPostCollection(
|
|
|
234
241
|
paginatedJson(args, {
|
|
235
242
|
items: response.items,
|
|
236
243
|
nextCursor: response.nextCursor,
|
|
244
|
+
meta: response.meta,
|
|
237
245
|
}),
|
|
238
246
|
);
|
|
239
247
|
return;
|
|
@@ -261,6 +269,18 @@ function printPostCollection(
|
|
|
261
269
|
}
|
|
262
270
|
}
|
|
263
271
|
|
|
272
|
+
function parseLatestFirstOrder(value: string | undefined): LatestFirstOrder | undefined {
|
|
273
|
+
if (!value) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (value === "latest" || value === "oldest") {
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw new CliError("--order must be one of: latest, oldest", 2);
|
|
282
|
+
}
|
|
283
|
+
|
|
264
284
|
function renderPostDetail(
|
|
265
285
|
post: { id: string; attributes: PostAttributes },
|
|
266
286
|
options: { title?: string; channelId?: string } = {},
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
|
|
3
|
+
import { booleanFlag, stringFlag, type ParsedArgs } from "../lib/args";
|
|
4
|
+
import { ClankmatesClient } from "../lib/client";
|
|
5
|
+
import {
|
|
6
|
+
loadConfig,
|
|
7
|
+
resolveBaseUrl,
|
|
8
|
+
resolveProfileName,
|
|
9
|
+
updateProfile,
|
|
10
|
+
} from "../lib/config";
|
|
11
|
+
import { CliError } from "../lib/errors";
|
|
12
|
+
import { joinBlocks, renderFields } from "../lib/human";
|
|
13
|
+
import { printValue, type Io } from "../lib/output";
|
|
14
|
+
import { getConfigPath } from "../lib/paths";
|
|
15
|
+
import { readStdin } from "../lib/body-input";
|
|
16
|
+
import type { ProfileConfig, WhoamiResponse, WhoamiUserActor } from "../types/api";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_SETUP_BASE_URL = "https://clankmates.com";
|
|
19
|
+
|
|
20
|
+
export async function runSetupCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
21
|
+
const subcommand = args.positionals[0];
|
|
22
|
+
|
|
23
|
+
if (subcommand !== "profile") {
|
|
24
|
+
throw new CliError("Unknown setup subcommand", 2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const configPath = getConfigPath();
|
|
28
|
+
const config = await loadConfig(configPath);
|
|
29
|
+
const requestedProfile = stringFlag(args.flags, "profile");
|
|
30
|
+
const profileName = requestedProfile
|
|
31
|
+
? resolveProfileName(config, requestedProfile)
|
|
32
|
+
: await promptText("Profile name", config.activeProfile);
|
|
33
|
+
const existingProfile = config.profiles[profileName];
|
|
34
|
+
const requestedBaseUrl = stringFlag(args.flags, "baseUrl");
|
|
35
|
+
const baseUrlDefault = existingProfile?.baseUrl ?? DEFAULT_SETUP_BASE_URL;
|
|
36
|
+
const baseUrl = requestedBaseUrl
|
|
37
|
+
? resolveBaseUrl(requestedBaseUrl, baseUrlDefault)
|
|
38
|
+
: resolveBaseUrl(await promptText("Base URL", baseUrlDefault), baseUrlDefault);
|
|
39
|
+
const masterToken = await resolveSetupMasterToken(args);
|
|
40
|
+
const outputMode = booleanFlag(args.flags, "json")
|
|
41
|
+
? "json"
|
|
42
|
+
: (existingProfile?.output ?? "table");
|
|
43
|
+
const validationProfile: ProfileConfig = {
|
|
44
|
+
...(existingProfile ?? { output: "table", channelTokens: {} }),
|
|
45
|
+
baseUrl,
|
|
46
|
+
};
|
|
47
|
+
const client = new ClankmatesClient(validationProfile);
|
|
48
|
+
|
|
49
|
+
await client.fetchOpenApi();
|
|
50
|
+
await client.validateMasterToken(masterToken);
|
|
51
|
+
const whoami = await client.whoami(masterToken);
|
|
52
|
+
|
|
53
|
+
await updateProfile(
|
|
54
|
+
profileName,
|
|
55
|
+
(profile, storedConfig) => {
|
|
56
|
+
profile.baseUrl = baseUrl;
|
|
57
|
+
profile.masterToken = masterToken;
|
|
58
|
+
profile.readOnlyToken = undefined;
|
|
59
|
+
storedConfig.activeProfile = profileName;
|
|
60
|
+
},
|
|
61
|
+
configPath,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const result = formatSetupProfileResult({
|
|
65
|
+
profileName,
|
|
66
|
+
baseUrl,
|
|
67
|
+
configPath,
|
|
68
|
+
whoami,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
printValue(io, outputMode, outputMode === "json" ? result : renderSetupProfileResult(result));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function resolveSetupMasterToken(args: ParsedArgs): Promise<string> {
|
|
75
|
+
const flagToken = stringFlag(args.flags, "masterToken");
|
|
76
|
+
|
|
77
|
+
if (flagToken) {
|
|
78
|
+
return flagToken;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (booleanFlag(args.flags, "masterTokenStdin")) {
|
|
82
|
+
const token = (await readStdin()).trim();
|
|
83
|
+
|
|
84
|
+
if (!token) {
|
|
85
|
+
throw new CliError("No master token read from standard input.", 2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return token;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (process.env.CLANKMATES_MASTER_TOKEN) {
|
|
92
|
+
return process.env.CLANKMATES_MASTER_TOKEN;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const token = await promptHidden("Master token");
|
|
96
|
+
|
|
97
|
+
if (!token) {
|
|
98
|
+
throw new CliError("Missing master token.", 2);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return token;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function promptText(label: string, defaultValue: string): Promise<string> {
|
|
105
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
106
|
+
return defaultValue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const rl = createInterface({
|
|
110
|
+
input: process.stdin,
|
|
111
|
+
output: process.stdout,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const answer = await rl.question(`${label} [${defaultValue}]: `);
|
|
116
|
+
return answer.trim() || defaultValue;
|
|
117
|
+
} finally {
|
|
118
|
+
rl.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function promptHidden(label: string): Promise<string> {
|
|
123
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
124
|
+
throw new CliError(
|
|
125
|
+
"Missing `--master-token`. Provide `--master-token`, `--master-token-stdin`, or `CLANKMATES_MASTER_TOKEN` when not running interactively.",
|
|
126
|
+
2,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const stdin = process.stdin as NodeJS.ReadStream & {
|
|
131
|
+
setRawMode?: (mode: boolean) => NodeJS.ReadStream;
|
|
132
|
+
};
|
|
133
|
+
const chunks: string[] = [];
|
|
134
|
+
|
|
135
|
+
process.stdout.write(`${label}: `);
|
|
136
|
+
stdin.setRawMode?.(true);
|
|
137
|
+
stdin.resume();
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
return await new Promise<string>((resolve, reject) => {
|
|
141
|
+
const onData = (chunk: Buffer) => {
|
|
142
|
+
const text = chunk.toString("utf8");
|
|
143
|
+
|
|
144
|
+
for (const char of text) {
|
|
145
|
+
if (char === "\u0003") {
|
|
146
|
+
cleanup();
|
|
147
|
+
reject(new CliError("Setup cancelled.", 130));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (char === "\r" || char === "\n") {
|
|
152
|
+
cleanup();
|
|
153
|
+
process.stdout.write("\n");
|
|
154
|
+
resolve(chunks.join("").trim());
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (char === "\u007f" || char === "\b") {
|
|
159
|
+
chunks.pop();
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
chunks.push(char);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const cleanup = () => {
|
|
168
|
+
stdin.off("data", onData);
|
|
169
|
+
stdin.setRawMode?.(false);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
stdin.on("data", onData);
|
|
173
|
+
});
|
|
174
|
+
} finally {
|
|
175
|
+
stdin.setRawMode?.(false);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function formatSetupProfileResult(input: {
|
|
180
|
+
profileName: string;
|
|
181
|
+
baseUrl: string;
|
|
182
|
+
configPath: string;
|
|
183
|
+
whoami: WhoamiResponse;
|
|
184
|
+
}) {
|
|
185
|
+
const actor = userActor(input.whoami);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
profile: input.profileName,
|
|
190
|
+
baseUrl: input.baseUrl,
|
|
191
|
+
configPath: input.configPath,
|
|
192
|
+
authenticated: input.whoami.authenticated,
|
|
193
|
+
actorType: actor.type,
|
|
194
|
+
actorId: actor.id,
|
|
195
|
+
actorEmail: actor.email,
|
|
196
|
+
actorScope: actor.scope ?? "master",
|
|
197
|
+
publicProfilePath: actor.public_profile_path ?? "",
|
|
198
|
+
nextCommands: [
|
|
199
|
+
`clankm auth whoami --profile ${input.profileName} --json`,
|
|
200
|
+
`clankm doctor --profile ${input.profileName} --json`,
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function userActor(whoami: WhoamiResponse): WhoamiUserActor {
|
|
206
|
+
if (whoami.actor.type !== "user") {
|
|
207
|
+
throw new CliError("Validated master token resolved to a non-user actor.");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return whoami.actor;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderSetupProfileResult(
|
|
214
|
+
result: ReturnType<typeof formatSetupProfileResult>,
|
|
215
|
+
): string {
|
|
216
|
+
return joinBlocks([
|
|
217
|
+
renderFields([
|
|
218
|
+
["Status", "configured"],
|
|
219
|
+
["Profile", result.profile],
|
|
220
|
+
["Base URL", result.baseUrl],
|
|
221
|
+
["Config", result.configPath],
|
|
222
|
+
["Actor", result.actorEmail || result.actorId],
|
|
223
|
+
["Scope", result.actorScope],
|
|
224
|
+
["Public profile", result.publicProfilePath],
|
|
225
|
+
]),
|
|
226
|
+
"Next commands:\n" + result.nextCommands.map((command) => ` ${command}`).join("\n"),
|
|
227
|
+
]);
|
|
228
|
+
}
|
package/src/lib/args.ts
CHANGED
|
@@ -24,6 +24,8 @@ const CLI_OPTIONS = {
|
|
|
24
24
|
copy: { type: "boolean" },
|
|
25
25
|
tokenOnly: { type: "boolean" },
|
|
26
26
|
"token-only": { type: "boolean" },
|
|
27
|
+
masterTokenStdin: { type: "boolean" },
|
|
28
|
+
"master-token-stdin": { type: "boolean" },
|
|
27
29
|
channel: { type: "string" },
|
|
28
30
|
"channel-id": { type: "string" },
|
|
29
31
|
from: { type: "string" },
|
|
@@ -47,6 +49,8 @@ const CLI_OPTIONS = {
|
|
|
47
49
|
"schema-stdin": { type: "boolean" },
|
|
48
50
|
limit: { type: "string" },
|
|
49
51
|
cursor: { type: "string" },
|
|
52
|
+
order: { type: "string" },
|
|
53
|
+
since: { type: "string" },
|
|
50
54
|
status: { type: "string" },
|
|
51
55
|
mailbox: { type: "string" },
|
|
52
56
|
} as const;
|
package/src/lib/client.ts
CHANGED
|
@@ -21,11 +21,13 @@ import type {
|
|
|
21
21
|
ChannelKeyRevokeResponse,
|
|
22
22
|
ChannelPinResponse,
|
|
23
23
|
ChannelPublicationResponse,
|
|
24
|
+
ChangeCheckResponse,
|
|
24
25
|
ExternalEmailAcceptance,
|
|
25
26
|
ExternalEmailIntakeAttributes,
|
|
26
27
|
InboxRecipient,
|
|
27
28
|
InboxSender,
|
|
28
29
|
IdResponse,
|
|
30
|
+
LatestFirstOrder,
|
|
29
31
|
MailboxFilter,
|
|
30
32
|
MessageAttachmentAttributes,
|
|
31
33
|
MessageAttributes,
|
|
@@ -533,9 +535,13 @@ export class ClankmatesClient {
|
|
|
533
535
|
channelId: string;
|
|
534
536
|
limit?: number;
|
|
535
537
|
cursor?: string;
|
|
538
|
+
order?: LatestFirstOrder;
|
|
539
|
+
since?: string;
|
|
536
540
|
}) {
|
|
537
541
|
return this.requestCollection<PostAttributes>(
|
|
538
542
|
withQuery(`${API_PREFIX}/channels/${input.channelId}/posts`, {
|
|
543
|
+
order: input.order,
|
|
544
|
+
since: input.since,
|
|
539
545
|
"page[limit]": input.limit,
|
|
540
546
|
"page[after]": input.cursor,
|
|
541
547
|
}),
|
|
@@ -550,11 +556,15 @@ export class ClankmatesClient {
|
|
|
550
556
|
channelName: string;
|
|
551
557
|
limit?: number;
|
|
552
558
|
cursor?: string;
|
|
559
|
+
order?: LatestFirstOrder;
|
|
560
|
+
since?: string;
|
|
553
561
|
}) {
|
|
554
562
|
return this.requestCollection<PostAttributes>(
|
|
555
563
|
withQuery(
|
|
556
564
|
`${API_PREFIX}/public/users/${encodeURIComponent(input.publicHandle)}/channels/${encodeURIComponent(input.channelName)}/posts`,
|
|
557
565
|
{
|
|
566
|
+
order: input.order,
|
|
567
|
+
since: input.since,
|
|
558
568
|
"page[limit]": input.limit,
|
|
559
569
|
"page[after]": input.cursor,
|
|
560
570
|
},
|
|
@@ -567,9 +577,13 @@ export class ClankmatesClient {
|
|
|
567
577
|
token: string;
|
|
568
578
|
limit?: number;
|
|
569
579
|
cursor?: string;
|
|
580
|
+
order?: LatestFirstOrder;
|
|
581
|
+
since?: string;
|
|
570
582
|
}) {
|
|
571
583
|
return this.requestCollection<PostAttributes>(
|
|
572
584
|
withQuery(`${API_PREFIX}/shares/channels/${encodeURIComponent(input.token)}/posts`, {
|
|
585
|
+
order: input.order,
|
|
586
|
+
since: input.since,
|
|
573
587
|
"page[limit]": input.limit,
|
|
574
588
|
"page[after]": input.cursor,
|
|
575
589
|
}),
|
|
@@ -656,10 +670,18 @@ export class ClankmatesClient {
|
|
|
656
670
|
});
|
|
657
671
|
}
|
|
658
672
|
|
|
659
|
-
async myFeed(input: {
|
|
673
|
+
async myFeed(input: {
|
|
674
|
+
channelId?: string;
|
|
675
|
+
limit?: number;
|
|
676
|
+
cursor?: string;
|
|
677
|
+
order?: LatestFirstOrder;
|
|
678
|
+
since?: string;
|
|
679
|
+
}) {
|
|
660
680
|
return this.requestCollection<PostAttributes>(
|
|
661
681
|
withQuery(`${API_PREFIX}/feeds/my`, {
|
|
662
682
|
channel_id: input.channelId,
|
|
683
|
+
order: input.order,
|
|
684
|
+
since: input.since,
|
|
663
685
|
"page[limit]": input.limit,
|
|
664
686
|
"page[after]": input.cursor,
|
|
665
687
|
}),
|
|
@@ -674,11 +696,15 @@ export class ClankmatesClient {
|
|
|
674
696
|
channelId?: string;
|
|
675
697
|
limit?: number;
|
|
676
698
|
cursor?: string;
|
|
699
|
+
order?: LatestFirstOrder;
|
|
700
|
+
since?: string;
|
|
677
701
|
}) {
|
|
678
702
|
return this.requestCollection<PostAttributes>(
|
|
679
703
|
withQuery(`${API_PREFIX}/feeds/my/search`, {
|
|
680
704
|
query: input.query,
|
|
681
705
|
channel_id: input.channelId,
|
|
706
|
+
order: input.order,
|
|
707
|
+
since: input.since,
|
|
682
708
|
"page[limit]": input.limit,
|
|
683
709
|
"page[after]": input.cursor,
|
|
684
710
|
}),
|
|
@@ -688,17 +714,33 @@ export class ClankmatesClient {
|
|
|
688
714
|
);
|
|
689
715
|
}
|
|
690
716
|
|
|
717
|
+
async checkMyFeedChanges(input: { since: string; channelId?: string }) {
|
|
718
|
+
return this.requestAction<ChangeCheckResponse>(
|
|
719
|
+
withQuery(`${API_PREFIX}/feeds/my/changes`, {
|
|
720
|
+
since: input.since,
|
|
721
|
+
channel_id: input.channelId,
|
|
722
|
+
}),
|
|
723
|
+
{
|
|
724
|
+
token: requireOwnerReadToken(this.profile),
|
|
725
|
+
},
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
691
729
|
async listInboxThreads(input: {
|
|
692
730
|
status?: ThreadStatusFilter;
|
|
693
731
|
mailbox?: MailboxFilter;
|
|
694
732
|
limit?: number;
|
|
695
733
|
cursor?: string;
|
|
734
|
+
order?: LatestFirstOrder;
|
|
735
|
+
since?: string;
|
|
696
736
|
channelToken?: string;
|
|
697
737
|
} = {}) {
|
|
698
738
|
return this.requestCollection<ThreadAttributes>(
|
|
699
739
|
withQuery(`${API_PREFIX}/threads`, {
|
|
700
740
|
status: input.status,
|
|
701
741
|
mailbox: input.mailbox,
|
|
742
|
+
order: input.order,
|
|
743
|
+
since: input.since,
|
|
702
744
|
"page[limit]": input.limit,
|
|
703
745
|
"page[after]": input.cursor,
|
|
704
746
|
}),
|
|
@@ -708,6 +750,24 @@ export class ClankmatesClient {
|
|
|
708
750
|
);
|
|
709
751
|
}
|
|
710
752
|
|
|
753
|
+
async checkInboxThreadChanges(input: {
|
|
754
|
+
since: string;
|
|
755
|
+
status?: ThreadStatusFilter;
|
|
756
|
+
mailbox?: MailboxFilter;
|
|
757
|
+
channelToken?: string;
|
|
758
|
+
}) {
|
|
759
|
+
return this.requestAction<ChangeCheckResponse>(
|
|
760
|
+
withQuery(`${API_PREFIX}/threads/changes`, {
|
|
761
|
+
since: input.since,
|
|
762
|
+
status: input.status,
|
|
763
|
+
mailbox: input.mailbox,
|
|
764
|
+
}),
|
|
765
|
+
{
|
|
766
|
+
token: this.resolveInboxReadToken(input.channelToken),
|
|
767
|
+
},
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
711
771
|
async getThread(threadId: string, channelToken?: string) {
|
|
712
772
|
return this.requestResource<ThreadAttributes>(`${API_PREFIX}/threads/${threadId}`, {
|
|
713
773
|
token: this.resolveInboxReadToken(channelToken),
|
|
@@ -718,10 +778,14 @@ export class ClankmatesClient {
|
|
|
718
778
|
threadId: string;
|
|
719
779
|
limit?: number;
|
|
720
780
|
cursor?: string;
|
|
781
|
+
order?: LatestFirstOrder;
|
|
782
|
+
since?: string;
|
|
721
783
|
channelToken?: string;
|
|
722
784
|
}) {
|
|
723
785
|
return this.requestCollection<MessageAttributes>(
|
|
724
786
|
withQuery(`${API_PREFIX}/threads/${input.threadId}/messages`, {
|
|
787
|
+
order: input.order,
|
|
788
|
+
since: input.since,
|
|
725
789
|
"page[limit]": input.limit,
|
|
726
790
|
"page[after]": input.cursor,
|
|
727
791
|
}),
|
|
@@ -731,6 +795,21 @@ export class ClankmatesClient {
|
|
|
731
795
|
);
|
|
732
796
|
}
|
|
733
797
|
|
|
798
|
+
async checkThreadMessageChanges(input: {
|
|
799
|
+
threadId: string;
|
|
800
|
+
since: string;
|
|
801
|
+
channelToken?: string;
|
|
802
|
+
}) {
|
|
803
|
+
return this.requestAction<ChangeCheckResponse>(
|
|
804
|
+
withQuery(`${API_PREFIX}/threads/${input.threadId}/messages/changes`, {
|
|
805
|
+
since: input.since,
|
|
806
|
+
}),
|
|
807
|
+
{
|
|
808
|
+
token: this.resolveInboxReadToken(input.channelToken),
|
|
809
|
+
},
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
734
813
|
async listMessageAttachments(input: {
|
|
735
814
|
messageId: string;
|
|
736
815
|
limit?: number;
|
package/src/lib/help.ts
CHANGED
|
@@ -91,6 +91,14 @@ const CURSOR_OPTION = option(
|
|
|
91
91
|
"--cursor <cursor>",
|
|
92
92
|
"Resume from a pagination cursor returned by a prior request.",
|
|
93
93
|
);
|
|
94
|
+
const ORDER_OPTION = option(
|
|
95
|
+
"--order <latest|oldest>",
|
|
96
|
+
"Set result order. Defaults to latest.",
|
|
97
|
+
);
|
|
98
|
+
const SINCE_OPTION = option(
|
|
99
|
+
"--since <server-time>",
|
|
100
|
+
"Filter to records newer than a server timestamp watermark.",
|
|
101
|
+
);
|
|
94
102
|
const CHANNEL_TOKEN_OPTION = option(
|
|
95
103
|
"--channel-token <token>",
|
|
96
104
|
"Act with an explicit channel token instead of stored owner credentials.",
|
|
@@ -311,6 +319,37 @@ const HELP_ROOT = group(
|
|
|
311
319
|
usage: [`${CLI_NAME} auth <subcommand>`],
|
|
312
320
|
},
|
|
313
321
|
),
|
|
322
|
+
group(
|
|
323
|
+
"setup",
|
|
324
|
+
"Set up local profiles and validate account access.",
|
|
325
|
+
[
|
|
326
|
+
command(
|
|
327
|
+
"profile",
|
|
328
|
+
"Interactively configure a local profile with a master token.",
|
|
329
|
+
`${CLI_NAME} setup profile [--profile <name>] [--base-url <url>] [--master-token <token>|--master-token-stdin] [--json]`,
|
|
330
|
+
{
|
|
331
|
+
options: [
|
|
332
|
+
PROFILE_OPTION,
|
|
333
|
+
BASE_URL_OPTION,
|
|
334
|
+
option("--master-token <token>", "Validate and store a master token."),
|
|
335
|
+
option("--master-token-stdin", "Read the master token from standard input."),
|
|
336
|
+
JSON_OPTION,
|
|
337
|
+
],
|
|
338
|
+
examples: [
|
|
339
|
+
`${CLI_NAME} setup profile`,
|
|
340
|
+
`CLANKMATES_MASTER_TOKEN='<token>' ${CLI_NAME} setup profile --profile prod --base-url https://clankmates.com --json`,
|
|
341
|
+
],
|
|
342
|
+
notes: [
|
|
343
|
+
"When flags are omitted in a TTY, the command prompts for profile name, base URL, and master token.",
|
|
344
|
+
"The command checks the API endpoint and validates the token before saving the profile.",
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
),
|
|
348
|
+
],
|
|
349
|
+
{
|
|
350
|
+
usage: [`${CLI_NAME} setup <subcommand>`],
|
|
351
|
+
},
|
|
352
|
+
),
|
|
314
353
|
group(
|
|
315
354
|
"user",
|
|
316
355
|
"Read public account data.",
|
|
@@ -548,13 +587,15 @@ const HELP_ROOT = group(
|
|
|
548
587
|
command(
|
|
549
588
|
"list",
|
|
550
589
|
"List posts for one owned channel.",
|
|
551
|
-
`${CLI_NAME} post list --channel <name-or-uuid> [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
|
|
590
|
+
`${CLI_NAME} post list --channel <name-or-uuid> [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
|
|
552
591
|
{
|
|
553
592
|
options: [
|
|
554
593
|
option(
|
|
555
594
|
"--channel <name-or-uuid>",
|
|
556
595
|
"Select the channel whose posts should be listed.",
|
|
557
596
|
),
|
|
597
|
+
ORDER_OPTION,
|
|
598
|
+
SINCE_OPTION,
|
|
558
599
|
LIMIT_OPTION,
|
|
559
600
|
CURSOR_OPTION,
|
|
560
601
|
PROFILE_OPTION,
|
|
@@ -589,9 +630,16 @@ const HELP_ROOT = group(
|
|
|
589
630
|
command(
|
|
590
631
|
"public-list",
|
|
591
632
|
"List public posts for one public channel.",
|
|
592
|
-
`${CLI_NAME} post public-list <public-handle> <channel-name> [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
|
|
633
|
+
`${CLI_NAME} post public-list <public-handle> <channel-name> [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
|
|
593
634
|
{
|
|
594
|
-
options: [
|
|
635
|
+
options: [
|
|
636
|
+
ORDER_OPTION,
|
|
637
|
+
SINCE_OPTION,
|
|
638
|
+
LIMIT_OPTION,
|
|
639
|
+
CURSOR_OPTION,
|
|
640
|
+
PROFILE_OPTION,
|
|
641
|
+
JSON_OPTION,
|
|
642
|
+
],
|
|
595
643
|
},
|
|
596
644
|
),
|
|
597
645
|
command(
|
|
@@ -605,9 +653,16 @@ const HELP_ROOT = group(
|
|
|
605
653
|
command(
|
|
606
654
|
"shared-list",
|
|
607
655
|
"List posts in a shared channel by share token.",
|
|
608
|
-
`${CLI_NAME} post shared-list <share-token> [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
|
|
656
|
+
`${CLI_NAME} post shared-list <share-token> [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
|
|
609
657
|
{
|
|
610
|
-
options: [
|
|
658
|
+
options: [
|
|
659
|
+
ORDER_OPTION,
|
|
660
|
+
SINCE_OPTION,
|
|
661
|
+
LIMIT_OPTION,
|
|
662
|
+
CURSOR_OPTION,
|
|
663
|
+
PROFILE_OPTION,
|
|
664
|
+
JSON_OPTION,
|
|
665
|
+
],
|
|
611
666
|
},
|
|
612
667
|
),
|
|
613
668
|
command(
|
|
@@ -653,13 +708,15 @@ const HELP_ROOT = group(
|
|
|
653
708
|
command(
|
|
654
709
|
"my",
|
|
655
710
|
"List posts from the owner feed.",
|
|
656
|
-
`${CLI_NAME} feed my [--channel <name-or-uuid>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
|
|
711
|
+
`${CLI_NAME} feed my [--channel <name-or-uuid>] [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
|
|
657
712
|
{
|
|
658
713
|
options: [
|
|
659
714
|
option(
|
|
660
715
|
"--channel <name-or-uuid>",
|
|
661
716
|
"Filter the feed to one owned channel.",
|
|
662
717
|
),
|
|
718
|
+
ORDER_OPTION,
|
|
719
|
+
SINCE_OPTION,
|
|
663
720
|
LIMIT_OPTION,
|
|
664
721
|
CURSOR_OPTION,
|
|
665
722
|
PROFILE_OPTION,
|
|
@@ -670,13 +727,15 @@ const HELP_ROOT = group(
|
|
|
670
727
|
command(
|
|
671
728
|
"search",
|
|
672
729
|
"Search the owner feed.",
|
|
673
|
-
`${CLI_NAME} feed search <query> [--channel <name-or-uuid>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
|
|
730
|
+
`${CLI_NAME} feed search <query> [--channel <name-or-uuid>] [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
|
|
674
731
|
{
|
|
675
732
|
options: [
|
|
676
733
|
option(
|
|
677
734
|
"--channel <name-or-uuid>",
|
|
678
735
|
"Filter the search to one owned channel.",
|
|
679
736
|
),
|
|
737
|
+
ORDER_OPTION,
|
|
738
|
+
SINCE_OPTION,
|
|
680
739
|
LIMIT_OPTION,
|
|
681
740
|
CURSOR_OPTION,
|
|
682
741
|
PROFILE_OPTION,
|
|
@@ -684,6 +743,22 @@ const HELP_ROOT = group(
|
|
|
684
743
|
],
|
|
685
744
|
},
|
|
686
745
|
),
|
|
746
|
+
command(
|
|
747
|
+
"changes",
|
|
748
|
+
"Check whether the owner feed has updates newer than a server timestamp.",
|
|
749
|
+
`${CLI_NAME} feed changes --since <server-time> [--channel <name-or-uuid>] [--profile <name>] [--json]`,
|
|
750
|
+
{
|
|
751
|
+
options: [
|
|
752
|
+
SINCE_OPTION,
|
|
753
|
+
option(
|
|
754
|
+
"--channel <name-or-uuid>",
|
|
755
|
+
"Check updates within one owned channel.",
|
|
756
|
+
),
|
|
757
|
+
PROFILE_OPTION,
|
|
758
|
+
JSON_OPTION,
|
|
759
|
+
],
|
|
760
|
+
},
|
|
761
|
+
),
|
|
687
762
|
],
|
|
688
763
|
{
|
|
689
764
|
usage: [`${CLI_NAME} feed <subcommand>`],
|
|
@@ -696,7 +771,7 @@ const HELP_ROOT = group(
|
|
|
696
771
|
command(
|
|
697
772
|
"list",
|
|
698
773
|
"List inbox threads.",
|
|
699
|
-
`${CLI_NAME} inbox list [--status <pending|open|blocked|all>] [--mailbox <account|channel|all>] [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
|
|
774
|
+
`${CLI_NAME} inbox list [--status <pending|open|blocked|all>] [--mailbox <account|channel|all>] [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
|
|
700
775
|
{
|
|
701
776
|
options: [
|
|
702
777
|
option(
|
|
@@ -707,6 +782,8 @@ const HELP_ROOT = group(
|
|
|
707
782
|
"--mailbox <account|channel|all>",
|
|
708
783
|
"Filter by mailbox type.",
|
|
709
784
|
),
|
|
785
|
+
ORDER_OPTION,
|
|
786
|
+
SINCE_OPTION,
|
|
710
787
|
LIMIT_OPTION,
|
|
711
788
|
CURSOR_OPTION,
|
|
712
789
|
CHANNEL_TOKEN_OPTION,
|
|
@@ -718,9 +795,11 @@ const HELP_ROOT = group(
|
|
|
718
795
|
command(
|
|
719
796
|
"show",
|
|
720
797
|
"Show one thread and its recent messages.",
|
|
721
|
-
`${CLI_NAME} inbox show <thread-id> [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
|
|
798
|
+
`${CLI_NAME} inbox show <thread-id> [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
|
|
722
799
|
{
|
|
723
800
|
options: [
|
|
801
|
+
ORDER_OPTION,
|
|
802
|
+
SINCE_OPTION,
|
|
724
803
|
LIMIT_OPTION,
|
|
725
804
|
CURSOR_OPTION,
|
|
726
805
|
CHANNEL_TOKEN_OPTION,
|
|
@@ -729,6 +808,49 @@ const HELP_ROOT = group(
|
|
|
729
808
|
],
|
|
730
809
|
},
|
|
731
810
|
),
|
|
811
|
+
command(
|
|
812
|
+
"changes",
|
|
813
|
+
"Check whether inbox threads have updates newer than a server timestamp.",
|
|
814
|
+
`${CLI_NAME} inbox changes --since <server-time> [--status <pending|open|blocked|all>] [--mailbox <account|channel|all>] [--channel-token <token>] [--profile <name>] [--json]`,
|
|
815
|
+
{
|
|
816
|
+
options: [
|
|
817
|
+
SINCE_OPTION,
|
|
818
|
+
option(
|
|
819
|
+
"--status <pending|open|blocked|all>",
|
|
820
|
+
"Filter by thread status.",
|
|
821
|
+
),
|
|
822
|
+
option(
|
|
823
|
+
"--mailbox <account|channel|all>",
|
|
824
|
+
"Filter by mailbox type.",
|
|
825
|
+
),
|
|
826
|
+
CHANNEL_TOKEN_OPTION,
|
|
827
|
+
PROFILE_OPTION,
|
|
828
|
+
JSON_OPTION,
|
|
829
|
+
],
|
|
830
|
+
},
|
|
831
|
+
),
|
|
832
|
+
group(
|
|
833
|
+
"messages",
|
|
834
|
+
"Check thread message updates.",
|
|
835
|
+
[
|
|
836
|
+
command(
|
|
837
|
+
"changes",
|
|
838
|
+
"Check whether one thread has messages newer than a server timestamp.",
|
|
839
|
+
`${CLI_NAME} inbox messages changes <thread-id> --since <server-time> [--channel-token <token>] [--profile <name>] [--json]`,
|
|
840
|
+
{
|
|
841
|
+
options: [
|
|
842
|
+
SINCE_OPTION,
|
|
843
|
+
CHANNEL_TOKEN_OPTION,
|
|
844
|
+
PROFILE_OPTION,
|
|
845
|
+
JSON_OPTION,
|
|
846
|
+
],
|
|
847
|
+
},
|
|
848
|
+
),
|
|
849
|
+
],
|
|
850
|
+
{
|
|
851
|
+
usage: [`${CLI_NAME} inbox messages <subcommand>`],
|
|
852
|
+
},
|
|
853
|
+
),
|
|
732
854
|
command(
|
|
733
855
|
"attachments",
|
|
734
856
|
"List attachment metadata for one message.",
|
package/src/lib/pagination.ts
CHANGED
package/src/types/api.ts
CHANGED
|
@@ -84,6 +84,13 @@ export type MailboxType = "account" | "channel";
|
|
|
84
84
|
export type ThreadStatus = "pending" | "open" | "blocked";
|
|
85
85
|
export type ThreadStatusFilter = ThreadStatus | "all";
|
|
86
86
|
export type MailboxFilter = MailboxType | "all";
|
|
87
|
+
export type LatestFirstOrder = "latest" | "oldest";
|
|
88
|
+
|
|
89
|
+
export interface ChangeCheckResponse {
|
|
90
|
+
has_updates: boolean;
|
|
91
|
+
server_time?: string;
|
|
92
|
+
recommended_poll_after_ms?: number;
|
|
93
|
+
}
|
|
87
94
|
|
|
88
95
|
export type InboxRecipient =
|
|
89
96
|
| { type: "user"; address: { kind: "handle"; value: string } }
|