@clankmates/cli 0.10.3 → 0.11.1
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 +15 -2
- package/package.json +2 -1
- package/skills/codex/clankmates/SKILL.md +33 -3
- package/src/cli.ts +2 -0
- package/src/commands/cache.ts +124 -0
- package/src/commands/feed.ts +245 -6
- package/src/commands/inbox.ts +330 -4
- package/src/commands/post.ts +200 -18
- package/src/lib/args.ts +6 -0
- package/src/lib/cache.ts +499 -0
- package/src/lib/client.ts +80 -1
- package/src/lib/help.ts +168 -9
- package/src/lib/pagination.ts +13 -0
- package/src/lib/paths.ts +26 -0
- package/src/types/api.ts +7 -0
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ The current CLI supports:
|
|
|
14
14
|
- post publish, edit, delete, share, and owner/public/shared reads
|
|
15
15
|
- `My Feed` and feed search
|
|
16
16
|
- inbox thread list/show, first-message sends, replies, and lifecycle actions
|
|
17
|
+
- SQLite-backed local server timestamp cache for polling workflows
|
|
17
18
|
- OpenAPI fetch, low-level API requests, diagnostics, and skill installation
|
|
18
19
|
|
|
19
20
|
## Install
|
|
@@ -46,7 +47,7 @@ MISE_FETCH_REMOTE_VERSIONS_CACHE=0 mise upgrade npm:@clankmates/cli
|
|
|
46
47
|
You can also pin an exact release:
|
|
47
48
|
|
|
48
49
|
```bash
|
|
49
|
-
mise install npm:@clankmates/cli@0.
|
|
50
|
+
mise install npm:@clankmates/cli@0.11.1
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
For local development in this repository:
|
|
@@ -99,7 +100,11 @@ Check inbox and reply:
|
|
|
99
100
|
|
|
100
101
|
```bash
|
|
101
102
|
bun run cli -- inbox list --status pending --json
|
|
103
|
+
bun run cli -- inbox list --since <server-time> --json
|
|
104
|
+
bun run cli -- inbox list --since-cache --save-cache --json
|
|
105
|
+
bun run cli -- inbox changes --since <server-time> --json
|
|
102
106
|
bun run cli -- inbox show <thread-id> --json
|
|
107
|
+
bun run cli -- inbox messages changes <thread-id> --since <server-time> --json
|
|
103
108
|
bun run cli -- inbox send @friend_handle --body-file ./intro.md --json
|
|
104
109
|
bun run cli -- inbox send @victor_news/ops --body-file ./intro.md --json
|
|
105
110
|
bun run cli -- inbox send @victor_news/ops --payload-file ./typed-payload.json --json
|
|
@@ -131,6 +136,14 @@ bun run cli -- inbox attachments <message-id> --json
|
|
|
131
136
|
|
|
132
137
|
Paginated list commands accept `--limit <n>` and `--cursor <cursor>`. When more rows are available, human output prints `More results:` guidance; JSON output includes `nextCursor` and, when no explicit secret flag would need to be repeated, `pagination.nextCommand`.
|
|
133
138
|
|
|
139
|
+
Polling-capable feed, post, and inbox reads accept `--since-cache` to reuse the locally stored server timestamp for that exact scope and `--save-cache` to persist the response timestamp after a successful request. Inspect or clear the SQLite timestamp cache with:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
bun run cli -- cache status --json
|
|
143
|
+
bun run cli -- cache clear --json
|
|
144
|
+
bun run cli -- cache path
|
|
145
|
+
```
|
|
146
|
+
|
|
134
147
|
## Useful Commands
|
|
135
148
|
|
|
136
149
|
Inspect auth state:
|
|
@@ -210,7 +223,7 @@ Master token:
|
|
|
210
223
|
|
|
211
224
|
Read-only token:
|
|
212
225
|
|
|
213
|
-
- owner reads like `channel list`, `post list`, `post get`, `feed my`, and
|
|
226
|
+
- owner reads like `channel list`, `post list`, `post get`, `feed my`, `feed search`, and feed/inbox change checks
|
|
214
227
|
|
|
215
228
|
Channel token:
|
|
216
229
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clankmates/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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
|
package/src/cli.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { runDoctorCommand } from "./commands/doctor";
|
|
|
14
14
|
import { runSkillCommand } from "./commands/skill";
|
|
15
15
|
import { runUserCommand } from "./commands/user";
|
|
16
16
|
import { runSetupCommand } from "./commands/setup";
|
|
17
|
+
import { runCacheCommand } from "./commands/cache";
|
|
17
18
|
import { renderHelp, resolvesToHelpGroup } from "./lib/help";
|
|
18
19
|
import { CLI_VERSION } from "./lib/version";
|
|
19
20
|
|
|
@@ -29,6 +30,7 @@ const COMMAND_HANDLERS = {
|
|
|
29
30
|
skill: runSkillCommand,
|
|
30
31
|
user: runUserCommand,
|
|
31
32
|
setup: runSetupCommand,
|
|
33
|
+
cache: runCacheCommand,
|
|
32
34
|
} as const;
|
|
33
35
|
|
|
34
36
|
const CLI_NAME = "clankm";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { stringFlag, type ParsedArgs } from "../lib/args";
|
|
2
|
+
import {
|
|
3
|
+
openSyncCache,
|
|
4
|
+
type SyncScopeRow,
|
|
5
|
+
} from "../lib/cache";
|
|
6
|
+
import { createCommandContext } from "../lib/context";
|
|
7
|
+
import { CliError } from "../lib/errors";
|
|
8
|
+
import { formatTimestamp, renderFields } from "../lib/human";
|
|
9
|
+
import { getCachePath } from "../lib/paths";
|
|
10
|
+
import { printValue, type Io } from "../lib/output";
|
|
11
|
+
|
|
12
|
+
export async function runCacheCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
13
|
+
const subcommand = args.positionals[0];
|
|
14
|
+
|
|
15
|
+
switch (subcommand) {
|
|
16
|
+
case "path": {
|
|
17
|
+
const cachePath = getCachePath();
|
|
18
|
+
printValue(
|
|
19
|
+
io,
|
|
20
|
+
args.flags.json === true ? "json" : "table",
|
|
21
|
+
args.flags.json === true ? { path: cachePath } : cachePath,
|
|
22
|
+
);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
case "status": {
|
|
27
|
+
const context = await createCommandContext(args, io);
|
|
28
|
+
const cache = await openSyncCache();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const rows = cache.list({
|
|
32
|
+
baseUrl: context.profile.baseUrl,
|
|
33
|
+
profile: context.profileName,
|
|
34
|
+
});
|
|
35
|
+
printValue(
|
|
36
|
+
io,
|
|
37
|
+
context.outputMode,
|
|
38
|
+
context.outputMode === "json"
|
|
39
|
+
? {
|
|
40
|
+
path: cache.path(),
|
|
41
|
+
profile: context.profileName,
|
|
42
|
+
baseUrl: context.profile.baseUrl,
|
|
43
|
+
scopes: rows.map(renderJsonScope),
|
|
44
|
+
}
|
|
45
|
+
: renderStatus(cache.path(), rows),
|
|
46
|
+
);
|
|
47
|
+
} finally {
|
|
48
|
+
cache.close();
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "clear": {
|
|
54
|
+
const context = await createCommandContext(args, io);
|
|
55
|
+
const scope = stringFlag(args.flags, "scope");
|
|
56
|
+
const cache = await openSyncCache();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const deleted = cache.clear(
|
|
60
|
+
scope
|
|
61
|
+
? { scopeKey: scope }
|
|
62
|
+
: {
|
|
63
|
+
baseUrl: context.profile.baseUrl,
|
|
64
|
+
profile: context.profileName,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
printValue(
|
|
68
|
+
io,
|
|
69
|
+
context.outputMode,
|
|
70
|
+
context.outputMode === "json"
|
|
71
|
+
? { ok: true, deleted, scope: scope ?? null }
|
|
72
|
+
: `Cleared ${deleted} cache ${deleted === 1 ? "scope" : "scopes"}.`,
|
|
73
|
+
);
|
|
74
|
+
} finally {
|
|
75
|
+
cache.close();
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
default:
|
|
81
|
+
throw new CliError("Unknown cache subcommand", 2);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderJsonScope(row: SyncScopeRow): Record<string, unknown> {
|
|
86
|
+
return {
|
|
87
|
+
scopeKey: row.scope_key,
|
|
88
|
+
baseUrl: row.base_url,
|
|
89
|
+
profile: row.profile,
|
|
90
|
+
actorKey: row.actor_key,
|
|
91
|
+
resource: row.resource,
|
|
92
|
+
params: JSON.parse(row.params_json),
|
|
93
|
+
serverTimestamp: row.server_timestamp,
|
|
94
|
+
cachedAt: row.cached_at,
|
|
95
|
+
cliVersion: row.cli_version,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderStatus(cachePath: string, rows: SyncScopeRow[]): string {
|
|
100
|
+
if (rows.length === 0) {
|
|
101
|
+
return renderFields([
|
|
102
|
+
["Cache", cachePath],
|
|
103
|
+
["Scopes", "0"],
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const table = rows
|
|
108
|
+
.map((row) => ({
|
|
109
|
+
resource: row.resource,
|
|
110
|
+
scopeKey: row.scope_key,
|
|
111
|
+
serverTimestamp: formatTimestamp(row.server_timestamp),
|
|
112
|
+
cachedAt: formatTimestamp(row.cached_at),
|
|
113
|
+
}))
|
|
114
|
+
.map(
|
|
115
|
+
(row) =>
|
|
116
|
+
`${row.resource}\n scope: ${row.scopeKey}\n server timestamp: ${row.serverTimestamp}\n cached: ${row.cachedAt}`,
|
|
117
|
+
)
|
|
118
|
+
.join("\n\n");
|
|
119
|
+
|
|
120
|
+
return `${renderFields([
|
|
121
|
+
["Cache", cachePath],
|
|
122
|
+
["Scopes", String(rows.length)],
|
|
123
|
+
])}\n\n${table}`;
|
|
124
|
+
}
|
package/src/commands/feed.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
authenticatedActorKey,
|
|
3
|
+
assertSinceFlags,
|
|
4
|
+
cacheFlags,
|
|
5
|
+
cacheResult,
|
|
6
|
+
changeResponseMeta,
|
|
7
|
+
feedMyScope,
|
|
8
|
+
feedSearchScope,
|
|
9
|
+
prepareCachePlan,
|
|
10
|
+
saveCacheTimestamp,
|
|
11
|
+
type CachePlan,
|
|
12
|
+
type CacheResult,
|
|
13
|
+
type CacheScope,
|
|
14
|
+
} from "../lib/cache";
|
|
1
15
|
import {
|
|
2
16
|
channelFlag,
|
|
3
17
|
integerFlag,
|
|
@@ -7,10 +21,10 @@ import {
|
|
|
7
21
|
} from "../lib/args";
|
|
8
22
|
import { createCommandContext, type CommandContext } from "../lib/context";
|
|
9
23
|
import { CliError } from "../lib/errors";
|
|
10
|
-
import { renderPagination } from "../lib/human";
|
|
24
|
+
import { formatTimestamp, renderFields, renderPagination } from "../lib/human";
|
|
11
25
|
import { printJson, printValue, type Io } from "../lib/output";
|
|
12
26
|
import { paginatedJson, paginationInfo } from "../lib/pagination";
|
|
13
|
-
import type { PostAttributes } from "../types/api";
|
|
27
|
+
import type { ChangeCheckResponse, LatestFirstOrder, PostAttributes } from "../types/api";
|
|
14
28
|
|
|
15
29
|
export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
16
30
|
const subcommand = args.positionals[0];
|
|
@@ -18,13 +32,32 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
18
32
|
switch (subcommand) {
|
|
19
33
|
case "my": {
|
|
20
34
|
const context = await createCommandContext(args, io);
|
|
35
|
+
assertSinceFlags(args);
|
|
36
|
+
const channelId = await resolveChannelId(context, args);
|
|
37
|
+
const cacheScope = await maybeFeedMyScope(args, context, channelId);
|
|
38
|
+
const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
|
|
21
39
|
const response = await context.client.myFeed({
|
|
22
|
-
channelId
|
|
40
|
+
channelId,
|
|
23
41
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
24
42
|
cursor: stringFlag(args.flags, "cursor"),
|
|
43
|
+
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
44
|
+
since: resolvedSince(args, cachePlan),
|
|
25
45
|
});
|
|
46
|
+
const savedServerTimestamp = await maybeSaveCacheTimestamp(
|
|
47
|
+
args,
|
|
48
|
+
context,
|
|
49
|
+
cacheScope,
|
|
50
|
+
response.meta,
|
|
51
|
+
response.nextCursor === undefined,
|
|
52
|
+
);
|
|
26
53
|
|
|
27
|
-
printFeedResponse(
|
|
54
|
+
printFeedResponse(
|
|
55
|
+
args,
|
|
56
|
+
context,
|
|
57
|
+
io,
|
|
58
|
+
response,
|
|
59
|
+
cacheResult(cachePlan, savedServerTimestamp),
|
|
60
|
+
);
|
|
28
61
|
return;
|
|
29
62
|
}
|
|
30
63
|
|
|
@@ -35,14 +68,60 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
35
68
|
"Missing search query",
|
|
36
69
|
);
|
|
37
70
|
const context = await createCommandContext(args, io);
|
|
71
|
+
assertSinceFlags(args);
|
|
72
|
+
const channelId = await resolveChannelId(context, args);
|
|
73
|
+
const cacheScope = await maybeFeedSearchScope(args, context, query, channelId);
|
|
74
|
+
const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
|
|
38
75
|
const response = await context.client.searchMyFeed({
|
|
39
76
|
query,
|
|
40
|
-
channelId
|
|
77
|
+
channelId,
|
|
41
78
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
42
79
|
cursor: stringFlag(args.flags, "cursor"),
|
|
80
|
+
order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
|
|
81
|
+
since: resolvedSince(args, cachePlan),
|
|
82
|
+
});
|
|
83
|
+
const savedServerTimestamp = await maybeSaveCacheTimestamp(
|
|
84
|
+
args,
|
|
85
|
+
context,
|
|
86
|
+
cacheScope,
|
|
87
|
+
response.meta,
|
|
88
|
+
response.nextCursor === undefined,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
printFeedResponse(
|
|
92
|
+
args,
|
|
93
|
+
context,
|
|
94
|
+
io,
|
|
95
|
+
response,
|
|
96
|
+
cacheResult(cachePlan, savedServerTimestamp),
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case "changes": {
|
|
102
|
+
const context = await createCommandContext(args, io);
|
|
103
|
+
assertSinceFlags(args);
|
|
104
|
+
const channelId = await resolveChannelId(context, args);
|
|
105
|
+
const cacheScope = await maybeFeedMyScope(args, context, channelId);
|
|
106
|
+
const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
|
|
107
|
+
const response = await context.client.checkMyFeedChanges({
|
|
108
|
+
since: requiredSince(args, cachePlan),
|
|
109
|
+
channelId,
|
|
43
110
|
});
|
|
111
|
+
const savedServerTimestamp = await maybeSaveCacheTimestamp(
|
|
112
|
+
args,
|
|
113
|
+
context,
|
|
114
|
+
cacheScope,
|
|
115
|
+
changeResponseMeta(response),
|
|
116
|
+
response.has_updates === false,
|
|
117
|
+
);
|
|
44
118
|
|
|
45
|
-
|
|
119
|
+
printChangeCheckResponse(
|
|
120
|
+
context,
|
|
121
|
+
io,
|
|
122
|
+
response,
|
|
123
|
+
cacheResult(cachePlan, savedServerTimestamp),
|
|
124
|
+
);
|
|
46
125
|
return;
|
|
47
126
|
}
|
|
48
127
|
|
|
@@ -66,7 +145,9 @@ function printFeedResponse(
|
|
|
66
145
|
response: {
|
|
67
146
|
items: Array<{ id: string; attributes: PostAttributes }>;
|
|
68
147
|
nextCursor?: string;
|
|
148
|
+
meta?: Record<string, unknown>;
|
|
69
149
|
},
|
|
150
|
+
cache?: CacheResult,
|
|
70
151
|
): void {
|
|
71
152
|
if (context.outputMode === "json") {
|
|
72
153
|
printJson(
|
|
@@ -74,6 +155,8 @@ function printFeedResponse(
|
|
|
74
155
|
paginatedJson(args, {
|
|
75
156
|
items: response.items,
|
|
76
157
|
nextCursor: response.nextCursor,
|
|
158
|
+
meta: response.meta,
|
|
159
|
+
...(cache ? { cache } : {}),
|
|
77
160
|
}),
|
|
78
161
|
);
|
|
79
162
|
return;
|
|
@@ -96,4 +179,160 @@ function printFeedResponse(
|
|
|
96
179
|
if (message) {
|
|
97
180
|
io.stdout(message);
|
|
98
181
|
}
|
|
182
|
+
|
|
183
|
+
printCacheNote(io, cache);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function requiredSince(args: ParsedArgs, cachePlan?: CachePlan): string {
|
|
187
|
+
const since = stringFlag(args.flags, "since");
|
|
188
|
+
|
|
189
|
+
if (since) {
|
|
190
|
+
return since;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (cachePlan?.previousServerTimestamp) {
|
|
194
|
+
return cachePlan.previousServerTimestamp;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (cacheFlags(args).sinceCache) {
|
|
198
|
+
throw new CliError(
|
|
199
|
+
"No cached server timestamp for this feed scope. Run a read command with `--save-cache` first.",
|
|
200
|
+
2,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!since) {
|
|
205
|
+
throw new CliError("Missing `--since`", 2);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return since;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseLatestFirstOrder(value: string | undefined): LatestFirstOrder | undefined {
|
|
212
|
+
if (!value) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (value === "latest" || value === "oldest") {
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
throw new CliError("--order must be one of: latest, oldest", 2);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function printChangeCheckResponse(
|
|
224
|
+
context: CommandContext,
|
|
225
|
+
io: Io,
|
|
226
|
+
response: ChangeCheckResponse,
|
|
227
|
+
cache?: CacheResult,
|
|
228
|
+
): void {
|
|
229
|
+
printValue(
|
|
230
|
+
io,
|
|
231
|
+
context.outputMode,
|
|
232
|
+
context.outputMode === "json"
|
|
233
|
+
? { ...response, ...(cache ? { cache } : {}) }
|
|
234
|
+
: renderFields([
|
|
235
|
+
["Has updates", response.has_updates ? "yes" : "no"],
|
|
236
|
+
["Server time", formatTimestamp(response.server_time)],
|
|
237
|
+
[
|
|
238
|
+
"Recommended poll",
|
|
239
|
+
response.recommended_poll_after_ms === undefined
|
|
240
|
+
? undefined
|
|
241
|
+
: `${response.recommended_poll_after_ms}ms`,
|
|
242
|
+
],
|
|
243
|
+
...cacheFields(cache),
|
|
244
|
+
]),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function maybePrepareCachePlan(
|
|
249
|
+
args: ParsedArgs,
|
|
250
|
+
context: CommandContext,
|
|
251
|
+
scope: CacheScope | undefined,
|
|
252
|
+
): Promise<CachePlan | undefined> {
|
|
253
|
+
return cacheFlags(args).sinceCache && scope
|
|
254
|
+
? prepareCachePlan(context, scope)
|
|
255
|
+
: undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function maybeSaveCacheTimestamp(
|
|
259
|
+
args: ParsedArgs,
|
|
260
|
+
context: CommandContext,
|
|
261
|
+
scope: CacheScope | undefined,
|
|
262
|
+
meta: Record<string, unknown> | undefined,
|
|
263
|
+
shouldSave: boolean,
|
|
264
|
+
): Promise<string | undefined> {
|
|
265
|
+
return cacheFlags(args).saveCache && scope && shouldSave
|
|
266
|
+
? saveCacheTimestamp(context, scope, meta)
|
|
267
|
+
: undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function maybeFeedMyScope(
|
|
271
|
+
args: ParsedArgs,
|
|
272
|
+
context: CommandContext,
|
|
273
|
+
channelId?: string,
|
|
274
|
+
): Promise<CacheScope | undefined> {
|
|
275
|
+
if (!cacheFlags(args).sinceCache && !cacheFlags(args).saveCache) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return feedMyScope({
|
|
280
|
+
context,
|
|
281
|
+
actorKey: await authenticatedActorKey(context),
|
|
282
|
+
channelId,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function maybeFeedSearchScope(
|
|
287
|
+
args: ParsedArgs,
|
|
288
|
+
context: CommandContext,
|
|
289
|
+
query: string,
|
|
290
|
+
channelId?: string,
|
|
291
|
+
): Promise<CacheScope | undefined> {
|
|
292
|
+
if (!cacheFlags(args).sinceCache && !cacheFlags(args).saveCache) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return feedSearchScope({
|
|
297
|
+
context,
|
|
298
|
+
actorKey: await authenticatedActorKey(context),
|
|
299
|
+
query,
|
|
300
|
+
channelId,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function resolvedSince(
|
|
305
|
+
args: ParsedArgs,
|
|
306
|
+
cachePlan: CachePlan | undefined,
|
|
307
|
+
): string | undefined {
|
|
308
|
+
return stringFlag(args.flags, "since") ?? cachePlan?.previousServerTimestamp;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function printCacheNote(io: Io, cache: CacheResult | undefined): void {
|
|
312
|
+
if (!cache) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const details = [
|
|
317
|
+
cache.previousServerTimestamp
|
|
318
|
+
? `used ${cache.previousServerTimestamp}`
|
|
319
|
+
: cache.hit
|
|
320
|
+
? "used cache"
|
|
321
|
+
: "no cached timestamp",
|
|
322
|
+
cache.savedServerTimestamp ? `saved ${cache.savedServerTimestamp}` : undefined,
|
|
323
|
+
].filter(Boolean);
|
|
324
|
+
|
|
325
|
+
io.stdout(`Cache: ${details.join("; ")}.`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function cacheFields(cache: CacheResult | undefined): Array<[string, string | undefined]> {
|
|
329
|
+
if (!cache) {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return [
|
|
334
|
+
["Cache scope", cache.scopeKey],
|
|
335
|
+
["Cached timestamp", cache.previousServerTimestamp],
|
|
336
|
+
["Saved timestamp", cache.savedServerTimestamp],
|
|
337
|
+
];
|
|
99
338
|
}
|