@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.
Files changed (46) hide show
  1. package/README.md +4 -1
  2. package/package.json +1 -1
  3. package/src/commands/auth/access-keys.ts +206 -0
  4. package/src/commands/auth.ts +3 -196
  5. package/src/commands/channel/render.ts +224 -0
  6. package/src/commands/channel/tokens.ts +145 -0
  7. package/src/commands/channel/validation.ts +11 -0
  8. package/src/commands/channel.ts +11 -340
  9. package/src/commands/doctor/checks.ts +123 -0
  10. package/src/commands/doctor/render.ts +140 -0
  11. package/src/commands/doctor/suggestions.ts +42 -0
  12. package/src/commands/doctor/types.ts +75 -0
  13. package/src/commands/doctor.ts +12 -371
  14. package/src/commands/feed.ts +15 -178
  15. package/src/commands/inbox/content.ts +31 -0
  16. package/src/commands/inbox/filters.ts +70 -0
  17. package/src/commands/inbox/messages.ts +69 -0
  18. package/src/commands/inbox/participants.ts +152 -0
  19. package/src/commands/inbox/render.ts +13 -0
  20. package/src/commands/inbox/resource-output.ts +217 -0
  21. package/src/commands/inbox/schema.ts +185 -0
  22. package/src/commands/inbox/screening.ts +76 -0
  23. package/src/commands/inbox/sync-scopes.ts +59 -0
  24. package/src/commands/inbox/thread-output.ts +344 -0
  25. package/src/commands/inbox/watch.ts +203 -0
  26. package/src/commands/inbox.ts +37 -1243
  27. package/src/commands/post.ts +9 -114
  28. package/src/lib/args.ts +1 -0
  29. package/src/lib/cache/scopes.ts +216 -0
  30. package/src/lib/cache/store.ts +195 -0
  31. package/src/lib/cache/types.ts +31 -0
  32. package/src/lib/cache.ts +18 -436
  33. package/src/lib/client/auth.ts +122 -0
  34. package/src/lib/client/channel-keys.ts +57 -0
  35. package/src/lib/client/channels.ts +364 -0
  36. package/src/lib/client/core.ts +133 -0
  37. package/src/lib/client/feed.ts +76 -0
  38. package/src/lib/client/inbox.ts +361 -0
  39. package/src/lib/client/posts.ts +213 -0
  40. package/src/lib/client/raw-api.ts +33 -0
  41. package/src/lib/client/users.ts +88 -0
  42. package/src/lib/client.ts +177 -913
  43. package/src/lib/help.ts +26 -0
  44. package/src/lib/json_api.ts +74 -9
  45. package/src/lib/polling.ts +146 -0
  46. 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.",
@@ -1,5 +1,5 @@
1
1
  import { CliError } from "./errors";
2
- import type { JsonApiDocument, JsonApiResource } from "../types/api";
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 as JsonApiDocument<TAttributes>;
20
+ const document = asJsonApiDocument(payload);
15
21
 
16
- if (!document || Array.isArray(document.data) || typeof document.data !== "object") {
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 as JsonApiDocument<TAttributes>;
32
+ const document = asJsonApiDocument(payload);
27
33
 
28
- if (!document || !Array.isArray(document.data)) {
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
- items: document.data,
34
- nextCursor: extractCursor(document.links?.next),
35
- prevCursor: extractCursor(document.links?.prev),
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
+ }