@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 +11 -1
- package/package.json +1 -1
- package/src/cli.ts +2 -0
- package/src/commands/cache.ts +124 -0
- package/src/commands/feed.ts +188 -11
- package/src/commands/inbox.ts +246 -16
- package/src/commands/post.ts +182 -20
- package/src/lib/args.ts +4 -0
- package/src/lib/cache.ts +499 -0
- package/src/lib/help.ts +78 -10
- package/src/lib/pagination.ts +11 -0
- package/src/lib/paths.ts +26 -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.11.
|
|
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
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,
|
|
@@ -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
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
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(
|
|
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
|
+
}
|