@indigoai-us/hq-cli 5.36.1 → 5.36.3

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.
@@ -13,11 +13,11 @@
13
13
  * hq sync status — show local journal summary
14
14
  */
15
15
 
16
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="94a293c9-eb80-5100-a3e0-d64c93f7165c")}catch(e){}}();
16
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="856084be-3bbb-50cd-8e3b-1e19ea13f4fb")}catch(e){}}();
17
17
  import chalk from "chalk";
18
18
  import * as fs from "fs";
19
19
  import * as path from "path";
20
- import { share, sync, readJournal, getJournalPath, loadCachedTokens, VaultClient, computePersonalVaultPaths, } from "@indigoai-us/hq-cloud";
20
+ import { share, sync, readJournal, getJournalPath, loadCachedTokens, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, } from "@indigoai-us/hq-cloud";
21
21
  import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
22
22
  import { emitNarrowHint, isStrictRefusal, resolveBannerLevel, } from "../lib/narrow-hint-banner.js";
23
23
  /**
@@ -89,13 +89,18 @@ export async function pullAll(options, deps) {
89
89
  }
90
90
  const personal = options.skipPersonal ? null : pickCanonicalPerson(persons);
91
91
  if (personal) {
92
+ // Canonical slug, NOT the literal "personal": that legacy slug is the
93
+ // companies/personal company's journal. hq-sync-runner moved the vault
94
+ // slot to PERSONAL_VAULT_JOURNAL_SLUG; hq-cli journaling the same vault
95
+ // under the old slug split the bookkeeping across two files — every CLI
96
+ // sync re-flagged the menubar's already-synced files as conflicts.
92
97
  plan.push({
93
98
  slug: "personal",
94
99
  syncOptions: {
95
100
  company: personal.uid,
96
101
  hqRoot: options.hqRoot,
97
102
  personalMode: true,
98
- journalSlug: "personal",
103
+ journalSlug: PERSONAL_VAULT_JOURNAL_SLUG,
99
104
  ...(options.onConflict ? { onConflict: options.onConflict } : {}),
100
105
  },
101
106
  });
@@ -213,7 +218,7 @@ export async function pushAll(options, deps) {
213
218
  hqRoot: options.hqRoot,
214
219
  paths: computePersonalVaultPaths(options.hqRoot),
215
220
  personalMode: true,
216
- journalSlug: "personal",
221
+ journalSlug: PERSONAL_VAULT_JOURNAL_SLUG,
217
222
  skipUnchanged: true,
218
223
  propagateDeletes: true,
219
224
  propagateDeletePolicy: resolveDeletePolicy(),
@@ -481,7 +486,7 @@ export function registerCloudCommands(program) {
481
486
  getEntity: async () => null,
482
487
  });
483
488
  personalMode = true;
484
- journalSlug = "personal";
489
+ journalSlug = PERSONAL_VAULT_JOURNAL_SLUG;
485
490
  }
486
491
  // Default paths. For `--personal` with no explicit paths, default to
487
492
  // the canonical personal-vault top-level scope so re-runs from a
@@ -833,7 +838,7 @@ async function runPullPersonal(hqRoot, onConflict) {
833
838
  vaultConfig,
834
839
  hqRoot,
835
840
  personalMode: true,
836
- journalSlug: "personal",
841
+ journalSlug: PERSONAL_VAULT_JOURNAL_SLUG,
837
842
  });
838
843
  if (result.aborted) {
839
844
  console.log(chalk.yellow(`\n⚠ Pull aborted (${result.filesDownloaded} downloaded, ${result.filesSkipped} skipped, ${result.conflicts} conflicts)`));
@@ -954,7 +959,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
954
959
  getEntity: async () => null,
955
960
  });
956
961
  personalMode = true;
957
- journalSlug = "personal";
962
+ journalSlug = PERSONAL_VAULT_JOURNAL_SLUG;
958
963
  pushPaths = computePersonalVaultPaths(hqRoot);
959
964
  }
960
965
  else {
@@ -1163,4 +1168,4 @@ function resolveUploadAuthorFromCache() {
1163
1168
  }
1164
1169
  }
1165
1170
  //# sourceMappingURL=cloud.js.map
1166
- //# debugId=94a293c9-eb80-5100-a3e0-d64c93f7165c
1171
+ //# debugId=856084be-3bbb-50cd-8e3b-1e19ea13f4fb
@@ -37,5 +37,38 @@ export declare function buildDmBody(args: {
37
37
  inDelay?: string;
38
38
  now: number;
39
39
  }): DmSendBody;
40
+ /** A pending incoming connection request, as returned by the backend. */
41
+ export interface ConnectionRequest {
42
+ pairKey: string;
43
+ fromPersonUid: string;
44
+ fromDisplayName?: string;
45
+ fromEmail?: string;
46
+ message?: string;
47
+ createdAt?: string;
48
+ }
49
+ /**
50
+ * Resolve a connection request from a pending-requests list by a caller-supplied
51
+ * identifier (email, personUid, or pairKey). Pure → unit-testable.
52
+ *
53
+ * Matching is case-insensitive for emails. Returns the matched request, or null
54
+ * if no request matches.
55
+ */
56
+ export declare function matchRequest(requests: ConnectionRequest[], identifier: string): ConnectionRequest | null;
57
+ /**
58
+ * Build the POST body for an accept/decline/block action. Prefers a resolved
59
+ * pairKey (from a matched pending request); otherwise falls back to the
60
+ * recipient identifier the backend can resolve (`withPersonUid` for a prs_ UID,
61
+ * `toEmail`-style is not supported by the connections endpoints so we send
62
+ * `withEmail`). Pure → unit-testable.
63
+ *
64
+ * Throws Error with a user-facing message when the identifier can't be resolved.
65
+ */
66
+ export declare function buildConnectionActionBody(identifier: string, matched: ConnectionRequest | null): {
67
+ pairKey: string;
68
+ } | {
69
+ withPersonUid: string;
70
+ } | {
71
+ withEmail: string;
72
+ };
40
73
  export declare function registerDmCommand(program: Command): void;
41
74
  //# sourceMappingURL=dm.d.ts.map
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="69d48194-d5ae-508e-b614-68f2066db6ee")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="19c5d8e0-ec0a-5b57-b3dc-e2209b60cf2f")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { ensureCognitoToken } from "../utils/cognito-session.js";
@@ -89,10 +89,179 @@ function friendlyDmError(status, code, fallback) {
89
89
  return `Server error: ${fallback}`;
90
90
  return fallback;
91
91
  }
92
+ /**
93
+ * Resolve a connection request from a pending-requests list by a caller-supplied
94
+ * identifier (email, personUid, or pairKey). Pure → unit-testable.
95
+ *
96
+ * Matching is case-insensitive for emails. Returns the matched request, or null
97
+ * if no request matches.
98
+ */
99
+ export function matchRequest(requests, identifier) {
100
+ const id = identifier.trim();
101
+ const idLower = id.toLowerCase();
102
+ for (const r of requests) {
103
+ if (r.pairKey === id)
104
+ return r;
105
+ if (r.fromPersonUid === id)
106
+ return r;
107
+ if (r.fromEmail && r.fromEmail.toLowerCase() === idLower)
108
+ return r;
109
+ }
110
+ return null;
111
+ }
112
+ /**
113
+ * Build the POST body for an accept/decline/block action. Prefers a resolved
114
+ * pairKey (from a matched pending request); otherwise falls back to the
115
+ * recipient identifier the backend can resolve (`withPersonUid` for a prs_ UID,
116
+ * `toEmail`-style is not supported by the connections endpoints so we send
117
+ * `withEmail`). Pure → unit-testable.
118
+ *
119
+ * Throws Error with a user-facing message when the identifier can't be resolved.
120
+ */
121
+ export function buildConnectionActionBody(identifier, matched) {
122
+ if (matched) {
123
+ return { pairKey: matched.pairKey };
124
+ }
125
+ const r = identifier.trim();
126
+ const rcpt = detectRecipient(r);
127
+ if (rcpt?.toPersonUid)
128
+ return { withPersonUid: rcpt.toPersonUid };
129
+ if (rcpt?.toEmail)
130
+ return { withEmail: rcpt.toEmail };
131
+ throw new Error(`Could not resolve '${identifier}' to a pending request — pass the requester's email, personUid, or the pairKey from \`hq dm requests\`.`);
132
+ }
133
+ const CONNECTION_ACTIONS = {
134
+ accept: { path: "accept", done: "Connection accepted" },
135
+ decline: { path: "decline", done: "Request declined" },
136
+ block: { path: "block", done: "Blocked" },
137
+ };
138
+ /** Fetch the caller's pending incoming connection requests. */
139
+ async function fetchRequests(token) {
140
+ const res = await vaultApiFetch({
141
+ token,
142
+ path: "/v1/notify/connections/requests",
143
+ });
144
+ if (!res.ok) {
145
+ const err = (await res.json().catch(() => ({})));
146
+ throw new Error(friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText));
147
+ }
148
+ const data = (await res.json());
149
+ return data.requests ?? [];
150
+ }
151
+ async function runConnectionAction(action, identifier) {
152
+ try {
153
+ const token = await ensureCognitoToken();
154
+ // Resolve the pairKey from the caller's pending requests when possible so
155
+ // accept/decline/block work against a friendly email or personUid.
156
+ const requests = await fetchRequests(token).catch(() => []);
157
+ const matched = matchRequest(requests, identifier);
158
+ const body = buildConnectionActionBody(identifier, matched);
159
+ const res = await vaultApiFetch({
160
+ token,
161
+ path: `/v1/notify/connections/${CONNECTION_ACTIONS[action].path}`,
162
+ method: "POST",
163
+ body: body,
164
+ });
165
+ if (!res.ok) {
166
+ const err = (await res.json().catch(() => ({})));
167
+ console.error(chalk.red(friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText)));
168
+ process.exit(1);
169
+ }
170
+ const who = matched?.fromDisplayName ?? matched?.fromEmail ?? identifier;
171
+ console.log(chalk.green(`${CONNECTION_ACTIONS[action].done} — ${who}.`));
172
+ if (action === "accept" && matched?.message) {
173
+ console.log(chalk.dim("Their held message will now be delivered."));
174
+ }
175
+ }
176
+ catch (err) {
177
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
178
+ process.exit(1);
179
+ }
180
+ }
181
+ async function runDmSend(recipient, message, opts) {
182
+ try {
183
+ // Resolve prompt/details from inline text or a file.
184
+ let prompt = opts.prompt;
185
+ if (opts.promptFile)
186
+ prompt = readFileSync(opts.promptFile, "utf8");
187
+ let details = opts.details;
188
+ if (opts.detailsFile)
189
+ details = readFileSync(opts.detailsFile, "utf8");
190
+ const reqBody = buildDmBody({
191
+ recipient,
192
+ message: message ?? "",
193
+ prompt,
194
+ details,
195
+ at: opts.at,
196
+ inDelay: opts.in,
197
+ now: Date.now(),
198
+ });
199
+ const token = await ensureCognitoToken();
200
+ const res = await vaultApiFetch({
201
+ token,
202
+ path: "/v1/notify/dm",
203
+ method: "POST",
204
+ body: reqBody,
205
+ });
206
+ if (!res.ok) {
207
+ const err = (await res.json().catch(() => ({})));
208
+ console.error(chalk.red(friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText)));
209
+ process.exit(1);
210
+ }
211
+ // 202 connection_requested: the recipient isn't connected yet, so the DM is
212
+ // held as a connection request rather than delivered. This is a success, not
213
+ // an error — surface it quietly.
214
+ const data = (await res.json());
215
+ if (res.status === 202 || data.state === "connection_requested") {
216
+ console.log(chalk.green(`Request sent — pending ${recipient}'s acceptance.`));
217
+ console.log(chalk.dim("Your message is held and delivers automatically once they accept."));
218
+ return;
219
+ }
220
+ if (data.scheduled) {
221
+ console.log(chalk.green(`Scheduled DM to ${recipient} for ${data.deliverAt} (eventId ${data.eventId}).`));
222
+ console.log(chalk.dim("It delivers within ~60s of that time, even if you're offline."));
223
+ }
224
+ else {
225
+ console.log(chalk.green(`DM sent to ${recipient} (eventId ${data.eventId}).`));
226
+ }
227
+ }
228
+ catch (err) {
229
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
230
+ process.exit(1);
231
+ }
232
+ }
233
+ async function runDmRequests() {
234
+ try {
235
+ const token = await ensureCognitoToken();
236
+ const requests = await fetchRequests(token);
237
+ if (requests.length === 0) {
238
+ console.log(chalk.dim("No pending connection requests."));
239
+ return;
240
+ }
241
+ console.log(chalk.green(`${requests.length} pending connection request${requests.length === 1 ? "" : "s"}:`));
242
+ for (const r of requests) {
243
+ const name = r.fromDisplayName ?? r.fromEmail ?? r.fromPersonUid;
244
+ const emailPart = r.fromEmail ? chalk.dim(` <${r.fromEmail}>`) : "";
245
+ console.log(`\n ${chalk.bold(name)}${emailPart}`);
246
+ if (r.message) {
247
+ console.log(` ${chalk.dim('"' + r.message + '"')}`);
248
+ }
249
+ const idHint = r.fromEmail ?? r.fromPersonUid ?? r.pairKey;
250
+ console.log(chalk.dim(` Accept: hq dm accept ${idHint} · Decline: hq dm decline ${idHint}`));
251
+ }
252
+ }
253
+ catch (err) {
254
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
255
+ process.exit(1);
256
+ }
257
+ }
92
258
  export function registerDmCommand(program) {
93
- program
94
- .command("dm <recipient> [message]")
95
- .description("Send a direct message to a teammate (email or personUid). They receive it as an HQ Sync notification.")
259
+ const dm = program
260
+ .command("dm")
261
+ .description("Send a direct message and manage connection requests.");
262
+ dm
263
+ .command("send <recipient> [message]", { isDefault: true, hidden: true })
264
+ .description("Send a direct message to someone (email or personUid). They receive it as an HQ Sync notification. If you aren't connected yet, it sends a connection request that holds your message.")
96
265
  .option("--prompt <text>", "Agent-context prompt the recipient can one-click copy into their agent")
97
266
  .option("--prompt-file <path>", "Read the agent prompt from a file")
98
267
  .option("--details <text>", "Longer detail shown in the recipient's DM detail window")
@@ -100,49 +269,32 @@ export function registerDmCommand(program) {
100
269
  .option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time)")
101
270
  .option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d")
102
271
  .action(async (recipient, message, opts) => {
103
- try {
104
- // Resolve prompt/details from inline text or a file.
105
- let prompt = opts.prompt;
106
- if (opts.promptFile)
107
- prompt = readFileSync(opts.promptFile, "utf8");
108
- let details = opts.details;
109
- if (opts.detailsFile)
110
- details = readFileSync(opts.detailsFile, "utf8");
111
- const reqBody = buildDmBody({
112
- recipient,
113
- message: message ?? "",
114
- prompt,
115
- details,
116
- at: opts.at,
117
- inDelay: opts.in,
118
- now: Date.now(),
119
- });
120
- const token = await ensureCognitoToken();
121
- const res = await vaultApiFetch({
122
- token,
123
- path: "/v1/notify/dm",
124
- method: "POST",
125
- body: reqBody,
126
- });
127
- if (!res.ok) {
128
- const err = (await res.json().catch(() => ({})));
129
- console.error(chalk.red(friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText)));
130
- process.exit(1);
131
- }
132
- const data = (await res.json());
133
- if (data.scheduled) {
134
- console.log(chalk.green(`Scheduled DM to ${recipient} for ${data.deliverAt} (eventId ${data.eventId}).`));
135
- console.log(chalk.dim("It delivers within ~60s of that time, even if you're offline."));
136
- }
137
- else {
138
- console.log(chalk.green(`DM sent to ${recipient} (eventId ${data.eventId}).`));
139
- }
140
- }
141
- catch (err) {
142
- console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
143
- process.exit(1);
144
- }
272
+ await runDmSend(recipient, message, opts);
273
+ });
274
+ dm
275
+ .command("requests")
276
+ .description("List your pending incoming connection requests.")
277
+ .action(async () => {
278
+ await runDmRequests();
279
+ });
280
+ dm
281
+ .command("accept <person>")
282
+ .description("Accept a pending connection request (by email, personUid, or pairKey). Their held message is then delivered.")
283
+ .action(async (person) => {
284
+ await runConnectionAction("accept", person);
285
+ });
286
+ dm
287
+ .command("decline <person>")
288
+ .description("Decline a pending connection request (by email, personUid, or pairKey).")
289
+ .action(async (person) => {
290
+ await runConnectionAction("decline", person);
291
+ });
292
+ dm
293
+ .command("block <person>")
294
+ .description("Block a person so they can't send you further requests (by email, personUid, or pairKey).")
295
+ .action(async (person) => {
296
+ await runConnectionAction("block", person);
145
297
  });
146
298
  }
147
299
  //# sourceMappingURL=dm.js.map
148
- //# debugId=69d48194-d5ae-508e-b614-68f2066db6ee
300
+ //# debugId=19c5d8e0-ec0a-5b57-b3dc-e2209b60cf2f
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.36.1",
3
+ "version": "5.36.3",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "clean": "rm -rf dist"
16
16
  },
17
17
  "dependencies": {
18
- "@indigoai-us/hq-cloud": "^6.2.2",
18
+ "@indigoai-us/hq-cloud": "^6.2.4",
19
19
  "@indigoai-us/hq-onboarding": "^0.1.0",
20
20
  "@sentry/node": "^10.49.0",
21
21
  "chalk": "^5.3.0",
@@ -140,7 +140,7 @@ describe("pullAll", () => {
140
140
  expect(sync.calls[2]).toMatchObject({
141
141
  company: "psn_alice",
142
142
  personalMode: true,
143
- journalSlug: "personal",
143
+ journalSlug: "__hq_personal_vault__",
144
144
  });
145
145
 
146
146
  expect(result.attempted).toBe(3);
@@ -300,7 +300,7 @@ describe("pullAll", () => {
300
300
  expect(sync.calls).toHaveLength(1);
301
301
  expect(sync.calls[0].company).toBe("psn_old");
302
302
  expect(sync.calls[0].personalMode).toBe(true);
303
- expect(sync.calls[0].journalSlug).toBe("personal");
303
+ expect(sync.calls[0].journalSlug).toBe("__hq_personal_vault__");
304
304
  });
305
305
 
306
306
  // ── 8/9/10. US-011 narrow-hint banner ─────────────────────────────────────
@@ -565,7 +565,7 @@ describe("pullAll", () => {
565
565
  "psn_alice",
566
566
  ]);
567
567
  expect(sync.calls[1].personalMode).toBe(true);
568
- expect(sync.calls[1].journalSlug).toBe("personal");
568
+ expect(sync.calls[1].journalSlug).toBe("__hq_personal_vault__");
569
569
  });
570
570
 
571
571
  it("skipPersonal=true with empty memberships produces zero sync calls", async () => {
@@ -176,7 +176,7 @@ describe("pushAll", () => {
176
176
  expect(share.calls[2]).toMatchObject({
177
177
  company: "psn_alice",
178
178
  personalMode: true,
179
- journalSlug: "personal",
179
+ journalSlug: "__hq_personal_vault__",
180
180
  skipUnchanged: true,
181
181
  propagateDeletes: true,
182
182
  });
@@ -346,7 +346,7 @@ describe("pushAll", () => {
346
346
  expect(share.calls).toHaveLength(1);
347
347
  expect(share.calls[0].company).toBe("psn_old");
348
348
  expect(share.calls[0].personalMode).toBe(true);
349
- expect(share.calls[0].journalSlug).toBe("personal");
349
+ expect(share.calls[0].journalSlug).toBe("__hq_personal_vault__");
350
350
  });
351
351
 
352
352
  // ── 7. Aggregate counts include deletes ───────────────────────────────────
@@ -451,6 +451,6 @@ describe("pushAll", () => {
451
451
  "psn_alice",
452
452
  ]);
453
453
  expect(share.calls[1].personalMode).toBe(true);
454
- expect(share.calls[1].journalSlug).toBe("personal");
454
+ expect(share.calls[1].journalSlug).toBe("__hq_personal_vault__");
455
455
  });
456
456
  });
@@ -26,6 +26,7 @@ import {
26
26
  loadCachedTokens,
27
27
  VaultClient,
28
28
  computePersonalVaultPaths,
29
+ PERSONAL_VAULT_JOURNAL_SLUG,
29
30
  type ConflictStrategy,
30
31
  type EntityContext,
31
32
  type MembershipSyncConfig,
@@ -301,13 +302,18 @@ export async function pullAll(
301
302
 
302
303
  const personal = options.skipPersonal ? null : pickCanonicalPerson(persons);
303
304
  if (personal) {
305
+ // Canonical slug, NOT the literal "personal": that legacy slug is the
306
+ // companies/personal company's journal. hq-sync-runner moved the vault
307
+ // slot to PERSONAL_VAULT_JOURNAL_SLUG; hq-cli journaling the same vault
308
+ // under the old slug split the bookkeeping across two files — every CLI
309
+ // sync re-flagged the menubar's already-synced files as conflicts.
304
310
  plan.push({
305
311
  slug: "personal",
306
312
  syncOptions: {
307
313
  company: personal.uid,
308
314
  hqRoot: options.hqRoot,
309
315
  personalMode: true,
310
- journalSlug: "personal",
316
+ journalSlug: PERSONAL_VAULT_JOURNAL_SLUG,
311
317
  ...(options.onConflict ? { onConflict: options.onConflict } : {}),
312
318
  },
313
319
  });
@@ -442,7 +448,7 @@ export async function pushAll(
442
448
  hqRoot: options.hqRoot,
443
449
  paths: computePersonalVaultPaths(options.hqRoot),
444
450
  personalMode: true,
445
- journalSlug: "personal",
451
+ journalSlug: PERSONAL_VAULT_JOURNAL_SLUG,
446
452
  skipUnchanged: true,
447
453
  propagateDeletes: true,
448
454
  propagateDeletePolicy: resolveDeletePolicy(),
@@ -821,7 +827,7 @@ export function registerCloudCommands(program: Command): void {
821
827
  getEntity: async () => null,
822
828
  });
823
829
  personalMode = true;
824
- journalSlug = "personal";
830
+ journalSlug = PERSONAL_VAULT_JOURNAL_SLUG;
825
831
  }
826
832
 
827
833
  // Default paths. For `--personal` with no explicit paths, default to
@@ -1349,7 +1355,7 @@ async function runPullPersonal(
1349
1355
  vaultConfig,
1350
1356
  hqRoot,
1351
1357
  personalMode: true,
1352
- journalSlug: "personal",
1358
+ journalSlug: PERSONAL_VAULT_JOURNAL_SLUG,
1353
1359
  });
1354
1360
 
1355
1361
  if (result.aborted) {
@@ -1510,7 +1516,7 @@ async function runNowSingle(
1510
1516
  getEntity: async () => null,
1511
1517
  });
1512
1518
  personalMode = true;
1513
- journalSlug = "personal";
1519
+ journalSlug = PERSONAL_VAULT_JOURNAL_SLUG;
1514
1520
  pushPaths = computePersonalVaultPaths(hqRoot);
1515
1521
  } else {
1516
1522
  // For company targets we need a concrete slug to compute the push path.
@@ -1,5 +1,32 @@
1
- import { describe, it, expect } from "vitest";
2
- import { detectRecipient, parseDuration, buildDmBody } from "./dm.js";
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ vi,
8
+ type MockInstance,
9
+ } from "vitest";
10
+
11
+ // Stub ensureCognitoToken so the action handlers run without keychain/disk I/O.
12
+ vi.mock("../utils/cognito-session.js", async (importOriginal) => {
13
+ const original = (await importOriginal()) as Record<string, unknown>;
14
+ return {
15
+ ...original,
16
+ ensureCognitoToken: vi.fn(async () => "test-token"),
17
+ };
18
+ });
19
+
20
+ import { Command } from "commander";
21
+ import {
22
+ detectRecipient,
23
+ parseDuration,
24
+ buildDmBody,
25
+ matchRequest,
26
+ buildConnectionActionBody,
27
+ registerDmCommand,
28
+ type ConnectionRequest,
29
+ } from "./dm.js";
3
30
 
4
31
  describe("detectRecipient", () => {
5
32
  it("classifies an email", () => {
@@ -86,3 +113,185 @@ describe("buildDmBody", () => {
86
113
  expect(() => buildDmBody({ recipient: "a@b.com", message: "m", inDelay: "soon", now })).toThrow(/Invalid --in/);
87
114
  });
88
115
  });
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // matchRequest (pure)
119
+ // ---------------------------------------------------------------------------
120
+
121
+ const SAMPLE_REQUESTS: ConnectionRequest[] = [
122
+ {
123
+ pairKey: "pair_abc",
124
+ fromPersonUid: "prs_alice",
125
+ fromDisplayName: "Alice",
126
+ fromEmail: "alice@example.com",
127
+ message: "hey, lets connect",
128
+ createdAt: "2026-06-01T00:00:00.000Z",
129
+ },
130
+ {
131
+ pairKey: "pair_def",
132
+ fromPersonUid: "prs_bob",
133
+ fromDisplayName: "Bob",
134
+ fromEmail: "Bob@Example.com",
135
+ },
136
+ ];
137
+
138
+ describe("matchRequest", () => {
139
+ it("matches by email case-insensitively", () => {
140
+ expect(matchRequest(SAMPLE_REQUESTS, "ALICE@example.com")?.pairKey).toBe("pair_abc");
141
+ expect(matchRequest(SAMPLE_REQUESTS, "bob@example.com")?.pairKey).toBe("pair_def");
142
+ });
143
+ it("matches by personUid", () => {
144
+ expect(matchRequest(SAMPLE_REQUESTS, "prs_bob")?.pairKey).toBe("pair_def");
145
+ });
146
+ it("matches by pairKey", () => {
147
+ expect(matchRequest(SAMPLE_REQUESTS, "pair_abc")?.fromPersonUid).toBe("prs_alice");
148
+ });
149
+ it("returns null when nothing matches", () => {
150
+ expect(matchRequest(SAMPLE_REQUESTS, "nobody@example.com")).toBeNull();
151
+ });
152
+ });
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // buildConnectionActionBody (pure)
156
+ // ---------------------------------------------------------------------------
157
+
158
+ describe("buildConnectionActionBody", () => {
159
+ it("prefers the matched request's pairKey", () => {
160
+ expect(buildConnectionActionBody("alice@example.com", SAMPLE_REQUESTS[0])).toEqual({
161
+ pairKey: "pair_abc",
162
+ });
163
+ });
164
+ it("falls back to withPersonUid for an unmatched prs_ id", () => {
165
+ expect(buildConnectionActionBody("prs_zed", null)).toEqual({ withPersonUid: "prs_zed" });
166
+ });
167
+ it("falls back to withEmail for an unmatched email", () => {
168
+ expect(buildConnectionActionBody("Stranger@Example.com", null)).toEqual({
169
+ withEmail: "stranger@example.com",
170
+ });
171
+ });
172
+ it("throws on an unresolvable identifier", () => {
173
+ expect(() => buildConnectionActionBody("not-an-id", null)).toThrow(/Could not resolve/);
174
+ });
175
+ });
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Action handlers (HTTP/auth mocked)
179
+ // ---------------------------------------------------------------------------
180
+
181
+ function jsonResponse(status: number, body: unknown): Response {
182
+ return new Response(JSON.stringify(body), {
183
+ status,
184
+ headers: { "Content-Type": "application/json" },
185
+ });
186
+ }
187
+
188
+ describe("dm command actions", () => {
189
+ let fetchSpy: MockInstance<typeof fetch>;
190
+ let exitSpy: MockInstance<typeof process.exit>;
191
+ let logSpy: MockInstance<typeof console.log>;
192
+ let errSpy: MockInstance<typeof console.error>;
193
+ let program: Command;
194
+
195
+ beforeEach(() => {
196
+ fetchSpy = vi.spyOn(globalThis, "fetch");
197
+ exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
198
+ throw new Error(`__EXIT__:${code ?? 0}`);
199
+ }) as never);
200
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
201
+ errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
202
+ program = new Command();
203
+ registerDmCommand(program);
204
+ });
205
+
206
+ afterEach(() => {
207
+ vi.restoreAllMocks();
208
+ });
209
+
210
+ function logged(): string {
211
+ return logSpy.mock.calls.map((c) => String(c[0])).join("\n");
212
+ }
213
+
214
+ it("send: prints delivered on 200", async () => {
215
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, { eventId: "evt_1" }));
216
+ await program.parseAsync(["dm", "alice@example.com", "hi there"], { from: "user" });
217
+ const url = String(fetchSpy.mock.calls[0][0]);
218
+ expect(url).toContain("/v1/notify/dm");
219
+ expect(logged()).toMatch(/DM sent to alice@example.com/);
220
+ expect(errSpy).not.toHaveBeenCalled();
221
+ });
222
+
223
+ it("send: prints pending request on 202 connection_requested (not an error)", async () => {
224
+ fetchSpy.mockResolvedValueOnce(
225
+ jsonResponse(202, { state: "connection_requested" }),
226
+ );
227
+ await program.parseAsync(["dm", "stranger@example.com", "hello"], { from: "user" });
228
+ expect(logged()).toMatch(/Request sent — pending stranger@example\.com's acceptance\./);
229
+ // 202 is a success, not an error path.
230
+ expect(exitSpy).not.toHaveBeenCalled();
231
+ expect(errSpy).not.toHaveBeenCalled();
232
+ });
233
+
234
+ it("requests: lists pending incoming requests with identifiers", async () => {
235
+ fetchSpy.mockResolvedValueOnce(
236
+ jsonResponse(200, { requests: SAMPLE_REQUESTS }),
237
+ );
238
+ await program.parseAsync(["dm", "requests"], { from: "user" });
239
+ const url = String(fetchSpy.mock.calls[0][0]);
240
+ expect(url).toContain("/v1/notify/connections/requests");
241
+ const out = logged();
242
+ expect(out).toMatch(/2 pending connection requests/);
243
+ expect(out).toContain("Alice");
244
+ expect(out).toContain("alice@example.com");
245
+ expect(out).toContain('"hey, lets connect"');
246
+ });
247
+
248
+ it("requests: handles an empty list quietly", async () => {
249
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, { requests: [] }));
250
+ await program.parseAsync(["dm", "requests"], { from: "user" });
251
+ expect(logged()).toMatch(/No pending connection requests/);
252
+ });
253
+
254
+ it("accept: resolves email to pairKey and hits the accept endpoint", async () => {
255
+ // 1st call: fetch requests; 2nd call: accept.
256
+ fetchSpy
257
+ .mockResolvedValueOnce(jsonResponse(200, { requests: SAMPLE_REQUESTS }))
258
+ .mockResolvedValueOnce(jsonResponse(200, { state: "active" }));
259
+ await program.parseAsync(["dm", "accept", "alice@example.com"], { from: "user" });
260
+ const acceptCall = fetchSpy.mock.calls.find((c) =>
261
+ String(c[0]).includes("/v1/notify/connections/accept"),
262
+ );
263
+ expect(acceptCall).toBeTruthy();
264
+ const body = JSON.parse((acceptCall![1]?.body as string) ?? "{}");
265
+ expect(body).toEqual({ pairKey: "pair_abc" });
266
+ expect(logged()).toMatch(/Connection accepted — Alice\./);
267
+ });
268
+
269
+ it("decline: hits the decline endpoint", async () => {
270
+ fetchSpy
271
+ .mockResolvedValueOnce(jsonResponse(200, { requests: SAMPLE_REQUESTS }))
272
+ .mockResolvedValueOnce(jsonResponse(200, { state: "declined" }));
273
+ await program.parseAsync(["dm", "decline", "prs_bob"], { from: "user" });
274
+ const call = fetchSpy.mock.calls.find((c) =>
275
+ String(c[0]).includes("/v1/notify/connections/decline"),
276
+ );
277
+ expect(call).toBeTruthy();
278
+ const body = JSON.parse((call![1]?.body as string) ?? "{}");
279
+ expect(body).toEqual({ pairKey: "pair_def" });
280
+ expect(logged()).toMatch(/Request declined — Bob\./);
281
+ });
282
+
283
+ it("block: hits the block endpoint, falling back to withEmail for a stranger", async () => {
284
+ // No matching request → falls back to withEmail.
285
+ fetchSpy
286
+ .mockResolvedValueOnce(jsonResponse(200, { requests: [] }))
287
+ .mockResolvedValueOnce(jsonResponse(200, { state: "blocked" }));
288
+ await program.parseAsync(["dm", "block", "spammer@example.com"], { from: "user" });
289
+ const call = fetchSpy.mock.calls.find((c) =>
290
+ String(c[0]).includes("/v1/notify/connections/block"),
291
+ );
292
+ expect(call).toBeTruthy();
293
+ const body = JSON.parse((call![1]?.body as string) ?? "{}");
294
+ expect(body).toEqual({ withEmail: "spammer@example.com" });
295
+ expect(logged()).toMatch(/Blocked — spammer@example\.com\./);
296
+ });
297
+ });
@@ -117,11 +117,266 @@ function friendlyDmError(status: number, code: string | undefined, fallback: str
117
117
  return fallback;
118
118
  }
119
119
 
120
+ /** A pending incoming connection request, as returned by the backend. */
121
+ export interface ConnectionRequest {
122
+ pairKey: string;
123
+ fromPersonUid: string;
124
+ fromDisplayName?: string;
125
+ fromEmail?: string;
126
+ message?: string;
127
+ createdAt?: string;
128
+ }
129
+
130
+ /**
131
+ * Resolve a connection request from a pending-requests list by a caller-supplied
132
+ * identifier (email, personUid, or pairKey). Pure → unit-testable.
133
+ *
134
+ * Matching is case-insensitive for emails. Returns the matched request, or null
135
+ * if no request matches.
136
+ */
137
+ export function matchRequest(
138
+ requests: ConnectionRequest[],
139
+ identifier: string,
140
+ ): ConnectionRequest | null {
141
+ const id = identifier.trim();
142
+ const idLower = id.toLowerCase();
143
+ for (const r of requests) {
144
+ if (r.pairKey === id) return r;
145
+ if (r.fromPersonUid === id) return r;
146
+ if (r.fromEmail && r.fromEmail.toLowerCase() === idLower) return r;
147
+ }
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * Build the POST body for an accept/decline/block action. Prefers a resolved
153
+ * pairKey (from a matched pending request); otherwise falls back to the
154
+ * recipient identifier the backend can resolve (`withPersonUid` for a prs_ UID,
155
+ * `toEmail`-style is not supported by the connections endpoints so we send
156
+ * `withEmail`). Pure → unit-testable.
157
+ *
158
+ * Throws Error with a user-facing message when the identifier can't be resolved.
159
+ */
160
+ export function buildConnectionActionBody(
161
+ identifier: string,
162
+ matched: ConnectionRequest | null,
163
+ ): { pairKey: string } | { withPersonUid: string } | { withEmail: string } {
164
+ if (matched) {
165
+ return { pairKey: matched.pairKey };
166
+ }
167
+ const r = identifier.trim();
168
+ const rcpt = detectRecipient(r);
169
+ if (rcpt?.toPersonUid) return { withPersonUid: rcpt.toPersonUid };
170
+ if (rcpt?.toEmail) return { withEmail: rcpt.toEmail };
171
+ throw new Error(
172
+ `Could not resolve '${identifier}' to a pending request — pass the requester's email, personUid, or the pairKey from \`hq dm requests\`.`,
173
+ );
174
+ }
175
+
176
+ const CONNECTION_ACTIONS = {
177
+ accept: { path: "accept", done: "Connection accepted" },
178
+ decline: { path: "decline", done: "Request declined" },
179
+ block: { path: "block", done: "Blocked" },
180
+ } as const;
181
+
182
+ type ConnectionAction = keyof typeof CONNECTION_ACTIONS;
183
+
184
+ /** Fetch the caller's pending incoming connection requests. */
185
+ async function fetchRequests(token: string): Promise<ConnectionRequest[]> {
186
+ const res = await vaultApiFetch({
187
+ token,
188
+ path: "/v1/notify/connections/requests",
189
+ });
190
+ if (!res.ok) {
191
+ const err = (await res.json().catch(() => ({}))) as Record<string, string>;
192
+ throw new Error(
193
+ friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText),
194
+ );
195
+ }
196
+ const data = (await res.json()) as { requests?: ConnectionRequest[] };
197
+ return data.requests ?? [];
198
+ }
199
+
200
+ async function runConnectionAction(
201
+ action: ConnectionAction,
202
+ identifier: string,
203
+ ): Promise<void> {
204
+ try {
205
+ const token = await ensureCognitoToken();
206
+ // Resolve the pairKey from the caller's pending requests when possible so
207
+ // accept/decline/block work against a friendly email or personUid.
208
+ const requests = await fetchRequests(token).catch(() => [] as ConnectionRequest[]);
209
+ const matched = matchRequest(requests, identifier);
210
+ const body = buildConnectionActionBody(identifier, matched);
211
+
212
+ const res = await vaultApiFetch({
213
+ token,
214
+ path: `/v1/notify/connections/${CONNECTION_ACTIONS[action].path}`,
215
+ method: "POST",
216
+ body: body as unknown as Record<string, unknown>,
217
+ });
218
+ if (!res.ok) {
219
+ const err = (await res.json().catch(() => ({}))) as Record<string, string>;
220
+ console.error(
221
+ chalk.red(
222
+ friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText),
223
+ ),
224
+ );
225
+ process.exit(1);
226
+ }
227
+
228
+ const who = matched?.fromDisplayName ?? matched?.fromEmail ?? identifier;
229
+ console.log(chalk.green(`${CONNECTION_ACTIONS[action].done} — ${who}.`));
230
+ if (action === "accept" && matched?.message) {
231
+ console.log(chalk.dim("Their held message will now be delivered."));
232
+ }
233
+ } catch (err) {
234
+ console.error(
235
+ chalk.red("Error:"),
236
+ err instanceof Error ? err.message : String(err),
237
+ );
238
+ process.exit(1);
239
+ }
240
+ }
241
+
242
+ interface DmSendOpts {
243
+ prompt?: string;
244
+ promptFile?: string;
245
+ details?: string;
246
+ detailsFile?: string;
247
+ at?: string;
248
+ in?: string;
249
+ }
250
+
251
+ async function runDmSend(
252
+ recipient: string,
253
+ message: string | undefined,
254
+ opts: DmSendOpts,
255
+ ): Promise<void> {
256
+ try {
257
+ // Resolve prompt/details from inline text or a file.
258
+ let prompt = opts.prompt;
259
+ if (opts.promptFile) prompt = readFileSync(opts.promptFile, "utf8");
260
+ let details = opts.details;
261
+ if (opts.detailsFile) details = readFileSync(opts.detailsFile, "utf8");
262
+
263
+ const reqBody = buildDmBody({
264
+ recipient,
265
+ message: message ?? "",
266
+ prompt,
267
+ details,
268
+ at: opts.at,
269
+ inDelay: opts.in,
270
+ now: Date.now(),
271
+ });
272
+
273
+ const token = await ensureCognitoToken();
274
+ const res = await vaultApiFetch({
275
+ token,
276
+ path: "/v1/notify/dm",
277
+ method: "POST",
278
+ body: reqBody as unknown as Record<string, unknown>,
279
+ });
280
+
281
+ if (!res.ok) {
282
+ const err = (await res.json().catch(() => ({}))) as Record<string, string>;
283
+ console.error(
284
+ chalk.red(
285
+ friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText),
286
+ ),
287
+ );
288
+ process.exit(1);
289
+ }
290
+
291
+ // 202 connection_requested: the recipient isn't connected yet, so the DM is
292
+ // held as a connection request rather than delivered. This is a success, not
293
+ // an error — surface it quietly.
294
+ const data = (await res.json()) as {
295
+ eventId?: string;
296
+ createdAt?: string;
297
+ scheduled?: boolean;
298
+ deliverAt?: string;
299
+ state?: string;
300
+ };
301
+
302
+ if (res.status === 202 || data.state === "connection_requested") {
303
+ console.log(
304
+ chalk.green(`Request sent — pending ${recipient}'s acceptance.`),
305
+ );
306
+ console.log(
307
+ chalk.dim("Your message is held and delivers automatically once they accept."),
308
+ );
309
+ return;
310
+ }
311
+
312
+ if (data.scheduled) {
313
+ console.log(
314
+ chalk.green(
315
+ `Scheduled DM to ${recipient} for ${data.deliverAt} (eventId ${data.eventId}).`,
316
+ ),
317
+ );
318
+ console.log(
319
+ chalk.dim("It delivers within ~60s of that time, even if you're offline."),
320
+ );
321
+ } else {
322
+ console.log(
323
+ chalk.green(`DM sent to ${recipient} (eventId ${data.eventId}).`),
324
+ );
325
+ }
326
+ } catch (err) {
327
+ console.error(
328
+ chalk.red("Error:"),
329
+ err instanceof Error ? err.message : String(err),
330
+ );
331
+ process.exit(1);
332
+ }
333
+ }
334
+
335
+ async function runDmRequests(): Promise<void> {
336
+ try {
337
+ const token = await ensureCognitoToken();
338
+ const requests = await fetchRequests(token);
339
+ if (requests.length === 0) {
340
+ console.log(chalk.dim("No pending connection requests."));
341
+ return;
342
+ }
343
+ console.log(
344
+ chalk.green(
345
+ `${requests.length} pending connection request${requests.length === 1 ? "" : "s"}:`,
346
+ ),
347
+ );
348
+ for (const r of requests) {
349
+ const name = r.fromDisplayName ?? r.fromEmail ?? r.fromPersonUid;
350
+ const emailPart = r.fromEmail ? chalk.dim(` <${r.fromEmail}>`) : "";
351
+ console.log(`\n ${chalk.bold(name)}${emailPart}`);
352
+ if (r.message) {
353
+ console.log(` ${chalk.dim('"' + r.message + '"')}`);
354
+ }
355
+ const idHint = r.fromEmail ?? r.fromPersonUid ?? r.pairKey;
356
+ console.log(
357
+ chalk.dim(` Accept: hq dm accept ${idHint} · Decline: hq dm decline ${idHint}`),
358
+ );
359
+ }
360
+ } catch (err) {
361
+ console.error(
362
+ chalk.red("Error:"),
363
+ err instanceof Error ? err.message : String(err),
364
+ );
365
+ process.exit(1);
366
+ }
367
+ }
368
+
120
369
  export function registerDmCommand(program: Command): void {
121
- program
122
- .command("dm <recipient> [message]")
370
+ const dm = program
371
+ .command("dm")
123
372
  .description(
124
- "Send a direct message to a teammate (email or personUid). They receive it as an HQ Sync notification.",
373
+ "Send a direct message and manage connection requests.",
374
+ );
375
+
376
+ dm
377
+ .command("send <recipient> [message]", { isDefault: true, hidden: true })
378
+ .description(
379
+ "Send a direct message to someone (email or personUid). They receive it as an HQ Sync notification. If you aren't connected yet, it sends a connection request that holds your message.",
125
380
  )
126
381
  .option(
127
382
  "--prompt <text>",
@@ -145,78 +400,39 @@ export function registerDmCommand(program: Command): void {
145
400
  async (
146
401
  recipient: string,
147
402
  message: string | undefined,
148
- opts: {
149
- prompt?: string;
150
- promptFile?: string;
151
- details?: string;
152
- detailsFile?: string;
153
- at?: string;
154
- in?: string;
155
- },
403
+ opts: DmSendOpts,
156
404
  ) => {
157
- try {
158
- // Resolve prompt/details from inline text or a file.
159
- let prompt = opts.prompt;
160
- if (opts.promptFile) prompt = readFileSync(opts.promptFile, "utf8");
161
- let details = opts.details;
162
- if (opts.detailsFile) details = readFileSync(opts.detailsFile, "utf8");
163
-
164
- const reqBody = buildDmBody({
165
- recipient,
166
- message: message ?? "",
167
- prompt,
168
- details,
169
- at: opts.at,
170
- inDelay: opts.in,
171
- now: Date.now(),
172
- });
173
-
174
- const token = await ensureCognitoToken();
175
- const res = await vaultApiFetch({
176
- token,
177
- path: "/v1/notify/dm",
178
- method: "POST",
179
- body: reqBody as unknown as Record<string, unknown>,
180
- });
181
-
182
- if (!res.ok) {
183
- const err = (await res.json().catch(() => ({}))) as Record<string, string>;
184
- console.error(
185
- chalk.red(
186
- friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText),
187
- ),
188
- );
189
- process.exit(1);
190
- }
191
-
192
- const data = (await res.json()) as {
193
- eventId?: string;
194
- createdAt?: string;
195
- scheduled?: boolean;
196
- deliverAt?: string;
197
- };
198
-
199
- if (data.scheduled) {
200
- console.log(
201
- chalk.green(
202
- `Scheduled DM to ${recipient} for ${data.deliverAt} (eventId ${data.eventId}).`,
203
- ),
204
- );
205
- console.log(
206
- chalk.dim("It delivers within ~60s of that time, even if you're offline."),
207
- );
208
- } else {
209
- console.log(
210
- chalk.green(`DM sent to ${recipient} (eventId ${data.eventId}).`),
211
- );
212
- }
213
- } catch (err) {
214
- console.error(
215
- chalk.red("Error:"),
216
- err instanceof Error ? err.message : String(err),
217
- );
218
- process.exit(1);
219
- }
405
+ await runDmSend(recipient, message, opts);
220
406
  },
221
407
  );
408
+
409
+ dm
410
+ .command("requests")
411
+ .description("List your pending incoming connection requests.")
412
+ .action(async () => {
413
+ await runDmRequests();
414
+ });
415
+
416
+ dm
417
+ .command("accept <person>")
418
+ .description(
419
+ "Accept a pending connection request (by email, personUid, or pairKey). Their held message is then delivered.",
420
+ )
421
+ .action(async (person: string) => {
422
+ await runConnectionAction("accept", person);
423
+ });
424
+
425
+ dm
426
+ .command("decline <person>")
427
+ .description("Decline a pending connection request (by email, personUid, or pairKey).")
428
+ .action(async (person: string) => {
429
+ await runConnectionAction("decline", person);
430
+ });
431
+
432
+ dm
433
+ .command("block <person>")
434
+ .description("Block a person so they can't send you further requests (by email, personUid, or pairKey).")
435
+ .action(async (person: string) => {
436
+ await runConnectionAction("block", person);
437
+ });
222
438
  }