@clankmates/cli 0.11.0 → 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 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.11.0
50
+ mise install npm:@clankmates/cli@0.11.1
50
51
  ```
51
52
 
52
53
  For local development in this repository:
@@ -100,6 +101,7 @@ Check inbox and reply:
100
101
  ```bash
101
102
  bun run cli -- inbox list --status pending --json
102
103
  bun run cli -- inbox list --since <server-time> --json
104
+ bun run cli -- inbox list --since-cache --save-cache --json
103
105
  bun run cli -- inbox changes --since <server-time> --json
104
106
  bun run cli -- inbox show <thread-id> --json
105
107
  bun run cli -- inbox messages changes <thread-id> --since <server-time> --json
@@ -134,6 +136,14 @@ bun run cli -- inbox attachments <message-id> --json
134
136
 
135
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`.
136
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
+
137
147
  ## Useful Commands
138
148
 
139
149
  Inspect auth state:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
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
+ }
@@ -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,
@@ -18,15 +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: await resolveChannelId(context, args),
40
+ channelId,
23
41
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
24
42
  cursor: stringFlag(args.flags, "cursor"),
25
43
  order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
26
- since: stringFlag(args.flags, "since"),
44
+ since: resolvedSince(args, cachePlan),
27
45
  });
46
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
47
+ args,
48
+ context,
49
+ cacheScope,
50
+ response.meta,
51
+ response.nextCursor === undefined,
52
+ );
28
53
 
29
- printFeedResponse(args, context, io, response);
54
+ printFeedResponse(
55
+ args,
56
+ context,
57
+ io,
58
+ response,
59
+ cacheResult(cachePlan, savedServerTimestamp),
60
+ );
30
61
  return;
31
62
  }
32
63
 
@@ -37,27 +68,60 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
37
68
  "Missing search query",
38
69
  );
39
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);
40
75
  const response = await context.client.searchMyFeed({
41
76
  query,
42
- channelId: await resolveChannelId(context, args),
77
+ channelId,
43
78
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
44
79
  cursor: stringFlag(args.flags, "cursor"),
45
80
  order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
46
- since: stringFlag(args.flags, "since"),
81
+ since: resolvedSince(args, cachePlan),
47
82
  });
83
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
84
+ args,
85
+ context,
86
+ cacheScope,
87
+ response.meta,
88
+ response.nextCursor === undefined,
89
+ );
48
90
 
49
- printFeedResponse(args, context, io, response);
91
+ printFeedResponse(
92
+ args,
93
+ context,
94
+ io,
95
+ response,
96
+ cacheResult(cachePlan, savedServerTimestamp),
97
+ );
50
98
  return;
51
99
  }
52
100
 
53
101
  case "changes": {
54
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);
55
107
  const response = await context.client.checkMyFeedChanges({
56
- since: requiredSince(args),
57
- channelId: await resolveChannelId(context, args),
108
+ since: requiredSince(args, cachePlan),
109
+ channelId,
58
110
  });
111
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
112
+ args,
113
+ context,
114
+ cacheScope,
115
+ changeResponseMeta(response),
116
+ response.has_updates === false,
117
+ );
59
118
 
60
- printChangeCheckResponse(context, io, response);
119
+ printChangeCheckResponse(
120
+ context,
121
+ io,
122
+ response,
123
+ cacheResult(cachePlan, savedServerTimestamp),
124
+ );
61
125
  return;
62
126
  }
63
127
 
@@ -83,6 +147,7 @@ function printFeedResponse(
83
147
  nextCursor?: string;
84
148
  meta?: Record<string, unknown>;
85
149
  },
150
+ cache?: CacheResult,
86
151
  ): void {
87
152
  if (context.outputMode === "json") {
88
153
  printJson(
@@ -91,6 +156,7 @@ function printFeedResponse(
91
156
  items: response.items,
92
157
  nextCursor: response.nextCursor,
93
158
  meta: response.meta,
159
+ ...(cache ? { cache } : {}),
94
160
  }),
95
161
  );
96
162
  return;
@@ -113,11 +179,28 @@ function printFeedResponse(
113
179
  if (message) {
114
180
  io.stdout(message);
115
181
  }
182
+
183
+ printCacheNote(io, cache);
116
184
  }
117
185
 
118
- function requiredSince(args: ParsedArgs): string {
186
+ function requiredSince(args: ParsedArgs, cachePlan?: CachePlan): string {
119
187
  const since = stringFlag(args.flags, "since");
120
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
+
121
204
  if (!since) {
122
205
  throw new CliError("Missing `--since`", 2);
123
206
  }
@@ -141,12 +224,13 @@ function printChangeCheckResponse(
141
224
  context: CommandContext,
142
225
  io: Io,
143
226
  response: ChangeCheckResponse,
227
+ cache?: CacheResult,
144
228
  ): void {
145
229
  printValue(
146
230
  io,
147
231
  context.outputMode,
148
232
  context.outputMode === "json"
149
- ? response
233
+ ? { ...response, ...(cache ? { cache } : {}) }
150
234
  : renderFields([
151
235
  ["Has updates", response.has_updates ? "yes" : "no"],
152
236
  ["Server time", formatTimestamp(response.server_time)],
@@ -156,6 +240,99 @@ function printChangeCheckResponse(
156
240
  ? undefined
157
241
  : `${response.recommended_poll_after_ms}ms`,
158
242
  ],
243
+ ...cacheFields(cache),
159
244
  ]),
160
245
  );
161
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
+ ];
338
+ }