@indigoai-us/hq-cli 5.68.2 → 5.70.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.
- package/CHANGELOG.md +30 -0
- package/dist/commands/channels.js +6 -3
- package/dist/commands/dm.d.ts +76 -0
- package/dist/commands/dm.js +317 -3
- package/dist/commands/outposts.d.ts +16 -1
- package/dist/commands/outposts.js +230 -3
- package/dist/main.js +6 -2
- package/dist/utils/cli-telemetry.d.ts +6 -0
- package/dist/utils/cli-telemetry.js +60 -0
- package/dist/utils/cognito-session.d.ts +2 -1
- package/dist/utils/cognito-session.js +5 -2
- package/dist/utils/vault-api.d.ts +1 -0
- package/dist/utils/vault-api.js +3 -2
- package/package.json +1 -1
- package/src/commands/channels.ts +4 -1
- package/src/commands/dm.test.ts +265 -0
- package/src/commands/dm.ts +449 -1
- package/src/commands/outposts-self-deploy.test.ts +243 -0
- package/src/commands/outposts.ts +346 -1
- package/src/main.ts +5 -0
- package/src/utils/cli-telemetry.test.ts +153 -0
- package/src/utils/cli-telemetry.ts +61 -0
- package/src/utils/cognito-session.ts +4 -0
- package/src/utils/vault-api.test.ts +18 -0
- package/src/utils/vault-api.ts +2 -0
package/src/commands/dm.test.ts
CHANGED
|
@@ -18,6 +18,7 @@ vi.mock("../utils/cognito-session.js", async (importOriginal) => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
import { Command } from "commander";
|
|
21
|
+
import chalk from "chalk";
|
|
21
22
|
import {
|
|
22
23
|
detectRecipient,
|
|
23
24
|
parseDuration,
|
|
@@ -29,8 +30,18 @@ import {
|
|
|
29
30
|
channelSlug,
|
|
30
31
|
parseChannelName,
|
|
31
32
|
matchChannelsByName,
|
|
33
|
+
senderLabel,
|
|
34
|
+
formatRelativeTime,
|
|
35
|
+
isUnread,
|
|
36
|
+
unreadEventIds,
|
|
37
|
+
filterUnread,
|
|
38
|
+
firstLine,
|
|
39
|
+
buildThreadQuery,
|
|
40
|
+
formatInboxEvent,
|
|
41
|
+
formatThreadMessage,
|
|
32
42
|
type ConnectionRequest,
|
|
33
43
|
type ChannelSummary,
|
|
44
|
+
type DmInboxEvent,
|
|
34
45
|
} from "./dm.js";
|
|
35
46
|
|
|
36
47
|
describe("parseGroupRecipients", () => {
|
|
@@ -262,6 +273,127 @@ describe("buildConnectionActionBody", () => {
|
|
|
262
273
|
// Action handlers (HTTP/auth mocked)
|
|
263
274
|
// ---------------------------------------------------------------------------
|
|
264
275
|
|
|
276
|
+
describe("senderLabel", () => {
|
|
277
|
+
it("prefers display name, then email, then uid", () => {
|
|
278
|
+
expect(
|
|
279
|
+
senderLabel({ fromDisplayName: "Jonathan", fromEmail: "j@x.com", fromPersonUid: "prs_1" }),
|
|
280
|
+
).toBe("Jonathan");
|
|
281
|
+
expect(senderLabel({ fromEmail: "j@x.com", fromPersonUid: "prs_1" })).toBe("j@x.com");
|
|
282
|
+
expect(senderLabel({ fromPersonUid: "prs_1" })).toBe("prs_1");
|
|
283
|
+
expect(senderLabel({})).toBe("unknown");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("formatRelativeTime", () => {
|
|
288
|
+
const now = Date.parse("2026-07-16T12:00:00.000Z");
|
|
289
|
+
it("renders sub-minute as 'just now'", () => {
|
|
290
|
+
expect(formatRelativeTime("2026-07-16T11:59:30.000Z", now)).toBe("just now");
|
|
291
|
+
});
|
|
292
|
+
it("renders minutes, hours, and days", () => {
|
|
293
|
+
expect(formatRelativeTime("2026-07-16T11:57:00.000Z", now)).toBe("3m ago");
|
|
294
|
+
expect(formatRelativeTime("2026-07-16T10:00:00.000Z", now)).toBe("2h ago");
|
|
295
|
+
expect(formatRelativeTime("2026-07-11T12:00:00.000Z", now)).toBe("5d ago");
|
|
296
|
+
});
|
|
297
|
+
it("falls back to a date for anything older than a week", () => {
|
|
298
|
+
expect(formatRelativeTime("2026-06-01T09:00:00.000Z", now)).toBe("2026-06-01");
|
|
299
|
+
});
|
|
300
|
+
it("echoes an unparseable input", () => {
|
|
301
|
+
expect(formatRelativeTime("not-a-date", now)).toBe("not-a-date");
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("isUnread / unreadEventIds / filterUnread", () => {
|
|
306
|
+
const events: DmInboxEvent[] = [
|
|
307
|
+
{ eventId: "a", body: "x", createdAt: "2026-07-16T11:00:00.000Z" },
|
|
308
|
+
{
|
|
309
|
+
eventId: "b",
|
|
310
|
+
body: "y",
|
|
311
|
+
createdAt: "2026-07-16T11:30:00.000Z",
|
|
312
|
+
acknowledgedAt: "2026-07-16T11:31:00.000Z",
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
it("treats a message with no acknowledgedAt as unread", () => {
|
|
316
|
+
expect(isUnread(events[0])).toBe(true);
|
|
317
|
+
expect(isUnread(events[1])).toBe(false);
|
|
318
|
+
});
|
|
319
|
+
it("collects unread event ids", () => {
|
|
320
|
+
expect(unreadEventIds(events)).toEqual(["a"]);
|
|
321
|
+
});
|
|
322
|
+
it("filters to unread only", () => {
|
|
323
|
+
expect(filterUnread(events).map((e) => e.eventId)).toEqual(["a"]);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("firstLine", () => {
|
|
328
|
+
it("collapses whitespace to a single trimmed line", () => {
|
|
329
|
+
expect(firstLine(" hello\n world \t there ")).toBe("hello world there");
|
|
330
|
+
});
|
|
331
|
+
it("truncates with an ellipsis past the cap", () => {
|
|
332
|
+
const out = firstLine("abcdefghij", 5);
|
|
333
|
+
expect(out).toBe("abcd…");
|
|
334
|
+
expect(out.length).toBe(5);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("buildThreadQuery", () => {
|
|
339
|
+
it("routes an email through withEmail (lowercased)", () => {
|
|
340
|
+
expect(buildThreadQuery("Jonathan@GetIndigo.ai")).toEqual({
|
|
341
|
+
withEmail: "jonathan@getindigo.ai",
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
it("routes a personUid / agentUid through withPersonUid", () => {
|
|
345
|
+
expect(buildThreadQuery("prs_123")).toEqual({ withPersonUid: "prs_123" });
|
|
346
|
+
expect(buildThreadQuery("agt_123")).toEqual({ withPersonUid: "agt_123" });
|
|
347
|
+
});
|
|
348
|
+
it("rejects a bare name", () => {
|
|
349
|
+
expect(() => buildThreadQuery("Jonathan")).toThrow(/Resolve a name first/);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("format helpers (rendering)", () => {
|
|
354
|
+
beforeEach(() => {
|
|
355
|
+
chalk.level = 0; // plain text so assertions read the rendered content
|
|
356
|
+
});
|
|
357
|
+
const now = Date.parse("2026-07-16T12:00:00.000Z");
|
|
358
|
+
it("marks an unread inbox row with a bullet and shows the sender + body", () => {
|
|
359
|
+
const row = formatInboxEvent(
|
|
360
|
+
{
|
|
361
|
+
eventId: "e1",
|
|
362
|
+
fromDisplayName: "Jonathan Bach",
|
|
363
|
+
fromEmail: "jonathan@getindigo.ai",
|
|
364
|
+
body: "grp_jb_agent is created",
|
|
365
|
+
createdAt: "2026-07-16T11:57:00.000Z",
|
|
366
|
+
},
|
|
367
|
+
now,
|
|
368
|
+
);
|
|
369
|
+
expect(row).toContain("●");
|
|
370
|
+
expect(row).toContain("Jonathan Bach");
|
|
371
|
+
expect(row).toContain("<jonathan@getindigo.ai>");
|
|
372
|
+
expect(row).toContain("3m ago");
|
|
373
|
+
expect(row).toContain("grp_jb_agent is created");
|
|
374
|
+
});
|
|
375
|
+
it("tags thread direction: outgoing as 'you', incoming by sender", () => {
|
|
376
|
+
const out = formatThreadMessage(
|
|
377
|
+
{ eventId: "o", body: "hey", createdAt: "2026-07-16T11:00:00.000Z", direction: "out" },
|
|
378
|
+
now,
|
|
379
|
+
);
|
|
380
|
+
expect(out).toContain("→");
|
|
381
|
+
expect(out).toContain("you");
|
|
382
|
+
const inbound = formatThreadMessage(
|
|
383
|
+
{
|
|
384
|
+
eventId: "i",
|
|
385
|
+
fromDisplayName: "Jonathan",
|
|
386
|
+
body: "thanks",
|
|
387
|
+
createdAt: "2026-07-16T11:30:00.000Z",
|
|
388
|
+
direction: "in",
|
|
389
|
+
},
|
|
390
|
+
now,
|
|
391
|
+
);
|
|
392
|
+
expect(inbound).toContain("←");
|
|
393
|
+
expect(inbound).toContain("Jonathan");
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
265
397
|
function jsonResponse(status: number, body: unknown): Response {
|
|
266
398
|
return new Response(JSON.stringify(body), {
|
|
267
399
|
status,
|
|
@@ -480,4 +612,137 @@ describe("dm command actions", () => {
|
|
|
480
612
|
expect(body).toEqual({ withEmail: "spammer@example.com" });
|
|
481
613
|
expect(logged()).toMatch(/Blocked — spammer@example\.com\./);
|
|
482
614
|
});
|
|
615
|
+
|
|
616
|
+
// ---- receive side: inbox / thread / channel ----
|
|
617
|
+
|
|
618
|
+
it("inbox: lists incoming messages from GET /v1/notify/inbox", async () => {
|
|
619
|
+
fetchSpy.mockResolvedValueOnce(
|
|
620
|
+
jsonResponse(200, {
|
|
621
|
+
events: [
|
|
622
|
+
{
|
|
623
|
+
eventId: "e1",
|
|
624
|
+
fromDisplayName: "Jonathan Bach",
|
|
625
|
+
fromEmail: "jonathan@getindigo.ai",
|
|
626
|
+
body: "grp_jb_agent is created",
|
|
627
|
+
createdAt: "2026-07-16T11:57:00.000Z",
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
}),
|
|
631
|
+
);
|
|
632
|
+
await program.parseAsync(["dm", "inbox"], { from: "user" });
|
|
633
|
+
expect(String(fetchSpy.mock.calls[0][0])).toContain("/v1/notify/inbox");
|
|
634
|
+
expect(logged()).toContain("Jonathan Bach");
|
|
635
|
+
expect(logged()).toContain("grp_jb_agent is created");
|
|
636
|
+
expect(errSpy).not.toHaveBeenCalled();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("inbox --unread --mark-read: acks only the unread ids", async () => {
|
|
640
|
+
fetchSpy
|
|
641
|
+
.mockResolvedValueOnce(
|
|
642
|
+
jsonResponse(200, {
|
|
643
|
+
events: [
|
|
644
|
+
{ eventId: "u1", body: "new", createdAt: "2026-07-16T11:00:00.000Z" },
|
|
645
|
+
{
|
|
646
|
+
eventId: "r1",
|
|
647
|
+
body: "old",
|
|
648
|
+
createdAt: "2026-07-16T10:00:00.000Z",
|
|
649
|
+
acknowledgedAt: "2026-07-16T10:01:00.000Z",
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
}),
|
|
653
|
+
)
|
|
654
|
+
.mockResolvedValueOnce(jsonResponse(200, { acknowledged: 1 }));
|
|
655
|
+
await program.parseAsync(["dm", "inbox", "--unread", "--mark-read"], {
|
|
656
|
+
from: "user",
|
|
657
|
+
});
|
|
658
|
+
const ackCall = fetchSpy.mock.calls.find((c) =>
|
|
659
|
+
String(c[0]).includes("/v1/notify/inbox/ack"),
|
|
660
|
+
);
|
|
661
|
+
expect(ackCall).toBeTruthy();
|
|
662
|
+
expect(JSON.parse((ackCall![1]?.body as string) ?? "{}")).toEqual({
|
|
663
|
+
eventIds: ["u1"],
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it("thread <email>: reads via withEmail and acks incoming messages", async () => {
|
|
668
|
+
fetchSpy
|
|
669
|
+
.mockResolvedValueOnce(
|
|
670
|
+
jsonResponse(200, {
|
|
671
|
+
messages: [
|
|
672
|
+
{
|
|
673
|
+
eventId: "in1",
|
|
674
|
+
fromDisplayName: "Jonathan",
|
|
675
|
+
body: "thanks!",
|
|
676
|
+
createdAt: "2026-07-16T11:30:00.000Z",
|
|
677
|
+
direction: "in",
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
eventId: "out1",
|
|
681
|
+
body: "done",
|
|
682
|
+
createdAt: "2026-07-16T11:00:00.000Z",
|
|
683
|
+
direction: "out",
|
|
684
|
+
},
|
|
685
|
+
],
|
|
686
|
+
}),
|
|
687
|
+
)
|
|
688
|
+
.mockResolvedValueOnce(jsonResponse(200, { acknowledged: 1 }));
|
|
689
|
+
await program.parseAsync(["dm", "thread", "jonathan@getindigo.ai"], {
|
|
690
|
+
from: "user",
|
|
691
|
+
});
|
|
692
|
+
expect(String(fetchSpy.mock.calls[0][0])).toContain("/v1/notify/thread");
|
|
693
|
+
expect(String(fetchSpy.mock.calls[0][0])).toContain("withEmail=");
|
|
694
|
+
const ackCall = fetchSpy.mock.calls.find((c) =>
|
|
695
|
+
String(c[0]).includes("/v1/notify/inbox/ack"),
|
|
696
|
+
);
|
|
697
|
+
expect(ackCall).toBeTruthy();
|
|
698
|
+
expect(JSON.parse((ackCall![1]?.body as string) ?? "{}")).toEqual({
|
|
699
|
+
eventIds: ["in1"],
|
|
700
|
+
});
|
|
701
|
+
expect(logged()).toContain("thanks!");
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("thread --no-ack: reads without marking anything read", async () => {
|
|
705
|
+
fetchSpy.mockResolvedValueOnce(
|
|
706
|
+
jsonResponse(200, {
|
|
707
|
+
messages: [
|
|
708
|
+
{
|
|
709
|
+
eventId: "in1",
|
|
710
|
+
body: "hi",
|
|
711
|
+
createdAt: "2026-07-16T11:30:00.000Z",
|
|
712
|
+
direction: "in",
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
}),
|
|
716
|
+
);
|
|
717
|
+
await program.parseAsync(["dm", "thread", "prs_x", "--no-ack"], {
|
|
718
|
+
from: "user",
|
|
719
|
+
});
|
|
720
|
+
expect(
|
|
721
|
+
fetchSpy.mock.calls.some((c) => String(c[0]).includes("/inbox/ack")),
|
|
722
|
+
).toBe(false);
|
|
723
|
+
expect(String(fetchSpy.mock.calls[0][0])).toContain("withPersonUid=prs_x");
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("channel <name>: resolves the channel then reads its messages", async () => {
|
|
727
|
+
fetchSpy
|
|
728
|
+
.mockResolvedValueOnce(jsonResponse(200, channelsPayload))
|
|
729
|
+
.mockResolvedValueOnce(
|
|
730
|
+
jsonResponse(200, {
|
|
731
|
+
messages: [
|
|
732
|
+
{
|
|
733
|
+
eventId: "m1",
|
|
734
|
+
fromDisplayName: "Stefan",
|
|
735
|
+
body: "ship it",
|
|
736
|
+
createdAt: "2026-07-16T11:00:00.000Z",
|
|
737
|
+
},
|
|
738
|
+
],
|
|
739
|
+
}),
|
|
740
|
+
);
|
|
741
|
+
await program.parseAsync(["dm", "channel", "vyg-dev"], { from: "user" });
|
|
742
|
+
expect(String(fetchSpy.mock.calls[0][0])).toContain("/v1/notify/channels");
|
|
743
|
+
expect(String(fetchSpy.mock.calls[1][0])).toContain(
|
|
744
|
+
"/v1/notify/channels/chn_v/messages",
|
|
745
|
+
);
|
|
746
|
+
expect(logged()).toContain("ship it");
|
|
747
|
+
});
|
|
483
748
|
});
|