@fulldivevr/workcord-cli 1.0.0 → 1.1.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 (77) hide show
  1. package/README.md +67 -25
  2. package/dist/auth.js +38 -4
  3. package/dist/client.js +5 -5
  4. package/dist/commands/auth.js +7 -2
  5. package/dist/commands/challenges.js +2 -7
  6. package/dist/commands/custom-agents.js +3 -8
  7. package/dist/commands/dm.js +2 -3
  8. package/dist/commands/messages.js +8 -21
  9. package/dist/commands/room-agents.js +2 -7
  10. package/dist/commands/rooms.js +24 -31
  11. package/dist/commands/users.js +4 -9
  12. package/dist/commands/whoami.js +24 -7
  13. package/dist/generated/commands/activity.js +3 -6
  14. package/dist/generated/commands/ai.js +3 -6
  15. package/dist/generated/commands/ambassador.js +3 -6
  16. package/dist/generated/commands/apple-iap.js +4 -9
  17. package/dist/generated/commands/attribution.js +6 -15
  18. package/dist/generated/commands/auth.js +9 -24
  19. package/dist/generated/commands/challenges.js +13 -36
  20. package/dist/generated/commands/clips.js +3 -6
  21. package/dist/generated/commands/contact-match.js +7 -18
  22. package/dist/generated/commands/content-crawler.js +2 -3
  23. package/dist/generated/commands/custom-agents.js +22 -63
  24. package/dist/generated/commands/desktop-releases.js +5 -12
  25. package/dist/generated/commands/discover.js +9 -24
  26. package/dist/generated/commands/dm.js +10 -27
  27. package/dist/generated/commands/drafts.js +3 -6
  28. package/dist/generated/commands/embed.js +3 -6
  29. package/dist/generated/commands/explore.js +2 -3
  30. package/dist/generated/commands/feed.js +4 -9
  31. package/dist/generated/commands/feedback.js +2 -3
  32. package/dist/generated/commands/gamification.js +3 -6
  33. package/dist/generated/commands/google-play-iap.js +4 -9
  34. package/dist/generated/commands/health.js +3 -6
  35. package/dist/generated/commands/internal.js +9 -24
  36. package/dist/generated/commands/interview.js +4 -9
  37. package/dist/generated/commands/invite-redirect.js +2 -3
  38. package/dist/generated/commands/jesuscord-admin.js +3 -6
  39. package/dist/generated/commands/join.js +2 -3
  40. package/dist/generated/commands/kits.js +7 -18
  41. package/dist/generated/commands/meetings.js +6 -15
  42. package/dist/generated/commands/message-folders.js +5 -12
  43. package/dist/generated/commands/messages.js +17 -48
  44. package/dist/generated/commands/one-emulator.js +2 -3
  45. package/dist/generated/commands/pdf-export.js +2 -3
  46. package/dist/generated/commands/pin-folders.js +5 -12
  47. package/dist/generated/commands/pinned-messages.js +4 -9
  48. package/dist/generated/commands/polls.js +6 -15
  49. package/dist/generated/commands/premium-feeds.js +2 -3
  50. package/dist/generated/commands/processors.js +2 -3
  51. package/dist/generated/commands/questions.js +3 -6
  52. package/dist/generated/commands/referrals.js +2 -3
  53. package/dist/generated/commands/reports.js +2 -3
  54. package/dist/generated/commands/room-a2-a.js +6 -15
  55. package/dist/generated/commands/room-agents.js +13 -36
  56. package/dist/generated/commands/room-invites.js +9 -24
  57. package/dist/generated/commands/room-owned-agent.js +3 -6
  58. package/dist/generated/commands/room-page.js +2 -3
  59. package/dist/generated/commands/rooms.js +45 -132
  60. package/dist/generated/commands/saved-messages.js +4 -9
  61. package/dist/generated/commands/saved-stories.js +7 -18
  62. package/dist/generated/commands/search.js +2 -3
  63. package/dist/generated/commands/shadow-home-agent.js +3 -6
  64. package/dist/generated/commands/shadow.js +5 -12
  65. package/dist/generated/commands/share-cards.js +4 -9
  66. package/dist/generated/commands/share.js +2 -3
  67. package/dist/generated/commands/sponsors.js +2 -3
  68. package/dist/generated/commands/stories.js +12 -33
  69. package/dist/generated/commands/story-by-id.js +2 -3
  70. package/dist/generated/commands/submissions.js +4 -9
  71. package/dist/generated/commands/subscriptions.js +8 -21
  72. package/dist/generated/commands/tips.js +3 -6
  73. package/dist/generated/commands/users.js +16 -45
  74. package/dist/generated/commands/voice-token.js +2 -3
  75. package/dist/generated/commands/well-known.js +3 -6
  76. package/dist/generated/commands/wrapped.js +2 -3
  77. package/package.json +3 -2
package/README.md CHANGED
@@ -21,9 +21,10 @@ npx @fulldivevr/workcord-cli --help
21
21
  | Setting | How |
22
22
  | --- | --- |
23
23
  | Base URL | `WORKCORD_API_URL` env var or `--base-url <url>` — **required, no default** |
24
- | Signing secret | `WORKCORD_JWT_SECRET` env var **only** — never a flag |
24
+ | User token | `WORKCORD_TOKEN` env var **only** — a per-user access token from `workcord auth login`; the normal way to authenticate |
25
+ | Signing secret | `WORKCORD_JWT_SECRET` env var **only** — never a flag; privileged, enables `--as` impersonation |
25
26
  | Internal admin key | `WORKCORD_INTERNAL_API_KEY`, internal commands only |
26
- | Acting user | `--as <userId>`, per invocation |
27
+ | Acting user | `--as <userId>`, per invocation — only with `WORKCORD_JWT_SECRET` |
27
28
 
28
29
  Base URL must be a bare origin (scheme + host, no path). The `/api/v1` prefix
29
30
  is added by the CLI.
@@ -40,33 +41,46 @@ is added by the CLI.
40
41
 
41
42
  ```bash
42
43
  export WORKCORD_API_URL=https://work-rooms-api.wizeup.app
43
- export WORKCORD_JWT_SECRET=<the server's JWT_SECRET>
44
+ export WORKCORD_TOKEN=<token from `workcord auth login`>
44
45
 
45
- workcord --as <userId> users get-me
46
+ workcord users get-me
46
47
  ```
47
48
 
48
49
  Output is JSON on stdout, errors on stderr, non-zero exit on failure.
49
50
 
50
51
  ### How auth works
51
52
 
52
- Three distinct modes.
53
+ Every request carries a bearer token. There are two ways to get one, and one
54
+ extra header for internal admin routes.
53
55
 
54
- **1. Normal user login (email code).** Deployed WorkCord/VIA stacks run with
55
- `ENABLE_EMAIL_AUTH=true` and mail a 6-digit code:
56
+ **1. A real user token (normal path).** Log in once, export the token, and every
57
+ command runs as that user with exactly that user's permissions:
56
58
 
57
59
  ```bash
58
60
  workcord auth login --email you@company.com
59
- # → code arrives by email, CLI prompts for it, prints { token, accessToken, refreshToken, user, isNewUser }
61
+ # → code arrives by email, CLI prompts for it, prints
62
+ # { token, accessToken, refreshToken, user, isNewUser } on stdout
63
+ # and an `export WORKCORD_TOKEN=...` hint on stderr
64
+
65
+ export WORKCORD_TOKEN=<token>
66
+ workcord users get-me
67
+ workcord whoami # → {"mode":"token","userId":"...","source":"WORKCORD_TOKEN"}
60
68
  ```
61
69
 
62
- Non-interactive (no TTY) it prints a `sessionId` and you finish with:
70
+ `WORKCORD_TOKEN` is sent as-is in `Authorization: Bearer`; the CLI never signs
71
+ anything in this mode and `--as` is not needed. This is the least-privilege
72
+ setup and the right one for agents.
73
+
74
+ Non-interactive (no TTY) `auth login --email` prints a `sessionId` and you
75
+ finish with:
63
76
 
64
77
  ```bash
65
78
  workcord auth verify-email --session-id <id> --code 123456
66
79
  ```
67
80
 
68
81
  Email login returns `accessToken`; phone/social login returns `token`. The CLI
69
- emits both fields on either path so callers can read one consistently.
82
+ emits both fields on either path so callers can read one consistently — either
83
+ value works as `WORKCORD_TOKEN`.
70
84
 
71
85
  Email auth is domain-restricted. WorkCord prod and VIA both allow
72
86
  `fulldive.com`, `via.com`, `via.com.tw`, `viatech.com.cn`, `*.tvbs.com`,
@@ -78,16 +92,37 @@ Email auth is domain-restricted. WorkCord prod and VIA both allow
78
92
  set `AUTH_PHONE_OTP_DEV_BYPASS`, so `POST /auth/send-otp` returns 503
79
93
  `OTP_NOT_AVAILABLE` — the CLI turns that into an explicit "use `--email`" error.
80
94
 
81
- **2. Impersonation / agent auth.** With `WORKCORD_JWT_SECRET` set, the global
82
- `--as <userId>` flag mints an HS256 JWT locally (payload `{userId, id}`, 60-day
83
- expiry) instead of logging in. `WORKCORD_JWT_SECRET` must equal the **server's
84
- `JWT_SECRET`** env var — the names deliberately differ, and this is the most
85
- common setup mistake.
95
+ Tokens expire. On `401`, log in again and re-export `WORKCORD_TOKEN`.
96
+
97
+ **2. Local minting / impersonation (privileged, trusted CI only).** With
98
+ `WORKCORD_JWT_SECRET` set, the global `--as <userId>` flag mints an HS256 JWT
99
+ locally (payload `{userId, id}`, 60-day expiry) instead of logging in.
100
+ `WORKCORD_JWT_SECRET` must equal the **server's `JWT_SECRET`** env var — the
101
+ names deliberately differ, and this is the most common setup mistake.
102
+
103
+ ```bash
104
+ export WORKCORD_JWT_SECRET=<the server's JWT_SECRET>
105
+ workcord --as <userId> users get-me
106
+ ```
86
107
 
87
108
  Same secret on both sides means the CLI can act as **any** user, with no
88
109
  password and no server-side check. Treat it as privileged signing material:
89
110
  keep it out of source control, package metadata, logs, screenshots and public
90
- examples, and run this CLI only from trusted machines and CI jobs.
111
+ examples, and run this CLI only from trusted machines and CI jobs. Prefer
112
+ `WORKCORD_TOKEN` unless you actually need to switch identities.
113
+
114
+ Resolution order, in one place (`resolveAuth` in `src/auth.ts`):
115
+
116
+ | Situation | Result |
117
+ | --- | --- |
118
+ | `--as <userId>` passed, `WORKCORD_JWT_SECRET` set | mint and impersonate that user |
119
+ | `--as <userId>` passed, no secret | error — set the secret or drop `--as` |
120
+ | no `--as`, `WORKCORD_TOKEN` set | send that token verbatim |
121
+ | no `--as`, only `WORKCORD_JWT_SECRET` set | error — `--as <userId>` required |
122
+ | neither set | error naming both options |
123
+
124
+ `--as` wins over `WORKCORD_TOKEN` when both are available: passing it is an
125
+ explicit statement of impersonation intent.
91
126
 
92
127
  **3. Internal admin routes.** `WORKCORD_INTERNAL_API_KEY` is sent as the
93
128
  `x-internal-key` header; the server compares it to its own `INTERNAL_API_KEY`.
@@ -99,6 +134,9 @@ Required for `/internal/*` and the jesuscord admin routes. Treat it like the sig
99
134
  workcord [--as <userId>] <namespace> <command> [flags]
100
135
  ```
101
136
 
137
+ `--as` is only needed when minting with `WORKCORD_JWT_SECRET`; with
138
+ `WORKCORD_TOKEN` set, omit it everywhere.
139
+
102
140
  - `workcord --help` lists namespaces.
103
141
  - `workcord <namespace> --help` lists that namespace's commands.
104
142
  - `workcord <namespace> <command> --help` shows the command's flags.
@@ -133,8 +171,8 @@ profile fields.
133
171
 
134
172
  Server auto-extracts mention tokens from visible `@Name` text. Prefer
135
173
  `messages send-text --mention-user USER_ID` so the CLI resolves the room
136
- member's display name and avoids exposing raw user ids. `auth login` is the
137
- only semantic command that doesn't take `--as` (it returns one).
174
+ member's display name and avoids exposing raw user ids. `auth login` never
175
+ takes `--as` — it is how you obtain a `WORKCORD_TOKEN` in the first place.
138
176
 
139
177
  ### Error handling
140
178
 
@@ -196,8 +234,9 @@ from each room, merges them by `sentAt` descending, and includes
196
234
 
197
235
  **Switch user identity per call:**
198
236
 
199
- The CLI is stateless — `--as <userId>` per invocation. One process can act as
200
- 100 users by changing the flag.
237
+ The CLI is stateless. With `WORKCORD_TOKEN` the identity is the token's owner;
238
+ swap the env var to swap users. With `WORKCORD_JWT_SECRET` one process can act
239
+ as 100 users by changing `--as <userId>` per invocation.
201
240
 
202
241
  **Onboard a fresh agent identity:**
203
242
 
@@ -255,7 +294,7 @@ without running `regen` if you don't need the latest server changes.
255
294
  ```
256
295
  src/
257
296
  ├── cli.ts commander entry, global --as flag
258
- ├── auth.ts mints HS256 JWT from --as
297
+ ├── auth.ts auth resolution: WORKCORD_TOKEN vs minted --as JWT
259
298
  ├── client.ts fetch wrapper (JSON + multipart)
260
299
  ├── commands/ hand-written semantic commands
261
300
  └── generated/ auto-generated from openapi.json
@@ -266,7 +305,9 @@ scripts/
266
305
  ├── sync-spec.ts backend build → openapi.json
267
306
  └── generate-commands.ts spec → commander commands
268
307
 
269
- tests/smoke.sh hermetic end-to-end test
308
+ tests/
309
+ ├── auth.test.ts node --test suite for auth resolution
310
+ └── smoke.sh end-to-end test, needs a live server
270
311
  ```
271
312
 
272
313
  ### Adding a hand-written semantic command
@@ -283,7 +324,8 @@ flags (e.g. typed fields instead of an opaque `--body`):
283
324
  ### Testing
284
325
 
285
326
  ```bash
286
- npm run smoke
327
+ npm test # hermetic, no network — auth resolution rules
328
+ npm run smoke # end-to-end, needs a live server
287
329
  ```
288
330
 
289
331
  Hermetic end-to-end suite (creates two fresh users per run via OTP, exercises
@@ -297,14 +339,14 @@ Publishing is a manual maintainer action — there is no auto-publish CI.
297
339
 
298
340
  1. `npm run regen` — refresh the spec and generated commands; commit the diff.
299
341
  2. `npm run typecheck`
300
- 3. `npm run build`
342
+ 3. `npm test` then `npm run build`
301
343
  4. `npm pack --dry-run` — confirm the file list is exactly `dist/**/*.js`,
302
344
  `README.md`, `LICENSE`, `package.json`. No sources, no `openapi.json`, no
303
345
  `.npmrc`.
304
346
  5. Smoke: `node dist/cli.js --help` and `node dist/cli.js --version`.
305
347
  6. Bump `version` in `package.json`, then `npm publish`.
306
348
 
307
- `prepack` re-runs `typecheck` + `build`, so steps 2–3 also gate the publish.
349
+ `prepack` re-runs `typecheck` + `test` + `build`, so steps 2–3 also gate the publish.
308
350
 
309
351
  ## License
310
352
 
package/dist/auth.js CHANGED
@@ -1,9 +1,43 @@
1
1
  import jwt from "jsonwebtoken";
2
- export function mintToken(userId) {
3
- const secret = process.env.WORKCORD_JWT_SECRET;
4
- if (!secret) {
2
+ export function actingUserId(program) {
3
+ return program.opts().as;
4
+ }
5
+ export function mintToken(userId, secret) {
6
+ const key = secret ?? process.env.WORKCORD_JWT_SECRET;
7
+ if (!key) {
5
8
  throw new Error("WORKCORD_JWT_SECRET not set");
6
9
  }
7
- return jwt.sign({ userId, id: userId }, secret, { algorithm: "HS256", expiresIn: "60d" });
10
+ return jwt.sign({ userId, id: userId }, key, { algorithm: "HS256", expiresIn: "60d" });
11
+ }
12
+ export function resolveAuth(asUserId, env = process.env) {
13
+ const secret = env.WORKCORD_JWT_SECRET?.trim() || undefined;
14
+ const token = env.WORKCORD_TOKEN?.trim() || undefined;
15
+ if (asUserId) {
16
+ if (!secret) {
17
+ throw new Error("--as <userId> mints a token and needs WORKCORD_JWT_SECRET. Set it, or drop --as and set WORKCORD_TOKEN to a token from `workcord auth login`.");
18
+ }
19
+ const minted = mintToken(asUserId, secret);
20
+ return { mode: "minted", bearer: `Bearer ${minted}`, token: minted, userId: asUserId };
21
+ }
22
+ if (token) {
23
+ return { mode: "token", bearer: `Bearer ${token}`, token };
24
+ }
25
+ if (secret) {
26
+ throw new Error("--as <userId> is required when authenticating with WORKCORD_JWT_SECRET");
27
+ }
28
+ throw new Error("No credentials. Set WORKCORD_TOKEN to a token from `workcord auth login`, or set WORKCORD_JWT_SECRET and pass --as <userId>.");
29
+ }
30
+ export function decodeJwtPayload(token) {
31
+ const parts = token.split(".");
32
+ if (parts.length !== 3)
33
+ return null;
34
+ try {
35
+ const json = Buffer.from(parts[1], "base64url").toString("utf8");
36
+ const parsed = JSON.parse(json);
37
+ return parsed && typeof parsed === "object" ? parsed : null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
8
42
  }
9
43
  //# sourceMappingURL=auth.js.map
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mintToken } from "./auth.js";
1
+ import { resolveAuth } from "./auth.js";
2
2
  let baseUrlOverride;
3
3
  export function setBaseUrlOverride(url) {
4
4
  baseUrlOverride = url;
@@ -52,6 +52,7 @@ async function fetchWithRetry(build) {
52
52
  throw lastErr;
53
53
  }
54
54
  export async function call({ method = "GET", path, query, body, headers, asUserId }) {
55
+ const { bearer } = resolveAuth(asUserId);
55
56
  const base = getBaseUrl();
56
57
  const url = new URL(base + path);
57
58
  if (query) {
@@ -60,14 +61,13 @@ export async function call({ method = "GET", path, query, body, headers, asUserI
60
61
  url.searchParams.set(k, String(v));
61
62
  }
62
63
  }
63
- const token = mintToken(asUserId);
64
64
  const res = await fetchWithRetry(() => ({
65
65
  url,
66
66
  init: {
67
67
  method,
68
68
  headers: {
69
69
  "content-type": "application/json",
70
- authorization: `Bearer ${token}`,
70
+ authorization: bearer,
71
71
  ...headers,
72
72
  },
73
73
  body: body !== undefined ? JSON.stringify(body) : undefined,
@@ -92,9 +92,9 @@ function safeJson(s) {
92
92
  }
93
93
  }
94
94
  export async function callMultipart({ path, asUserId, fields, file }) {
95
+ const { bearer } = resolveAuth(asUserId);
95
96
  const base = getBaseUrl();
96
97
  const url = base + path;
97
- const token = mintToken(asUserId);
98
98
  const res = await fetchWithRetry(() => {
99
99
  const fd = new FormData();
100
100
  if (fields) {
@@ -107,7 +107,7 @@ export async function callMultipart({ path, asUserId, fields, file }) {
107
107
  url,
108
108
  init: {
109
109
  method: "POST",
110
- headers: { authorization: `Bearer ${token}` },
110
+ headers: { authorization: bearer },
111
111
  body: fd,
112
112
  },
113
113
  };
@@ -38,7 +38,11 @@ function normalize(data) {
38
38
  return r;
39
39
  }
40
40
  function emit(data) {
41
- process.stdout.write(JSON.stringify(normalize(data)) + "\n");
41
+ const result = normalize(data);
42
+ process.stdout.write(JSON.stringify(result) + "\n");
43
+ if (result.token) {
44
+ process.stderr.write(`export WORKCORD_TOKEN=${result.token}\n`);
45
+ }
42
46
  }
43
47
  async function promptCode() {
44
48
  const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -96,7 +100,8 @@ export function registerAuth(program) {
96
100
  .command("login")
97
101
  .description("End-to-end login. Returns { token, accessToken, refreshToken, user, isNewUser }. " +
98
102
  "--email uses the emailed 6-digit code (deployed WorkCord/VIA); --phone uses OTP " +
99
- "(local/RoomCord only). Does NOT use --as / WORKCORD_JWT_SECRET — the server mints the token.")
103
+ "(local/RoomCord only). Does NOT use --as / WORKCORD_JWT_SECRET — the server mints the token. " +
104
+ "Export the returned token as WORKCORD_TOKEN to authenticate every other command.")
100
105
  .option("--email <address>", "email address (mutually exclusive with --phone)")
101
106
  .option("--phone <e164>", 'phone number in E.164 format (e.g. "+14155550199")')
102
107
  .option("--code <code>", "verification code; prompted interactively for --email when omitted")
@@ -1,11 +1,6 @@
1
+ import { actingUserId } from "../auth.js";
1
2
  import { call } from "../client.js";
2
3
  import { getOrCreateGroup } from "./_group.js";
3
- function currentUser(program) {
4
- const { as } = program.opts();
5
- if (!as)
6
- throw new Error("--as <userId> is required");
7
- return as;
8
- }
9
4
  /**
10
5
  * Custom challenge commands that are NOT covered by the OpenAPI-generated
11
6
  * commands (src/generated/commands/challenges.ts). Everything else — create,
@@ -34,7 +29,7 @@ export function registerChallenges(program) {
34
29
  const data = (await call({
35
30
  method: "POST",
36
31
  path: `/api/v1/challenges/${encodeURIComponent(opts.id)}/regenerate-cards?offset=${offset}&limit=10`,
37
- asUserId: currentUser(program),
32
+ asUserId: actingUserId(program),
38
33
  body: {},
39
34
  }));
40
35
  totals.total = data.total;
@@ -1,12 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { actingUserId } from "../auth.js";
2
3
  import { call } from "../client.js";
3
4
  import { getOrCreateGroup } from "./_group.js";
4
- function currentUser(program) {
5
- const { as } = program.opts();
6
- if (!as)
7
- throw new Error("--as <userId> is required");
8
- return as;
9
- }
10
5
  function bool(v, name) {
11
6
  if (v === undefined)
12
7
  return undefined;
@@ -54,7 +49,7 @@ export function registerCustomAgents(program) {
54
49
  const data = await call({
55
50
  method: "POST",
56
51
  path: "/api/v1/agents",
57
- asUserId: currentUser(program),
52
+ asUserId: actingUserId(program),
58
53
  body: {
59
54
  name: opts.name,
60
55
  description: opts.description,
@@ -111,7 +106,7 @@ export function registerCustomAgents(program) {
111
106
  const data = await call({
112
107
  method: "PATCH",
113
108
  path: `/api/v1/agents/${encodeURIComponent(opts.agent)}`,
114
- asUserId: currentUser(program),
109
+ asUserId: actingUserId(program),
115
110
  body,
116
111
  });
117
112
  process.stdout.write(JSON.stringify(data) + "\n");
@@ -1,3 +1,4 @@
1
+ import { actingUserId } from "../auth.js";
1
2
  import { call } from "../client.js";
2
3
  import { getOrCreateGroup } from "./_group.js";
3
4
  export function registerDm(program) {
@@ -10,9 +11,7 @@ export function registerDm(program) {
10
11
  .requiredOption("--text <text>", "message body — mention users with @userId inline")
11
12
  .option("--reply-to <messageId>", "parent message id (for threaded replies)")
12
13
  .action(async (opts) => {
13
- const { as } = program.opts();
14
- if (!as)
15
- throw new Error("--as <userId> is required");
14
+ const as = actingUserId(program);
16
15
  const dm = (await call({
17
16
  method: "POST",
18
17
  path: "/api/v1/dm/start",
@@ -1,3 +1,4 @@
1
+ import { actingUserId } from "../auth.js";
1
2
  import { call } from "../client.js";
2
3
  import { getOrCreateGroup } from "./_group.js";
3
4
  const MAX_LIMIT = 500;
@@ -113,9 +114,7 @@ export function registerMessages(program) {
113
114
  .option("--attachments <urls>", "comma-separated GCS URLs (use `workcord rooms upload-image-file` to get URLs first)")
114
115
  .option("--no-shadow-guidance", "do not opt in to Shadow guidance cards")
115
116
  .action(async (opts) => {
116
- const { as } = program.opts();
117
- if (!as)
118
- throw new Error("--as <userId> is required");
117
+ const as = actingUserId(program);
119
118
  const mentions = await resolveMentions(opts.room, as, opts.mentionUser);
120
119
  const content = mentions.length > 0 ? `${mentions.map((m) => `@${m.name}`).join(" ")} ${opts.text}` : opts.text;
121
120
  const body = {
@@ -143,9 +142,7 @@ export function registerMessages(program) {
143
142
  .option("--after <iso>", "only messages with sentAt > this ISO timestamp; overrides --cursor server-side")
144
143
  .option("--filter <filter>", `one of: ${Array.from(MESSAGE_FILTERS).join(", ")}`)
145
144
  .action(async (opts) => {
146
- const { as } = program.opts();
147
- if (!as)
148
- throw new Error("--as <userId> is required");
145
+ const as = actingUserId(program);
149
146
  const data = await call({
150
147
  method: "GET",
151
148
  path: `/api/v1/rooms/${encodeURIComponent(opts.room)}/messages`,
@@ -169,9 +166,7 @@ export function registerMessages(program) {
169
166
  .option("--limit <n>", `page size, 1-${MAX_PAGE_LIMIT}`, "20")
170
167
  .option("--cursor <iso>", "pagination cursor from the previous page's cursor")
171
168
  .action(async (opts) => {
172
- const { as } = program.opts();
173
- if (!as)
174
- throw new Error("--as <userId> is required");
169
+ const as = actingUserId(program);
175
170
  const data = await call({
176
171
  method: "GET",
177
172
  path: `/api/v1/rooms/${encodeURIComponent(opts.room)}/messages/${encodeURIComponent(opts.message)}/thread`,
@@ -190,9 +185,7 @@ export function registerMessages(program) {
190
185
  .requiredOption("--room <roomId>", "target room id")
191
186
  .requiredOption("--message <messageId>", "thread root message id")
192
187
  .action(async (opts) => {
193
- const { as } = program.opts();
194
- if (!as)
195
- throw new Error("--as <userId> is required");
188
+ const as = actingUserId(program);
196
189
  const data = await call({
197
190
  method: "POST",
198
191
  path: `/api/v1/rooms/${encodeURIComponent(opts.room)}/messages/${encodeURIComponent(opts.message)}/thread/read`,
@@ -209,9 +202,7 @@ export function registerMessages(program) {
209
202
  .option("--limit <n>", "max messages per page (server caps at 100)", "50")
210
203
  .option("--max-pages <n>", "safety cap on pages walked back", "20")
211
204
  .action(async (opts) => {
212
- const { as } = program.opts();
213
- if (!as)
214
- throw new Error("--as <userId> is required");
205
+ const as = actingUserId(program);
215
206
  const sinceMs = new Date(opts.since).getTime();
216
207
  if (Number.isNaN(sinceMs))
217
208
  throw new Error(`--since not a valid ISO timestamp: ${opts.since}`);
@@ -259,9 +250,7 @@ export function registerMessages(program) {
259
250
  .option("--inbox-category <value>", "room inbox category filter")
260
251
  .option("--no-shadow-guidance", "do not opt in to Shadow guidance cards")
261
252
  .action(async (opts) => {
262
- const { as } = program.opts();
263
- if (!as)
264
- throw new Error("--as <userId> is required");
253
+ const as = actingUserId(program);
265
254
  const limit = parsePositiveInt(opts.limit, "--limit", MAX_LIMIT);
266
255
  const perRoomLimit = parsePositiveInt(opts.perRoomLimit, "--per-room-limit", MAX_PAGE_LIMIT);
267
256
  const roomPageLimit = parsePositiveInt(opts.roomPageLimit, "--room-page-limit", MAX_PAGE_LIMIT);
@@ -345,9 +334,7 @@ export function registerMessages(program) {
345
334
  .requiredOption("--message <messageId>", "target message")
346
335
  .requiredOption("--emoji <type>", 'reaction type — emoji or id (e.g. "👍" or "thumbs_up")')
347
336
  .action(async (opts) => {
348
- const { as } = program.opts();
349
- if (!as)
350
- throw new Error("--as <userId> is required");
337
+ const as = actingUserId(program);
351
338
  const data = await call({
352
339
  method: "POST",
353
340
  path: `/api/v1/rooms/${encodeURIComponent(opts.room)}/messages/${encodeURIComponent(opts.message)}/reactions`,
@@ -1,11 +1,6 @@
1
+ import { actingUserId } from "../auth.js";
1
2
  import { call } from "../client.js";
2
3
  import { getOrCreateGroup } from "./_group.js";
3
- function currentUser(program) {
4
- const { as } = program.opts();
5
- if (!as)
6
- throw new Error("--as <userId> is required");
7
- return as;
8
- }
9
4
  function bool(v, name) {
10
5
  if (v === undefined)
11
6
  return undefined;
@@ -24,7 +19,7 @@ export function registerRoomAgents(program) {
24
19
  .requiredOption("--agent <agentId>", "hosted custom/system agent id")
25
20
  .option("--reply-in-thread <true|false>", "whether the agent replies in threads")
26
21
  .action(async (opts) => {
27
- const asUserId = currentUser(program);
22
+ const asUserId = actingUserId(program);
28
23
  const installed = (await call({
29
24
  method: "POST",
30
25
  path: `/api/v1/rooms/${encodeURIComponent(opts.room)}/agents/install/${encodeURIComponent(opts.agent)}`,