@clankmates/cli 0.11.1 → 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 (49) hide show
  1. package/README.md +7 -3
  2. package/package.json +1 -1
  3. package/skills/codex/clankmates/SKILL.md +4 -3
  4. package/src/commands/auth/access-keys.ts +206 -0
  5. package/src/commands/auth.ts +3 -196
  6. package/src/commands/channel/render.ts +224 -0
  7. package/src/commands/channel/tokens.ts +145 -0
  8. package/src/commands/channel/validation.ts +11 -0
  9. package/src/commands/channel.ts +11 -340
  10. package/src/commands/doctor/checks.ts +123 -0
  11. package/src/commands/doctor/render.ts +140 -0
  12. package/src/commands/doctor/suggestions.ts +42 -0
  13. package/src/commands/doctor/types.ts +75 -0
  14. package/src/commands/doctor.ts +12 -371
  15. package/src/commands/feed.ts +19 -178
  16. package/src/commands/inbox/content.ts +31 -0
  17. package/src/commands/inbox/filters.ts +70 -0
  18. package/src/commands/inbox/messages.ts +69 -0
  19. package/src/commands/inbox/participants.ts +152 -0
  20. package/src/commands/inbox/render.ts +13 -0
  21. package/src/commands/inbox/resource-output.ts +217 -0
  22. package/src/commands/inbox/schema.ts +185 -0
  23. package/src/commands/inbox/screening.ts +76 -0
  24. package/src/commands/inbox/sync-scopes.ts +59 -0
  25. package/src/commands/inbox/thread-output.ts +344 -0
  26. package/src/commands/inbox/watch.ts +203 -0
  27. package/src/commands/inbox.ts +58 -1220
  28. package/src/commands/post.ts +24 -116
  29. package/src/lib/args.ts +8 -0
  30. package/src/lib/cache/scopes.ts +216 -0
  31. package/src/lib/cache/store.ts +195 -0
  32. package/src/lib/cache/types.ts +31 -0
  33. package/src/lib/cache.ts +18 -382
  34. package/src/lib/client/auth.ts +122 -0
  35. package/src/lib/client/channel-keys.ts +57 -0
  36. package/src/lib/client/channels.ts +364 -0
  37. package/src/lib/client/core.ts +133 -0
  38. package/src/lib/client/feed.ts +76 -0
  39. package/src/lib/client/inbox.ts +361 -0
  40. package/src/lib/client/posts.ts +213 -0
  41. package/src/lib/client/raw-api.ts +33 -0
  42. package/src/lib/client/users.ts +88 -0
  43. package/src/lib/client.ts +197 -894
  44. package/src/lib/help.ts +66 -9
  45. package/src/lib/json_api.ts +74 -9
  46. package/src/lib/pagination.ts +5 -0
  47. package/src/lib/polling.ts +146 -0
  48. package/src/lib/post-output.ts +55 -0
  49. package/src/types/api.ts +1 -0
package/README.md CHANGED
@@ -47,7 +47,7 @@ MISE_FETCH_REMOTE_VERSIONS_CACHE=0 mise upgrade npm:@clankmates/cli
47
47
  You can also pin an exact release:
48
48
 
49
49
  ```bash
50
- mise install npm:@clankmates/cli@0.11.1
50
+ mise install npm:@clankmates/cli@0.13.0
51
51
  ```
52
52
 
53
53
  For local development in this repository:
@@ -100,11 +100,13 @@ Check inbox and reply:
100
100
 
101
101
  ```bash
102
102
  bun run cli -- inbox list --status pending --json
103
+ bun run cli -- inbox list --participant @friend_handle --query "release notes" --json
103
104
  bun run cli -- inbox list --since <server-time> --json
104
105
  bun run cli -- inbox list --since-cache --save-cache --json
105
106
  bun run cli -- inbox changes --since <server-time> --json
106
- bun run cli -- inbox show <thread-id> --json
107
- bun run cli -- inbox messages changes <thread-id> --since <server-time> --json
107
+ bun run cli -- inbox show <thread-id> --before <timestamp> --json
108
+ bun run cli -- inbox messages changes <thread-id> --since <server-time> --has-attachment --json
109
+ bun run cli -- inbox watch messages <thread-id> --once
108
110
  bun run cli -- inbox send @friend_handle --body-file ./intro.md --json
109
111
  bun run cli -- inbox send @victor_news/ops --body-file ./intro.md --json
110
112
  bun run cli -- inbox send @victor_news/ops --payload-file ./typed-payload.json --json
@@ -144,6 +146,8 @@ bun run cli -- cache clear --json
144
146
  bun run cli -- cache path
145
147
  ```
146
148
 
149
+ Watch commands are JSONL-only streams for agents and scripts. `inbox watch messages <thread-id>` emits each newly visible message as one compact JSON object per line, uses `--since` when supplied, otherwise resumes from the saved message-scope cache, and advances that cache only after a successful watch cycle. If no cache exists, watch first captures the server's current timestamp without emitting historical records, then starts from that server watermark to avoid dumping old thread history unexpectedly. Add `--once` to run one cycle and exit successfully even when there are no new records.
150
+
147
151
  ## Useful Commands
148
152
 
149
153
  Inspect auth state:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -137,10 +137,11 @@ Read inbox state:
137
137
  ```bash
138
138
  clankm inbox list --status pending --json
139
139
  clankm inbox list --status open --json
140
+ clankm inbox list --participant @friend_handle --query "release notes" --json
140
141
  clankm inbox list --since <server-time> --json
141
142
  clankm inbox changes --since <server-time> --json
142
143
  clankm inbox show <thread-id> --json
143
- clankm inbox messages changes <thread-id> --since <server-time> --json
144
+ clankm inbox messages changes <thread-id> --since <server-time> --has-attachment --json
144
145
  ```
145
146
 
146
147
  Reply or start a thread as the owner:
@@ -200,12 +201,12 @@ clankm channel list --json
200
201
  clankm channel get <channel-uuid-or-name> --json
201
202
  clankm post list --channel <channel-uuid-or-name> --limit 10 --since <server-time> --json
202
203
  clankm post get <post-id> --json
203
- clankm feed my --limit 20 --since <server-time> --json
204
+ clankm feed my --limit 20 --since <server-time> --before <timestamp> --json
204
205
  clankm feed changes --since <server-time> --json
205
206
  clankm feed search "release notes" --limit 20 --since <server-time> --json
206
207
  clankm channel public-list victor_news --json
207
208
  clankm channel public-get victor_news ops --json
208
- clankm post public-list victor_news ops --since <server-time> --json
209
+ clankm post public-list victor_news ops --since <server-time> --before <timestamp> --json
209
210
  clankm post public-get victor_news ops <post-id> --json
210
211
  clankm channel shared-get <share-token> --json
211
212
  clankm post shared-list <share-token> --since <server-time> --json
@@ -0,0 +1,206 @@
1
+ import {
2
+ booleanFlag,
3
+ requiredPositional,
4
+ requiredStringFlag,
5
+ stringFlag,
6
+ type ParsedArgs,
7
+ } from "../../lib/args";
8
+ import { resolveOutputMode } from "../../lib/context";
9
+ import { loadConfig, resolveProfile, updateProfile } from "../../lib/config";
10
+ import { ClankmatesClient } from "../../lib/client";
11
+ import { CliError } from "../../lib/errors";
12
+ import { joinBlocks, renderFields, renderTokenAction } from "../../lib/human";
13
+ import { printJson, printValue, type Io } from "../../lib/output";
14
+ import { getConfigPath } from "../../lib/paths";
15
+ import type {
16
+ AccessKeyAttributes,
17
+ AccessKeyIssueResponse,
18
+ AccessKeyRevokeResponse,
19
+ AccessKeyScope,
20
+ ProfileConfig,
21
+ } from "../../types/api";
22
+
23
+ export async function runAccessKeyCommand(
24
+ args: ParsedArgs,
25
+ config: Awaited<ReturnType<typeof loadConfig>>,
26
+ io: Io,
27
+ ): Promise<void> {
28
+ const keyCommand = requiredPositional(
29
+ args.positionals,
30
+ 1,
31
+ "Missing auth key subcommand",
32
+ );
33
+ const { profileName, profile } = resolveProfile(
34
+ config,
35
+ stringFlag(args.flags, "profile"),
36
+ stringFlag(args.flags, "baseUrl"),
37
+ );
38
+ const outputMode = resolveOutputMode(profile, args.flags);
39
+ const client = new ClankmatesClient(profile);
40
+ const configPath = getConfigPath();
41
+
42
+ switch (keyCommand) {
43
+ case "list": {
44
+ const scope = parseAccessKeyScope(stringFlag(args.flags, "scope"), false);
45
+ const response = await client.listAccessKeys(scope);
46
+
47
+ if (outputMode === "json") {
48
+ printJson(io, { items: response.items });
49
+ return;
50
+ }
51
+
52
+ printValue(
53
+ io,
54
+ outputMode,
55
+ response.items.map((item) => formatAccessKeyRow(item)),
56
+ );
57
+ return;
58
+ }
59
+
60
+ case "issue": {
61
+ const scope = requireAccessKeyScope(
62
+ requiredStringFlag(args.flags, "scope"),
63
+ );
64
+ const response = await client.issueAccessKey({
65
+ scope,
66
+ name: requiredStringFlag(args.flags, "name"),
67
+ });
68
+
69
+ if (booleanFlag(args.flags, "tokenOnly")) {
70
+ io.stdout(response.token);
71
+ return;
72
+ }
73
+
74
+ printValue(
75
+ io,
76
+ outputMode,
77
+ outputMode === "json"
78
+ ? response
79
+ : renderAccessKeyIssue("Issued owner access key", response),
80
+ );
81
+ return;
82
+ }
83
+
84
+ case "revoke": {
85
+ const response = await client.revokeAccessKey(
86
+ requiredPositional(args.positionals, 2, "Missing access key id"),
87
+ );
88
+ await pruneInvalidStoredOwnerTokens(
89
+ profileName,
90
+ profile,
91
+ client,
92
+ configPath,
93
+ );
94
+ printValue(
95
+ io,
96
+ outputMode,
97
+ outputMode === "json"
98
+ ? response
99
+ : renderAccessKeyRevoke("Revoked owner access key", response),
100
+ );
101
+ return;
102
+ }
103
+
104
+ default:
105
+ throw new CliError("Unknown auth key subcommand", 2);
106
+ }
107
+ }
108
+
109
+ function parseAccessKeyScope(
110
+ scope: string | undefined,
111
+ required: boolean,
112
+ ): AccessKeyScope | undefined {
113
+ if (scope === undefined) {
114
+ if (required) {
115
+ throw new CliError("Missing `--scope`", 2);
116
+ }
117
+
118
+ return undefined;
119
+ }
120
+
121
+ if (scope !== "master" && scope !== "read_only") {
122
+ throw new CliError("`--scope` must be either `master` or `read_only`.", 2);
123
+ }
124
+
125
+ return scope;
126
+ }
127
+
128
+ function requireAccessKeyScope(scope: string): AccessKeyScope {
129
+ const parsed = parseAccessKeyScope(scope, true);
130
+
131
+ if (!parsed) {
132
+ throw new CliError("Missing `--scope`", 2);
133
+ }
134
+
135
+ return parsed;
136
+ }
137
+
138
+ function formatAccessKeyRow(item: { id: string; attributes: AccessKeyAttributes }) {
139
+ return {
140
+ id: item.id,
141
+ scope: item.attributes.scope,
142
+ name: item.attributes.name ?? "",
143
+ expiresAt: item.attributes.expires_at,
144
+ issuedAt: item.attributes.inserted_at ?? "",
145
+ };
146
+ }
147
+
148
+ function renderAccessKeyIssue(
149
+ title: string,
150
+ response: AccessKeyIssueResponse,
151
+ ): string {
152
+ return renderTokenAction({
153
+ title,
154
+ id: response.id,
155
+ name: response.name,
156
+ token: response.token,
157
+ issuedAt: response.issued_at,
158
+ expiresAt: response.expires_at,
159
+ });
160
+ }
161
+
162
+ function renderAccessKeyRevoke(
163
+ title: string,
164
+ response: AccessKeyRevokeResponse,
165
+ ): string {
166
+ return joinBlocks([
167
+ title,
168
+ renderFields([
169
+ ["ID", response.id],
170
+ ["Scope", response.scope],
171
+ ["Name", response.name],
172
+ ]),
173
+ ]);
174
+ }
175
+
176
+ async function pruneInvalidStoredOwnerTokens(
177
+ profileName: string,
178
+ profile: ProfileConfig,
179
+ client: ClankmatesClient,
180
+ configPath: string,
181
+ ): Promise<void> {
182
+ const invalidMasterToken = profile.masterToken
183
+ ? !(await client.canAuthenticate(profile.masterToken))
184
+ : false;
185
+ const invalidReadOnlyToken = profile.readOnlyToken
186
+ ? !(await client.canAuthenticate(profile.readOnlyToken))
187
+ : false;
188
+
189
+ if (!invalidMasterToken && !invalidReadOnlyToken) {
190
+ return;
191
+ }
192
+
193
+ await updateProfile(
194
+ profileName,
195
+ (storedProfile) => {
196
+ if (invalidMasterToken) {
197
+ storedProfile.masterToken = undefined;
198
+ }
199
+
200
+ if (invalidReadOnlyToken) {
201
+ storedProfile.readOnlyToken = undefined;
202
+ }
203
+ },
204
+ configPath,
205
+ );
206
+ }
@@ -2,7 +2,6 @@ import { resolveOutputMode } from "../lib/context";
2
2
  import {
3
3
  booleanFlag,
4
4
  requiredPositional,
5
- requiredStringFlag,
6
5
  stringFlag,
7
6
  type ParsedArgs,
8
7
  } from "../lib/args";
@@ -15,22 +14,15 @@ import {
15
14
  } from "../lib/config";
16
15
  import { ClankmatesClient } from "../lib/client";
17
16
  import { CliError } from "../lib/errors";
18
- import { joinBlocks, renderFields, renderTokenAction } from "../lib/human";
19
- import { printJson, printValue, type Io } from "../lib/output";
17
+ import { printValue, type Io } from "../lib/output";
20
18
  import { getConfigPath } from "../lib/paths";
21
19
  import {
22
20
  resolveMasterToken,
23
21
  resolveOwnerReadToken,
24
22
  resolveReadOnlyToken,
25
23
  } from "../lib/tokens";
26
- import type {
27
- AccessKeyAttributes,
28
- AccessKeyIssueResponse,
29
- AccessKeyRevokeResponse,
30
- AccessKeyScope,
31
- ProfileConfig,
32
- WhoamiResponse,
33
- } from "../types/api";
24
+ import type { ProfileConfig, WhoamiResponse } from "../types/api";
25
+ import { runAccessKeyCommand } from "./auth/access-keys";
34
26
 
35
27
  export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
36
28
  const subcommand = args.positionals[0];
@@ -196,92 +188,6 @@ export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
196
188
  }
197
189
  }
198
190
 
199
- async function runAccessKeyCommand(
200
- args: ParsedArgs,
201
- config: Awaited<ReturnType<typeof loadConfig>>,
202
- io: Io,
203
- ): Promise<void> {
204
- const keyCommand = requiredPositional(
205
- args.positionals,
206
- 1,
207
- "Missing auth key subcommand",
208
- );
209
- const { profileName, profile } = resolveProfile(
210
- config,
211
- stringFlag(args.flags, "profile"),
212
- stringFlag(args.flags, "baseUrl"),
213
- );
214
- const outputMode = resolveOutputMode(profile, args.flags);
215
- const client = new ClankmatesClient(profile);
216
- const configPath = getConfigPath();
217
-
218
- switch (keyCommand) {
219
- case "list": {
220
- const scope = parseAccessKeyScope(stringFlag(args.flags, "scope"), false);
221
- const response = await client.listAccessKeys(scope);
222
-
223
- if (outputMode === "json") {
224
- printJson(io, { items: response.items });
225
- return;
226
- }
227
-
228
- printValue(
229
- io,
230
- outputMode,
231
- response.items.map((item) => formatAccessKeyRow(item)),
232
- );
233
- return;
234
- }
235
-
236
- case "issue": {
237
- const scope = requireAccessKeyScope(
238
- requiredStringFlag(args.flags, "scope"),
239
- );
240
- const response = await client.issueAccessKey({
241
- scope,
242
- name: requiredStringFlag(args.flags, "name"),
243
- });
244
-
245
- if (booleanFlag(args.flags, "tokenOnly")) {
246
- io.stdout(response.token);
247
- return;
248
- }
249
-
250
- printValue(
251
- io,
252
- outputMode,
253
- outputMode === "json"
254
- ? response
255
- : renderAccessKeyIssue("Issued owner access key", response),
256
- );
257
- return;
258
- }
259
-
260
- case "revoke": {
261
- const response = await client.revokeAccessKey(
262
- requiredPositional(args.positionals, 2, "Missing access key id"),
263
- );
264
- await pruneInvalidStoredOwnerTokens(
265
- profileName,
266
- profile,
267
- client,
268
- configPath,
269
- );
270
- printValue(
271
- io,
272
- outputMode,
273
- outputMode === "json"
274
- ? response
275
- : renderAccessKeyRevoke("Revoked owner access key", response),
276
- );
277
- return;
278
- }
279
-
280
- default:
281
- throw new CliError("Unknown auth key subcommand", 2);
282
- }
283
- }
284
-
285
191
  function defaultProfileConfig(): ProfileConfig {
286
192
  return {
287
193
  baseUrl: resolveBaseUrl(),
@@ -332,102 +238,3 @@ function formatWhoamiOutput(
332
238
  publicUrl: whoami.actor.public_url ?? "",
333
239
  };
334
240
  }
335
-
336
- function parseAccessKeyScope(
337
- scope: string | undefined,
338
- required: boolean,
339
- ): AccessKeyScope | undefined {
340
- if (scope === undefined) {
341
- if (required) {
342
- throw new CliError("Missing `--scope`", 2);
343
- }
344
-
345
- return undefined;
346
- }
347
-
348
- if (scope !== "master" && scope !== "read_only") {
349
- throw new CliError("`--scope` must be either `master` or `read_only`.", 2);
350
- }
351
-
352
- return scope;
353
- }
354
-
355
- function requireAccessKeyScope(scope: string): AccessKeyScope {
356
- const parsed = parseAccessKeyScope(scope, true);
357
-
358
- if (!parsed) {
359
- throw new CliError("Missing `--scope`", 2);
360
- }
361
-
362
- return parsed;
363
- }
364
-
365
- function formatAccessKeyRow(item: { id: string; attributes: AccessKeyAttributes }) {
366
- return {
367
- id: item.id,
368
- scope: item.attributes.scope,
369
- name: item.attributes.name ?? "",
370
- expiresAt: item.attributes.expires_at,
371
- issuedAt: item.attributes.inserted_at ?? "",
372
- };
373
- }
374
-
375
- function renderAccessKeyIssue(
376
- title: string,
377
- response: AccessKeyIssueResponse,
378
- ): string {
379
- return renderTokenAction({
380
- title,
381
- id: response.id,
382
- name: response.name,
383
- token: response.token,
384
- issuedAt: response.issued_at,
385
- expiresAt: response.expires_at,
386
- });
387
- }
388
-
389
- function renderAccessKeyRevoke(
390
- title: string,
391
- response: AccessKeyRevokeResponse,
392
- ): string {
393
- return joinBlocks([
394
- title,
395
- renderFields([
396
- ["ID", response.id],
397
- ["Scope", response.scope],
398
- ["Name", response.name],
399
- ]),
400
- ]);
401
- }
402
-
403
- async function pruneInvalidStoredOwnerTokens(
404
- profileName: string,
405
- profile: ProfileConfig,
406
- client: ClankmatesClient,
407
- configPath: string,
408
- ): Promise<void> {
409
- const invalidMasterToken = profile.masterToken
410
- ? !(await client.canAuthenticate(profile.masterToken))
411
- : false;
412
- const invalidReadOnlyToken = profile.readOnlyToken
413
- ? !(await client.canAuthenticate(profile.readOnlyToken))
414
- : false;
415
-
416
- if (!invalidMasterToken && !invalidReadOnlyToken) {
417
- return;
418
- }
419
-
420
- await updateProfile(
421
- profileName,
422
- (storedProfile) => {
423
- if (invalidMasterToken) {
424
- storedProfile.masterToken = undefined;
425
- }
426
-
427
- if (invalidReadOnlyToken) {
428
- storedProfile.readOnlyToken = undefined;
429
- }
430
- },
431
- configPath,
432
- );
433
- }