@clankmates/cli 0.12.0 → 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 +4 -1
- package/package.json +1 -1
- 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 +15 -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 +37 -1243
- package/src/commands/post.ts +9 -114
- package/src/lib/args.ts +1 -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 -436
- 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 +177 -913
- package/src/lib/help.ts +26 -0
- package/src/lib/json_api.ts +74 -9
- package/src/lib/polling.ts +146 -0
- package/src/lib/post-output.ts +55 -0
package/src/lib/help.ts
CHANGED
|
@@ -925,6 +925,32 @@ const HELP_ROOT = group(
|
|
|
925
925
|
],
|
|
926
926
|
},
|
|
927
927
|
),
|
|
928
|
+
group(
|
|
929
|
+
"watch",
|
|
930
|
+
"Stream inbox changes as JSONL records.",
|
|
931
|
+
[
|
|
932
|
+
command(
|
|
933
|
+
"messages",
|
|
934
|
+
"Watch one thread and emit each newly visible message as one JSONL line.",
|
|
935
|
+
`${CLI_NAME} inbox watch messages <thread-id> [--once] [--since <server-time>|--since-cache] [--query <text>] [--has-attachment] [--before <timestamp>] [--limit <n>] [--channel-token <token>] [--profile <name>]`,
|
|
936
|
+
{
|
|
937
|
+
options: [
|
|
938
|
+
option("--once", "Run one watch cycle and exit."),
|
|
939
|
+
SINCE_OPTION,
|
|
940
|
+
SINCE_CACHE_OPTION,
|
|
941
|
+
...MESSAGE_SEARCH_OPTIONS,
|
|
942
|
+
BEFORE_OPTION,
|
|
943
|
+
LIMIT_OPTION,
|
|
944
|
+
CHANNEL_TOKEN_OPTION,
|
|
945
|
+
PROFILE_OPTION,
|
|
946
|
+
],
|
|
947
|
+
},
|
|
948
|
+
),
|
|
949
|
+
],
|
|
950
|
+
{
|
|
951
|
+
usage: [`${CLI_NAME} inbox watch <subcommand>`],
|
|
952
|
+
},
|
|
953
|
+
),
|
|
928
954
|
group(
|
|
929
955
|
"messages",
|
|
930
956
|
"Check thread message updates.",
|
package/src/lib/json_api.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "./errors";
|
|
2
|
-
import type {
|
|
2
|
+
import type { JsonApiResource } from "../types/api";
|
|
3
3
|
|
|
4
4
|
export interface JsonApiCollectionResult<TAttributes extends object> {
|
|
5
5
|
items: Array<JsonApiResource<TAttributes>>;
|
|
@@ -8,12 +8,18 @@ export interface JsonApiCollectionResult<TAttributes extends object> {
|
|
|
8
8
|
meta?: Record<string, unknown>;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
interface JsonApiDocumentShape {
|
|
12
|
+
data: unknown;
|
|
13
|
+
links?: unknown;
|
|
14
|
+
meta?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
export function expectResource<TAttributes extends object>(
|
|
12
18
|
payload: unknown
|
|
13
19
|
): JsonApiResource<TAttributes> {
|
|
14
|
-
const document = payload
|
|
20
|
+
const document = asJsonApiDocument(payload);
|
|
15
21
|
|
|
16
|
-
if (!
|
|
22
|
+
if (!isJsonApiResource<TAttributes>(document.data)) {
|
|
17
23
|
throw new CliError("Expected a JSON:API resource object in response");
|
|
18
24
|
}
|
|
19
25
|
|
|
@@ -23,20 +29,79 @@ export function expectResource<TAttributes extends object>(
|
|
|
23
29
|
export function expectCollection<TAttributes extends object>(
|
|
24
30
|
payload: unknown
|
|
25
31
|
): JsonApiCollectionResult<TAttributes> {
|
|
26
|
-
const document = payload
|
|
32
|
+
const document = asJsonApiDocument(payload);
|
|
27
33
|
|
|
28
|
-
if (!
|
|
34
|
+
if (!Array.isArray(document.data)) {
|
|
29
35
|
throw new CliError("Expected a JSON:API collection in response");
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
const items = document.data.filter(isJsonApiResource<TAttributes>);
|
|
39
|
+
|
|
40
|
+
if (items.length !== document.data.length) {
|
|
41
|
+
throw new CliError("Expected a JSON:API collection in response");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
items,
|
|
46
|
+
nextCursor: extractCursor(linkValue(document.links, "next")),
|
|
47
|
+
prevCursor: extractCursor(linkValue(document.links, "prev")),
|
|
48
|
+
meta: isRecord(document.meta) ? document.meta : undefined
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function asJsonApiDocument(
|
|
53
|
+
payload: unknown,
|
|
54
|
+
): JsonApiDocumentShape {
|
|
55
|
+
if (!isRecord(payload)) {
|
|
56
|
+
throw new CliError("Expected a JSON:API document in response");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!("data" in payload)) {
|
|
60
|
+
throw new CliError("Expected a JSON:API document in response");
|
|
61
|
+
}
|
|
62
|
+
|
|
32
63
|
return {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
meta: document.meta
|
|
64
|
+
data: payload.data,
|
|
65
|
+
links: payload.links,
|
|
66
|
+
meta: payload.meta,
|
|
37
67
|
};
|
|
38
68
|
}
|
|
39
69
|
|
|
70
|
+
function isJsonApiResource<TAttributes extends object>(
|
|
71
|
+
value: unknown,
|
|
72
|
+
): value is JsonApiResource<TAttributes> {
|
|
73
|
+
if (!isRecord(value)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
typeof value.id === "string" &&
|
|
79
|
+
typeof value.type === "string" &&
|
|
80
|
+
isRecord(value.attributes)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function linkValue(
|
|
85
|
+
links: unknown,
|
|
86
|
+
key: string,
|
|
87
|
+
): string | null | undefined {
|
|
88
|
+
if (!isRecord(links)) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const value = links[key];
|
|
93
|
+
|
|
94
|
+
if (typeof value === "string" || value === null) {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
102
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
40
105
|
function extractCursor(link: string | null | undefined): string | undefined {
|
|
41
106
|
if (!link) {
|
|
42
107
|
return undefined;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cacheFlags,
|
|
3
|
+
prepareCachePlan,
|
|
4
|
+
saveCacheTimestamp,
|
|
5
|
+
type CachePlan,
|
|
6
|
+
type CacheResult,
|
|
7
|
+
type CacheScope,
|
|
8
|
+
} from "./cache";
|
|
9
|
+
import { stringFlag, type ParsedArgs } from "./args";
|
|
10
|
+
import type { CommandContext } from "./context";
|
|
11
|
+
import { CliError } from "./errors";
|
|
12
|
+
import { formatTimestamp, renderFields } from "./human";
|
|
13
|
+
import { printValue, type Io } from "./output";
|
|
14
|
+
import type { ChangeCheckResponse, LatestFirstOrder } from "../types/api";
|
|
15
|
+
|
|
16
|
+
export function parseLatestFirstOrder(
|
|
17
|
+
value: string | undefined,
|
|
18
|
+
): LatestFirstOrder | undefined {
|
|
19
|
+
if (!value) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (value === "latest" || value === "oldest") {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new CliError("--order must be one of: latest, oldest", 2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function requiredSince(
|
|
31
|
+
args: ParsedArgs,
|
|
32
|
+
cachePlan?: CachePlan,
|
|
33
|
+
label = "resource",
|
|
34
|
+
): string {
|
|
35
|
+
const since = stringFlag(args.flags, "since");
|
|
36
|
+
|
|
37
|
+
if (since) {
|
|
38
|
+
return since;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (cachePlan?.previousServerTimestamp) {
|
|
42
|
+
return cachePlan.previousServerTimestamp;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (cacheFlags(args).sinceCache) {
|
|
46
|
+
throw new CliError(
|
|
47
|
+
`No cached server timestamp for this ${label} scope. Run a read command with \`--save-cache\` first.`,
|
|
48
|
+
2,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new CliError("Missing `--since`", 2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolvedSince(
|
|
56
|
+
args: ParsedArgs,
|
|
57
|
+
cachePlan: CachePlan | undefined,
|
|
58
|
+
): string | undefined {
|
|
59
|
+
return stringFlag(args.flags, "since") ?? cachePlan?.previousServerTimestamp;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function maybePrepareCachePlan(
|
|
63
|
+
args: ParsedArgs,
|
|
64
|
+
context: CommandContext,
|
|
65
|
+
scope: CacheScope | undefined,
|
|
66
|
+
): Promise<CachePlan | undefined> {
|
|
67
|
+
return cacheFlags(args).sinceCache && scope
|
|
68
|
+
? prepareCachePlan(context, scope)
|
|
69
|
+
: undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function maybeSaveCacheTimestamp(
|
|
73
|
+
args: ParsedArgs,
|
|
74
|
+
context: CommandContext,
|
|
75
|
+
scope: CacheScope | undefined,
|
|
76
|
+
meta: Record<string, unknown> | undefined,
|
|
77
|
+
shouldSave: boolean,
|
|
78
|
+
): Promise<string | undefined> {
|
|
79
|
+
return cacheFlags(args).saveCache && scope && shouldSave
|
|
80
|
+
? saveCacheTimestamp(context, scope, meta)
|
|
81
|
+
: undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderCacheNote(cache: CacheResult | undefined): string | undefined {
|
|
85
|
+
if (!cache) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const details = [
|
|
90
|
+
cache.previousServerTimestamp
|
|
91
|
+
? `used ${cache.previousServerTimestamp}`
|
|
92
|
+
: cache.hit
|
|
93
|
+
? "used cache"
|
|
94
|
+
: "no cached timestamp",
|
|
95
|
+
cache.savedServerTimestamp ? `saved ${cache.savedServerTimestamp}` : undefined,
|
|
96
|
+
].filter(Boolean);
|
|
97
|
+
|
|
98
|
+
return `Cache: ${details.join("; ")}.`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function printCacheNote(io: Io, cache: CacheResult | undefined): void {
|
|
102
|
+
const note = renderCacheNote(cache);
|
|
103
|
+
|
|
104
|
+
if (note) {
|
|
105
|
+
io.stdout(note);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function cacheFields(
|
|
110
|
+
cache: CacheResult | undefined,
|
|
111
|
+
): Array<[string, string | undefined]> {
|
|
112
|
+
if (!cache) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [
|
|
117
|
+
["Cache scope", cache.scopeKey],
|
|
118
|
+
["Cached timestamp", cache.previousServerTimestamp],
|
|
119
|
+
["Saved timestamp", cache.savedServerTimestamp],
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function printChangeCheckResponse(
|
|
124
|
+
context: CommandContext,
|
|
125
|
+
io: Io,
|
|
126
|
+
response: ChangeCheckResponse,
|
|
127
|
+
cache?: CacheResult,
|
|
128
|
+
): void {
|
|
129
|
+
printValue(
|
|
130
|
+
io,
|
|
131
|
+
context.outputMode,
|
|
132
|
+
context.outputMode === "json"
|
|
133
|
+
? { ...response, ...(cache ? { cache } : {}) }
|
|
134
|
+
: renderFields([
|
|
135
|
+
["Has updates", response.has_updates ? "yes" : "no"],
|
|
136
|
+
["Server time", formatTimestamp(response.server_time)],
|
|
137
|
+
[
|
|
138
|
+
"Recommended poll",
|
|
139
|
+
response.recommended_poll_after_ms === undefined
|
|
140
|
+
? undefined
|
|
141
|
+
: `${response.recommended_poll_after_ms}ms`,
|
|
142
|
+
],
|
|
143
|
+
...cacheFields(cache),
|
|
144
|
+
]),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CacheResult } from "./cache";
|
|
2
|
+
import { renderPagination } from "./human";
|
|
3
|
+
import { printJson, printValue, type Io } from "./output";
|
|
4
|
+
import { paginatedJson, paginationInfo } from "./pagination";
|
|
5
|
+
import { printCacheNote } from "./polling";
|
|
6
|
+
import type { ParsedArgs } from "./args";
|
|
7
|
+
import type { OutputMode, PostAttributes } from "../types/api";
|
|
8
|
+
|
|
9
|
+
export function printPostCollection(
|
|
10
|
+
args: ParsedArgs,
|
|
11
|
+
outputMode: OutputMode,
|
|
12
|
+
io: Io,
|
|
13
|
+
response: {
|
|
14
|
+
items: Array<{ id: string; attributes: PostAttributes }>;
|
|
15
|
+
nextCursor?: string;
|
|
16
|
+
meta?: Record<string, unknown>;
|
|
17
|
+
},
|
|
18
|
+
cache?: CacheResult,
|
|
19
|
+
): void {
|
|
20
|
+
if (outputMode === "json") {
|
|
21
|
+
printJson(
|
|
22
|
+
io,
|
|
23
|
+
paginatedJson(args, {
|
|
24
|
+
items: response.items,
|
|
25
|
+
nextCursor: response.nextCursor,
|
|
26
|
+
meta: response.meta,
|
|
27
|
+
...(cache ? { cache } : {}),
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
printValue(
|
|
34
|
+
io,
|
|
35
|
+
outputMode,
|
|
36
|
+
response.items.map((item) => ({
|
|
37
|
+
id: item.id,
|
|
38
|
+
source: item.attributes.source,
|
|
39
|
+
date: item.attributes.updated_at ?? item.attributes.inserted_at ?? "",
|
|
40
|
+
body: item.attributes.body,
|
|
41
|
+
})),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const pagination = paginationInfo(args, response.nextCursor);
|
|
45
|
+
const message = renderPagination(
|
|
46
|
+
pagination?.nextCursor,
|
|
47
|
+
pagination?.nextCommand,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (message) {
|
|
51
|
+
io.stdout(message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
printCacheNote(io, cache);
|
|
55
|
+
}
|