@indigoai-us/hq-cli 5.36.1 → 5.36.2
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/dist/commands/dm.d.ts +33 -0
- package/dist/commands/dm.js +199 -47
- package/package.json +1 -1
- package/src/commands/dm.test.ts +211 -2
- package/src/commands/dm.ts +290 -74
package/dist/commands/dm.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/dm.js
CHANGED
|
@@ -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]="
|
|
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
|
|
95
|
-
.description("Send a direct message
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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=
|
|
300
|
+
//# debugId=19c5d8e0-ec0a-5b57-b3dc-e2209b60cf2f
|
package/package.json
CHANGED
package/src/commands/dm.test.ts
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
+
});
|
package/src/commands/dm.ts
CHANGED
|
@@ -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
|
|
370
|
+
const dm = program
|
|
371
|
+
.command("dm")
|
|
123
372
|
.description(
|
|
124
|
-
"Send a direct message
|
|
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
|
-
|
|
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
|
}
|